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

Commit 35e0913

Browse files
committed
Modify PreserveUtc to SkipDateTimeConversion per PR conversation, make changes required to achieve behavior
1 parent 8d7aefc commit 35e0913

File tree

6 files changed

+257
-45
lines changed

6 files changed

+257
-45
lines changed

src/ServiceStack.Text/Common/DateTimeSerializer.cs

Lines changed: 78 additions & 16 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
{
@@ -29,7 +30,9 @@ public static class DateTimeSerializer
2930
public const string XsdDateTimeFormat3F = "yyyy-MM-ddTHH:mm:ss.fffZ"; //25
3031
public const string XsdDateTimeFormatSeconds = "yyyy-MM-ddTHH:mm:ssZ"; //21
3132
public const string DateTimeFormatSecondsUtcOffset = "yyyy-MM-ddTHH:mm:sszzz"; //22
33+
public const string DateTimeFormatSecondsNoOffset = "yyyy-MM-ddTHH:mm:ss";
3234
public const string DateTimeFormatTicksUtcOffset = "yyyy-MM-ddTHH:mm:ss.fffffffzzz"; //30
35+
public const string DateTimeFormatTicksNoUtcOffset = "yyyy-MM-ddTHH:mm:ss.fffffff";
3336

3437
public const string EscapedWcfJsonPrefix = "\\/Date(";
3538
public const string EscapedWcfJsonSuffix = ")\\/";
@@ -42,7 +45,7 @@ public static class DateTimeSerializer
4245
private static readonly int XsdTimeSeparatorIndex = XsdDateTimeFormat.IndexOf(XsdTimeSeparator);
4346
private const string XsdUtcSuffix = "Z";
4447
private static readonly char[] DateTimeSeperators = new[] { '-', '/' };
45-
48+
private static readonly Regex UtcOffsetInfoRegex = new Regex("([+-](?:2[0-3]|[0-1][0-9]):[0-5][0-9])", RegexOptions.Compiled);
4649
public static Func<string, Exception, DateTime> OnParseErrorFn { get; set; }
4750

4851
/// <summary>
@@ -52,14 +55,15 @@ public static class DateTimeSerializer
5255
/// <returns></returns>
5356
public static DateTime Prepare(this DateTime dateTime, bool parsedAsUtc=false)
5457
{
55-
if (JsConfig.AlwaysUseUtc)
58+
if (JsConfig.SkipDateTimeConversion)
5659
{
57-
return dateTime.Kind != DateTimeKind.Utc ? dateTime.ToStableUniversalTime() : dateTime;
60+
return dateTime;
5861
}
59-
if (JsConfig.PreserveUtc && dateTime.Kind == DateTimeKind.Utc)
62+
if (JsConfig.AlwaysUseUtc)
6063
{
61-
return dateTime;
64+
return dateTime.Kind != DateTimeKind.Utc ? dateTime.ToStableUniversalTime() : dateTime;
6265
}
66+
6367
return parsedAsUtc ? dateTime.ToLocalTime() : dateTime;
6468
}
6569

@@ -104,6 +108,7 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
104108

105109
return unspecifiedDate.Prepare();
106110
}
111+
DateTimeKind kind = DateTimeKind.Unspecified;
107112

108113
switch (JsConfig.DateHandler)
109114
{
@@ -117,6 +122,12 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
117122
if (long.TryParse(dateTimeStr, out unixTimeMs))
118123
return unixTimeMs.FromUnixTimeMs();
119124
break;
125+
case DateHandler.ISO8601:
126+
if (JsConfig.SkipDateTimeConversion)
127+
{
128+
dateTimeStr = RemoveUtcOffsets(dateTimeStr, out kind);
129+
}
130+
break;
120131
}
121132

122133
dateTimeStr = RepairXsdTimeSeparator(dateTimeStr);
@@ -153,7 +164,20 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
153164

154165
try
155166
{
156-
var dateTime = DateTime.Parse(dateTimeStr, null, DateTimeStyles.AssumeLocal);
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+
dateTime = DateTime.Parse(dateTimeStr, null, DateTimeStyles.AssumeLocal);
180+
}
157181
return dateTime.Prepare();
158182
}
159183
catch (FormatException)
@@ -174,6 +198,18 @@ public static DateTime ParseShortestXsdDateTime(string dateTimeStr)
174198
}
175199
}
176200

201+
private static string RemoveUtcOffsets(string dateTimeStr, out DateTimeKind kind)
202+
{
203+
var startOfTz = UtcOffsetInfoRegex.Match(dateTimeStr);
204+
if (startOfTz.Index > 0)
205+
{
206+
kind = DateTimeKind.Local;
207+
return dateTimeStr.Substring(0, startOfTz.Index);
208+
}
209+
kind = dateTimeStr.Contains("Z") ? DateTimeKind.Utc : DateTimeKind.Unspecified;
210+
return dateTimeStr;
211+
}
212+
177213
/// <summary>
178214
/// 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.
179215
/// These string are occasionally generated by SQLite and can cause errors in OrmLite when reading these columns from the DB.
@@ -213,7 +249,7 @@ private static string RepairXsdTimeSeparator(string dateTimeStr)
213249
if (dateTimeStr.EndsWith(XsdUtcSuffix))
214250
{
215251
dateTimeStr = dateTimeStr.Substring(0, dateTimeStr.Length - 1);
216-
dateKind = JsConfig.PreserveUtc ? DateTimeKind.Utc : dateKind;
252+
dateKind = JsConfig.SkipDateTimeConversion ? DateTimeKind.Utc : dateKind;
217253
}
218254

219255
var parts = dateTimeStr.Split('T');
@@ -412,19 +448,37 @@ public static string ToShortestXsdDateTimeString(DateTime dateTime)
412448
var timeOfDay = dateTime.TimeOfDay;
413449

414450
var isStartOfDay = timeOfDay.Ticks == 0;
415-
if (isStartOfDay && !(JsConfig.PreserveUtc && dateTime.Kind == DateTimeKind.Utc))
451+
if (isStartOfDay && !(JsConfig.SkipDateTimeConversion))
416452
return dateTime.ToString(ShortDateTimeFormat);
417453

418454
var hasFractionalSecs = (timeOfDay.Milliseconds != 0)
419455
|| ((timeOfDay.Ticks%TimeSpan.TicksPerMillisecond) != 0);
420-
if (!hasFractionalSecs)
421-
return dateTime.Kind != DateTimeKind.Utc
422-
? dateTime.ToString(DateTimeFormatSecondsUtcOffset)
423-
: dateTime.ToStableUniversalTime().ToString(XsdDateTimeFormatSeconds);
456+
if (JsConfig.SkipDateTimeConversion)
457+
{
458+
if (!hasFractionalSecs)
459+
return dateTime.Kind == DateTimeKind.Local
460+
? dateTime.ToString(DateTimeFormatSecondsUtcOffset)
461+
: dateTime.Kind == DateTimeKind.Unspecified
462+
? dateTime.ToString(DateTimeFormatSecondsNoOffset)
463+
: dateTime.ToStableUniversalTime().ToString(XsdDateTimeFormatSeconds);
464+
465+
return dateTime.Kind == DateTimeKind.Local
466+
? dateTime.ToString(DateTimeFormatTicksUtcOffset)
467+
: dateTime.Kind == DateTimeKind.Unspecified
468+
? dateTime.ToString(DateTimeFormatTicksNoUtcOffset)
469+
: PclExport.Instance.ToXsdDateTimeString(dateTime);
470+
}
471+
else
472+
{
473+
if (!hasFractionalSecs)
474+
return dateTime.Kind != DateTimeKind.Utc
475+
? dateTime.ToString(DateTimeFormatSecondsUtcOffset)
476+
: dateTime.ToStableUniversalTime().ToString(XsdDateTimeFormatSeconds);
424477

425-
return dateTime.Kind != DateTimeKind.Utc
426-
? dateTime.ToString(DateTimeFormatTicksUtcOffset)
427-
: PclExport.Instance.ToXsdDateTimeString(dateTime);
478+
return dateTime.Kind != DateTimeKind.Utc
479+
? dateTime.ToString(DateTimeFormatTicksUtcOffset)
480+
: PclExport.Instance.ToXsdDateTimeString(dateTime);
481+
}
428482
}
429483

430484
static readonly char[] TimeZoneChars = new[] { '+', '-' };
@@ -541,7 +595,15 @@ public static void WriteWcfJsonDate(TextWriter writer, DateTime dateTime)
541595

542596
if (JsConfig.DateHandler == DateHandler.ISO8601)
543597
{
544-
writer.Write(dateTime.ToString("o", CultureInfo.InvariantCulture));
598+
if (!JsConfig.SkipDateTimeConversion)
599+
{
600+
writer.Write(dateTime.ToString("o", CultureInfo.InvariantCulture));
601+
}
602+
else
603+
{
604+
var dt = dateTime.ToString("o", CultureInfo.InvariantCulture);
605+
writer.Write(dt);
606+
}
545607
return;
546608
}
547609

src/ServiceStack.Text/JsConfig.cs

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ public static JsConfigScope With(
5454
Func<Type, string> typeWriter = null,
5555
Func<string, Type> typeFinder = null,
5656
bool? treatEnumAsInteger = null,
57-
bool? preserveUtc = null,
57+
bool? skipDateTimeConversion = null,
5858
bool? alwaysUseUtc = null,
5959
bool? assumeUtc = null,
6060
bool? appendUtcOffset = null,
@@ -91,7 +91,7 @@ public static JsConfigScope With(
9191
TypeWriter = typeWriter ?? sTypeWriter,
9292
TypeFinder = typeFinder ?? sTypeFinder,
9393
TreatEnumAsInteger = treatEnumAsInteger ?? sTreatEnumAsInteger,
94-
PreserveUtc = preserveUtc ?? sPreserveUtc,
94+
SkipDateTimeConversion = skipDateTimeConversion ?? sSkipDateTimeConversion,
9595
AlwaysUseUtc = alwaysUseUtc ?? sAlwaysUseUtc,
9696
AssumeUtc = assumeUtc ?? sAssumeUtc,
9797
AppendUtcOffset = appendUtcOffset ?? sAppendUtcOffset,
@@ -514,22 +514,25 @@ public static bool AlwaysUseUtc
514514
}
515515

516516
/// <summary>
517-
/// Gets or sets a value indicating if the framework should preserve <see cref="DateTime"/> <see cref="DateTimeKind"/> when input was in UTC format instead of converting to local.
518-
/// JsConfig.DateHandler = DateHandler.ISO8601 should be used when set true for consistent serialization
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.
519522
/// </summary>
520-
private static bool? sPreserveUtc;
521-
public static bool PreserveUtc
523+
private static bool? sSkipDateTimeConversion;
524+
public static bool SkipDateTimeConversion
522525
{
523526
// obeying the use of ThreadStatic, but allowing for setting JsConfig once as is the normal case
524527
get
525528
{
526-
return (JsConfigScope.Current != null ? JsConfigScope.Current.PreserveUtc : null)
527-
?? sPreserveUtc
529+
return (JsConfigScope.Current != null ? JsConfigScope.Current.SkipDateTimeConversion : null)
530+
?? sSkipDateTimeConversion
528531
?? false;
529532
}
530533
set
531534
{
532-
if (!sPreserveUtc.HasValue) sPreserveUtc = value;
535+
if (!sSkipDateTimeConversion.HasValue) sSkipDateTimeConversion = value;
533536
}
534537
}
535538
/// <summary>

src/ServiceStack.Text/JsConfigScope.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +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? PreserveUtc { get; set; }
74+
public bool? SkipDateTimeConversion { get; set; }
7575
public bool? AlwaysUseUtc { get; set; }
7676
public bool? AssumeUtc { get; set; }
7777
public bool? AppendUtcOffset { get; set; }

tests/ServiceStack.Text.Tests/JsonTests/JsonDateTimeTests.cs

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -364,22 +364,109 @@ public void Can_deserialize_json_date_iso8601_withZOffset_asUtc_alwaysUseUtc_tru
364364
}
365365

366366
[Test]
367-
public void Can_deserialize_json_date_iso8601_withZOffset_asUtc_preserveUtc_true()
367+
public void Can_deserialize_json_date_iso8601_withZOffset_asUtc_skipDateTimeConversion_true()
368368
{
369-
JsConfig.PreserveUtc = true;
369+
JsConfig.SkipDateTimeConversion = true;
370370
JsConfig.DateHandler = DateHandler.ISO8601;
371371

372-
const string json = "\"1994-11-24T12:34:56.0000000Z\"";
372+
const string json = "\"1994-11-24T12:34:56Z\"";
373373
var fromJson = JsonSerializer.DeserializeFromString<DateTime>(json);
374374

375375
var dateTime = new DateTime(1994, 11, 24, 12, 34, 56, DateTimeKind.Utc);
376376
Assert.That(fromJson, Is.EqualTo(dateTime));
377-
Assert.That(fromJson.Kind, Is.EqualTo(dateTime.Kind));
378-
var backToJson = JsonSerializer.SerializeToString(dateTime);
379-
Assert.That(json, Is.EqualTo(backToJson));
377+
Assert.That(fromJson.Kind, Is.EqualTo(dateTime.Kind));
380378
JsConfig.Reset();
381379
}
382380

381+
[Test]
382+
public void Can_deserialize_json_date_iso8601_with_skipDateTimeConversion_true()
383+
{
384+
JsConfig.DateHandler = DateHandler.ISO8601;
385+
JsConfig.SkipDateTimeConversion = true;
386+
string serilizedResult;
387+
Utils.DateTimeISO8601Tests.TestObject deserilizedResult;
388+
389+
var testObject = new Utils.DateTimeISO8601Tests.TestObject
390+
{
391+
Date = new DateTime(2013, 1, 1, 0, 0, 1, DateTimeKind.Utc)
392+
};
393+
serilizedResult = JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject);
394+
deserilizedResult = JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(serilizedResult);
395+
Assert.AreEqual(deserilizedResult.Date, testObject.Date);
396+
Assert.AreEqual(DateTimeKind.Utc, deserilizedResult.Date.Kind);
397+
398+
using (JsConfig.With(skipDateTimeConversion: false))
399+
{
400+
Assert.AreEqual(DateTimeKind.Local, JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject)).Date.Kind);
401+
}
402+
403+
testObject = new Utils.DateTimeISO8601Tests.TestObject
404+
{
405+
Date = new DateTime(2013, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddMilliseconds(2)
406+
};
407+
serilizedResult = JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject);
408+
deserilizedResult = JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(serilizedResult);
409+
Assert.AreEqual(deserilizedResult.Date, testObject.Date);
410+
Assert.AreEqual(DateTimeKind.Utc, deserilizedResult.Date.Kind);
411+
412+
using (JsConfig.With(skipDateTimeConversion: false))
413+
{
414+
Assert.AreEqual(DateTimeKind.Local, JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject)).Date.Kind);
415+
}
416+
using (JsConfig.With(alwaysUseUtc: true, skipDateTimeConversion: false)) //It will work now
417+
{
418+
Assert.AreEqual(DateTimeKind.Utc, JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject)).Date.Kind);
419+
}
420+
421+
//make sure it still keeps local local
422+
testObject = new Utils.DateTimeISO8601Tests.TestObject
423+
{
424+
Date = new DateTime(2013, 1, 2, 0, 2, 0, DateTimeKind.Local).AddMilliseconds(2)
425+
};
426+
serilizedResult = JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject);
427+
deserilizedResult = JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(serilizedResult);
428+
Assert.AreEqual(deserilizedResult.Date, testObject.Date);
429+
Assert.AreEqual(DateTimeKind.Local, deserilizedResult.Date.Kind);
430+
431+
using (JsConfig.With(alwaysUseUtc: true))
432+
{
433+
Assert.AreEqual(DateTimeKind.Local, JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject)).Date.Kind);
434+
}
435+
using (JsConfig.With(alwaysUseUtc: true, skipDateTimeConversion: false))
436+
{
437+
Assert.AreEqual(DateTimeKind.Utc, JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject)).Date.Kind);
438+
}
439+
440+
441+
testObject = new Utils.DateTimeISO8601Tests.TestObject
442+
{
443+
Date = new DateTime(2013, 1, 2, 0, 2, 0, DateTimeKind.Unspecified).AddMilliseconds(2)
444+
};
445+
serilizedResult = JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject);
446+
deserilizedResult = JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(serilizedResult);
447+
Assert.AreEqual(deserilizedResult.Date, testObject.Date);
448+
Assert.AreEqual(DateTimeKind.Unspecified, deserilizedResult.Date.Kind);
449+
450+
using (JsConfig.With(alwaysUseUtc: true))
451+
{
452+
Assert.AreEqual(DateTimeKind.Unspecified, JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject)).Date.Kind);
453+
}
454+
using (JsConfig.With(alwaysUseUtc: true, skipDateTimeConversion: false))
455+
{
456+
Assert.AreEqual(DateTimeKind.Utc, JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject)).Date.Kind);
457+
}
458+
459+
using (JsConfig.With(skipDateTimeConversion: false))
460+
{
461+
serilizedResult = JsonSerializer.SerializeToString<Utils.DateTimeISO8601Tests.TestObject>(testObject);
462+
deserilizedResult = JsonSerializer.DeserializeFromString<Utils.DateTimeISO8601Tests.TestObject>(serilizedResult);
463+
Assert.AreEqual(DateTimeKind.Local, deserilizedResult.Date.Kind);
464+
}
465+
JsConfig.Reset();
466+
}
467+
468+
469+
383470
[Test]
384471
public void Can_deserialize_json_date_iso8601_withoutOffset_as_Unspecified()
385472
{

tests/ServiceStack.Text.Tests/ServiceStack.Text.Tests.csproj

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -366,7 +366,9 @@
366366
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
367367
</None>
368368
</ItemGroup>
369-
<ItemGroup />
369+
<ItemGroup>
370+
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
371+
</ItemGroup>
370372
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
371373
<Import Project="$(SolutionDir)\.nuget\NuGet.targets" />
372374
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.

0 commit comments

Comments
 (0)