UtcDateTimeConverter.cs 2.0 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. using System.Text.Json;
  2. using System.Text.Json.Serialization;
  3. namespace Web.Api.Common;
  4. /// <summary>
  5. /// DB 의 모든 DateTime 컬럼은 UTC 로 저장된다(Domain Entity 가 일관되게 DateTime.UtcNow 사용).
  6. /// SQL Server datetime2 컬럼은 timezone 정보가 없어 EF Core 가 Kind = Unspecified 로 materialize 한다.
  7. /// System.Text.Json 의 기본 동작은 Unspecified DateTime 을 Z/offset 없이 출력해서, 클라이언트 JS 가
  8. /// 로컬 시간으로 오해 → 9시간(KST 오프셋) 차이 발생. 이 converter 가 Z 마커를 강제해서 문제를 해결.
  9. /// </summary>
  10. public sealed class UtcDateTimeConverter : JsonConverter<DateTime>
  11. {
  12. public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  13. {
  14. var dt = reader.GetDateTime();
  15. return dt.Kind == DateTimeKind.Utc ? dt : DateTime.SpecifyKind(dt, DateTimeKind.Utc);
  16. }
  17. public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options)
  18. {
  19. var utc = value.Kind switch
  20. {
  21. DateTimeKind.Utc => value,
  22. DateTimeKind.Local => value.ToUniversalTime(),
  23. _ => DateTime.SpecifyKind(value, DateTimeKind.Utc)
  24. };
  25. writer.WriteStringValue(utc.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"));
  26. }
  27. }
  28. public sealed class UtcNullableDateTimeConverter : JsonConverter<DateTime?>
  29. {
  30. private static readonly UtcDateTimeConverter Inner = new();
  31. public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
  32. {
  33. if (reader.TokenType == JsonTokenType.Null)
  34. {
  35. return null;
  36. }
  37. return Inner.Read(ref reader, typeof(DateTime), options);
  38. }
  39. public override void Write(Utf8JsonWriter writer, DateTime? value, JsonSerializerOptions options)
  40. {
  41. if (value is null)
  42. {
  43. writer.WriteNullValue();
  44. return;
  45. }
  46. Inner.Write(writer, value.Value, options);
  47. }
  48. }