Skip to content
This repository was archived by the owner on Dec 24, 2022. It is now read-only.

Commit ca882dc

Browse files
committed
Merge pull request #469 from rsafier/master
Add JsScope.PreserveUtc. When true overrides standard behavior of con…
2 parents eebbf15 + f9e7238 commit ca882dc

File tree

6 files changed

+342
-25
lines changed

6 files changed

+342
-25
lines changed

src/ServiceStack.Text/Common/DateTimeSerializer.cs

Lines changed: 84 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System.Text;
1717
using ServiceStack.Text.Json;
1818
using ServiceStack.Text.Support;
19+
using System.Text.RegularExpressions;
1920

2021
namespace ServiceStack.Text.Common
2122
{
@@ -24,11 +25,14 @@ public static class DateTimeSerializer
2425
public const string CondensedDateTimeFormat = "yyyyMMdd"; //8
2526
public const string ShortDateTimeFormat = "yyyy-MM-dd"; //11
2627
public const string DefaultDateTimeFormat = "dd/MM/yyyy HH:mm:ss"; //20
28+
public const string DefaultDateTimeFormatWithFraction = "dd/MM/yyyy HH:mm:ss.fff"; //24
2729
public const string XsdDateTimeFormat = "yyyy-MM-ddTHH:mm:ss.fffffffZ"; //29
2830
public const string XsdDateTimeFormat3F = "yyyy-MM-ddTHH:mm:ss.fffZ"; //25
2931
public const string XsdDateTimeFormatSeconds = "yyyy-MM-ddTHH:mm:ssZ"; //21
3032
public const string DateTimeFormatSecondsUtcOffset = "yyyy-MM-ddTHH:mm:sszzz"; //22
33+
public const string DateTimeFormatSecondsNoOffset = "yyyy-MM-ddTHH:mm:ss";
3134
public const string DateTimeFormatTicksUtcOffset = "yyyy-MM-ddTHH:mm:ss.fffffffzzz"; //30
35+
public const string DateTimeFormatTicksNoUtcOffset = "yyyy-MM-ddTHH:mm:ss.fffffff";
3236

3337
public const string EscapedWcfJsonPrefix = "\\/Date(";
3438
public const string EscapedWcfJsonSuffix = ")\\/";
@@ -41,20 +45,25 @@ public static class DateTimeSerializer
4145
private static readonly int XsdTimeSeparatorIndex = XsdDateTimeFormat.IndexOf(XsdTimeSeparator);
4246
private const string XsdUtcSuffix = "Z";
4347
private static readonly char[] DateTimeSeperators = new[] { '-', '/' };
44-
48+
private static readonly Regex UtcOffsetInfoRegex = new Regex("([+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])", RegexOptions.Compiled);
4549
public static Func<string, Exception, DateTime> OnParseErrorFn { get; set; }
4650

4751
/// <summary>
48-
/// If AlwaysUseUtc is set to true then convert all DateTime to UTC.
52+
/// If AlwaysUseUtc is set to true then convert all DateTime to UTC. If PreserveUtc is set to true then UTC dates will not convert to local
4953
/// </summary>
5054
/// <param name="dateTime"></param>
5155
/// <returns></returns>
5256
public static DateTime Prepare(this DateTime dateTime, bool parsedAsUtc = false)
5357
{
58+
if (JsConfig.SkipDateTimeConversion)
59+
{
60+
return dateTime;
61+
}
5462
if (JsConfig.AlwaysUseUtc)
5563
{
5664
return dateTime.Kind != DateTimeKind.Utc ? dateTime.ToStableUniversalTime() : dateTime;
5765
}
66+
5867
return parsedAsUtc ? dateTime.ToLocalTime() : dateTime;
5968
}
6069

@@ -91,6 +100,16 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
91100
return unspecifiedDate.Prepare();
92101
}
93102

103+
if (dateTimeStr.Length == DefaultDateTimeFormatWithFraction.Length)
104+
{
105+
var unspecifiedDate = JsConfig.AssumeUtc
106+
? DateTime.Parse(dateTimeStr, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal)
107+
: DateTime.Parse(dateTimeStr, CultureInfo.InvariantCulture);
108+
109+
return unspecifiedDate.Prepare();
110+
}
111+
DateTimeKind kind = DateTimeKind.Unspecified;
112+
94113
switch (JsConfig.DateHandler)
95114
{
96115
case DateHandler.UnixTime:
@@ -103,6 +122,12 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
103122
if (long.TryParse(dateTimeStr, out unixTimeMs))
104123
return unixTimeMs.FromUnixTimeMs();
105124
break;
125+
case DateHandler.ISO8601:
126+
if (JsConfig.SkipDateTimeConversion)
127+
{
128+
dateTimeStr = RemoveUtcOffsets(dateTimeStr, out kind);
129+
}
130+
break;
106131
}
107132

108133
dateTimeStr = RepairXsdTimeSeparator(dateTimeStr);
@@ -139,8 +164,21 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
139164

140165
try
141166
{
142-
var assumeKind = JsConfig.AssumeUtc ? DateTimeStyles.AssumeUniversal : DateTimeStyles.AssumeLocal;
143-
var dateTime = DateTime.Parse(dateTimeStr, CultureInfo.InvariantCulture, assumeKind);
167+
DateTime dateTime;
168+
if (JsConfig.SkipDateTimeConversion)
169+
{
170+
dateTime = DateTime.Parse(dateTimeStr, null,
171+
kind == DateTimeKind.Unspecified ?
172+
DateTimeStyles.None :
173+
kind == DateTimeKind.Local ?
174+
DateTimeStyles.AssumeLocal :
175+
DateTimeStyles.AssumeUniversal);
176+
}
177+
else
178+
{
179+
var assumeKind = JsConfig.AssumeUtc ? DateTimeStyles.AssumeUniversal : DateTimeStyles.AssumeLocal;
180+
dateTime = DateTime.Parse(dateTimeStr, CultureInfo.InvariantCulture, assumeKind);
181+
}
144182
return dateTime.Prepare();
145183
}
146184
catch (FormatException)
@@ -161,6 +199,18 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
161199
}
162200
}
163201

202+
private static string RemoveUtcOffsets(string dateTimeStr, out DateTimeKind kind)
203+
{
204+
var startOfTz = UtcOffsetInfoRegex.Match(dateTimeStr);
205+
if (startOfTz.Index > 0)
206+
{
207+
kind = DateTimeKind.Local;
208+
return dateTimeStr.Substring(0, startOfTz.Index);
209+
}
210+
kind = dateTimeStr.Contains("Z") ? DateTimeKind.Utc : DateTimeKind.Unspecified;
211+
return dateTimeStr;
212+
}
213+
164214
/// <summary>
165215
/// Repairs an out-of-spec XML date/time string which incorrectly uses a space instead of a 'T' to separate the date from the time.
166216
/// These string are occasionally generated by SQLite and can cause errors in OrmLite when reading these columns from the DB.
@@ -180,7 +230,7 @@ private static string RepairXsdTimeSeparator(string dateTimeStr)
180230

181231
public static DateTime? ParseManual(string dateTimeStr)
182232
{
183-
var dateKind = JsConfig.AssumeUtc || JsConfig.AlwaysUseUtc
233+
var dateKind = JsConfig.AssumeUtc || JsConfig.AlwaysUseUtc
184234
? DateTimeKind.Utc
185235
: DateTimeKind.Local;
186236

@@ -201,6 +251,7 @@ private static string RepairXsdTimeSeparator(string dateTimeStr)
201251
if (dateTimeStr.EndsWith(XsdUtcSuffix))
202252
{
203253
dateTimeStr = dateTimeStr.Substring(0, dateTimeStr.Length - 1);
254+
dateKind = JsConfig.SkipDateTimeConversion ? DateTimeKind.Utc : dateKind;
204255
}
205256

206257
var parts = dateTimeStr.Split('T');
@@ -320,7 +371,7 @@ public static DateTimeOffset ParseDateTimeOffset(string dateTimeOffsetStr)
320371
if (Env.IsMono)
321372
{
322373
// Without that Mono uses a Local timezone))
323-
dateTimeOffsetStr = dateTimeOffsetStr.Substring(0, dateTimeOffsetStr.Length - 1) + "+00:00";
374+
dateTimeOffsetStr = dateTimeOffsetStr.Substring(0, dateTimeOffsetStr.Length - 1) + "+00:00";
324375
}
325376
}
326377

@@ -364,7 +415,7 @@ public static TimeSpan ParseTimeSpan(string dateTimeStr)
364415
{
365416
return dateTimeStr.StartsWith("P", StringComparison.Ordinal) || dateTimeStr.StartsWith("-P", StringComparison.Ordinal)
366417
? ParseXsdTimeSpan(dateTimeStr)
367-
: dateTimeStr.Contains(":")
418+
: dateTimeStr.Contains(":")
368419
? TimeSpan.Parse(dateTimeStr)
369420
: ParseNSTimeInterval(dateTimeStr);
370421
}
@@ -399,11 +450,26 @@ public static string ToShortestXsdDateTimeString(DateTime dateTime)
399450
var timeOfDay = dateTime.TimeOfDay;
400451

401452
var isStartOfDay = timeOfDay.Ticks == 0;
402-
if (isStartOfDay)
453+
if (isStartOfDay && !(JsConfig.SkipDateTimeConversion))
403454
return dateTime.ToString(ShortDateTimeFormat);
404455

405-
var hasFractionalSecs = (timeOfDay.Milliseconds != 0)
406-
|| ((timeOfDay.Ticks % TimeSpan.TicksPerMillisecond) != 0);
456+
var hasFractionalSecs = (timeOfDay.Milliseconds != 0)
457+
|| ((timeOfDay.Ticks%TimeSpan.TicksPerMillisecond) != 0);
458+
if (JsConfig.SkipDateTimeConversion)
459+
{
460+
if (!hasFractionalSecs)
461+
return dateTime.Kind == DateTimeKind.Local
462+
? dateTime.ToString(DateTimeFormatSecondsUtcOffset)
463+
: dateTime.Kind == DateTimeKind.Unspecified
464+
? dateTime.ToString(DateTimeFormatSecondsNoOffset)
465+
: dateTime.ToStableUniversalTime().ToString(XsdDateTimeFormatSeconds);
466+
467+
return dateTime.Kind == DateTimeKind.Local
468+
? dateTime.ToString(DateTimeFormatTicksUtcOffset)
469+
: dateTime.Kind == DateTimeKind.Unspecified
470+
? dateTime.ToString(DateTimeFormatTicksNoUtcOffset)
471+
: PclExport.Instance.ToXsdDateTimeString(dateTime);
472+
}
407473
if (!hasFractionalSecs)
408474
return dateTime.Kind != DateTimeKind.Utc
409475
? dateTime.ToString(DateTimeFormatSecondsUtcOffset)
@@ -528,7 +594,15 @@ public static void WriteWcfJsonDate(TextWriter writer, DateTime dateTime)
528594

529595
if (JsConfig.DateHandler == DateHandler.ISO8601)
530596
{
597+
if (!JsConfig.SkipDateTimeConversion)
598+
{
531599
writer.Write(dateTime.ToString("o", CultureInfo.InvariantCulture));
600+
}
601+
else
602+
{
603+
var dt = dateTime.ToString("o", CultureInfo.InvariantCulture);
604+
writer.Write(dt);
605+
}
532606
return;
533607
}
534608

src/ServiceStack.Text/JsConfig.cs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public static JsConfigScope With(
5454
Func<Type, string> typeWriter = null,
5555
Func<string, Type> typeFinder = null,
5656
bool? treatEnumAsInteger = null,
57+
bool? skipDateTimeConversion = null,
5758
bool? alwaysUseUtc = null,
5859
bool? assumeUtc = null,
5960
bool? appendUtcOffset = null,
@@ -70,8 +71,8 @@ public static JsConfigScope With(
7071
TryToParsePrimitiveTypeValues = tryToParsePrimitiveTypeValues ?? sTryToParsePrimitiveTypeValues,
7172
TryToParseNumericType = tryToParseNumericType ?? sTryToParseNumericType,
7273

73-
ParsePrimitiveFloatingPointTypes = parsePrimitiveFloatingPointTypes ?? sParsePrimitiveFloatingPointTypes,
74-
ParsePrimitiveIntegerTypes = parsePrimitiveIntegerTypes ?? sParsePrimitiveIntegerTypes,
74+
ParsePrimitiveFloatingPointTypes = parsePrimitiveFloatingPointTypes ?? sParsePrimitiveFloatingPointTypes,
75+
ParsePrimitiveIntegerTypes = parsePrimitiveIntegerTypes ?? sParsePrimitiveIntegerTypes,
7576

7677
ExcludeDefaultValues = excludeDefaultValues ?? sExcludeDefaultValues,
7778
IncludeNullValues = includeNullValues ?? sIncludeNullValues,
@@ -90,6 +91,7 @@ public static JsConfigScope With(
9091
TypeWriter = typeWriter ?? sTypeWriter,
9192
TypeFinder = typeFinder ?? sTypeFinder,
9293
TreatEnumAsInteger = treatEnumAsInteger ?? sTreatEnumAsInteger,
94+
SkipDateTimeConversion = skipDateTimeConversion ?? sSkipDateTimeConversion,
9395
AlwaysUseUtc = alwaysUseUtc ?? sAlwaysUseUtc,
9496
AssumeUtc = assumeUtc ?? sAssumeUtc,
9597
AppendUtcOffset = appendUtcOffset ?? sAppendUtcOffset,
@@ -511,6 +513,28 @@ public static bool AlwaysUseUtc
511513
}
512514
}
513515

516+
/// <summary>
517+
/// Gets or sets a value indicating if the framework should skip automatic <see cref="DateTime"/> conversions.
518+
/// Dates will be handled literally, any included timezone encoding will be lost and the date will be treaded as DateTimeKind.Local
519+
/// Utc formatted input will result in DateTimeKind.Utc output. Any input without TZ data will be set DateTimeKind.Unspecified
520+
/// This will take precedence over other flags like AlwaysUseUtc
521+
/// JsConfig.DateHandler = DateHandler.ISO8601 should be used when set true for consistent de/serialization.
522+
/// </summary>
523+
private static bool? sSkipDateTimeConversion;
524+
public static bool SkipDateTimeConversion
525+
{
526+
// obeying the use of ThreadStatic, but allowing for setting JsConfig once as is the normal case
527+
get
528+
{
529+
return (JsConfigScope.Current != null ? JsConfigScope.Current.SkipDateTimeConversion : null)
530+
?? sSkipDateTimeConversion
531+
?? false;
532+
}
533+
set
534+
{
535+
if (!sSkipDateTimeConversion.HasValue) sSkipDateTimeConversion = value;
536+
}
537+
}
514538
/// <summary>
515539
/// Gets or sets a value indicating if the framework should always assume <see cref="DateTime"/> is in UTC format if Kind is Unspecified.
516540
/// </summary>

src/ServiceStack.Text/JsConfigScope.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ public void Dispose()
7171
public bool? EmitCamelCaseNames { get; set; }
7272
public bool? EmitLowercaseUnderscoreNames { get; set; }
7373
public bool? ThrowOnDeserializationError { get; set; }
74+
public bool? SkipDateTimeConversion { get; set; }
7475
public bool? AlwaysUseUtc { get; set; }
7576
public bool? AssumeUtc { get; set; }
7677
public bool? AppendUtcOffset { get; set; }

0 commit comments

Comments
 (0)