DateOnly是.NET 6中新引入的原始数据类型。显然,它适用于呈现、传递和存储仅日期信息,例如DateOrBirth、RegisterDate和WhatEverEventDate。
过去,.NET(Framework or Core)开发者基本使用三种方式:
- 使用string如yyyy-MM-dd,或yyyyMMdd。并将对象转换为DateTime用于计算日期跨度。
- 使用DateTime或DateTimeOffset并确保TimeOfDay为零。在进行跨时区对话时要格外小心。
- 使用Noda时间库或类似的。但是,根据您的上下文,使用额外的库可能会带来一些负面影响。
因此,拥有一个仅用于日期信息的专用类型真的是一件幸事。但是,我发现ASP.NET Core或System.Text.Json尚未正确支持DateOnly。如果你在Web API中使用DateOnly,你很快就会在绑定和序列化方面遇到麻烦。
本文提供DateOnly在ASP.NET Core 6中使用的解决方案,在未来引入7之前。
背景过去,我使用派生类Newtonsoft.Json.Converters.IsoDateTimeConverter来处理仅日期信息。
public class DateAndTimeConverter : IsoDateTimeConverter
{
static readonly Type typeOfDateTime = typeof(DateTime);
static readonly Type typeOfNullableDateTime = typeof(DateTime?);
static readonly Type typeOfDateTimeOffset = typeof(DateTimeOffset);
static readonly Type typeOfNullDateTimeOffset = typeof(DateTimeOffset?);
public override void WriteJson
(JsonWriter writer, object value, JsonSerializer serializer)
{
var type = value.GetType();
if (type == typeOfDateTimeOffset)
{
var dto = (DateTimeOffset)value;
if (dto == DateTimeOffset.MinValue)
{
writer.WriteNull();
return;
}
else if (dto.TimeOfDay == TimeSpan.Zero)
{
writer.WriteValue(dto.ToString("yyyy-MM-dd"));
return;
}
}
else if (type == typeOfNullDateTimeOffset)
{
var dto = (DateTimeOffset?)value;
if (!dto.HasValue || dto.Value == DateTimeOffset.MinValue)
{
writer.WriteNull();
return;
}
else if (dto.Value.TimeOfDay == TimeSpan.Zero)
{
writer.WriteValue(dto.Value.ToString("yyyy-MM-dd"));
return;
}
}
else if (type == typeOfDateTime)
{
var dt = (DateTime)value;
if (dt.TimeOfDay == TimeSpan.Zero)
{
writer.WriteValue(dt.ToString("yyyy-MM-dd"));
return;
}
}
else if (type == typeOfNullableDateTime)
{
var dto = (DateTime?)value;
if (!dto.HasValue || dto.Value == DateTime.MinValue)
{
writer.WriteNull();
return;
}
else if (dto.Value.TimeOfDay == TimeSpan.Zero)
{
writer.WriteValue(dto.Value.ToString("yyyy-MM-dd"));
return;
}
}
base.WriteJson(writer, value, serializer);
}
}
这些天来,我更喜欢使用JsonConverter。这种方法看起来更整洁,也更灵活。并且System.Text.Json有一个类具有类似的接口。
使用代码DateOnlyJsonConverter是nuget包Fonlow.DateOnlyExtensions中的转换器之一。您应该在ASP.NET Core控制器和.NET客户端中使用DateOnlyJsonConverter。
public sealed class DateOnlyJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, DateOnly value, JsonSerializer serializer)
{
writer.WriteValue(value.ToString("O"));
}
public override DateOnly ReadJson(JsonReader reader, Type objectType, DateOnly existingValue, bool hasExistingValue, JsonSerializer serializer)
{
var v = reader.Value;
if (v == null)
{
return DateOnly.MinValue;
}
var vType = v.GetType();
if (vType == typeof(DateTimeOffset)) //when the object is from a property in POST body. When used in service, better to have options.SerializerSettings.DateParseHandling = Newtonsoft.Json.DateParseHandling.DateTimeOffset;
{
return DateOnly.FromDateTime(((DateTimeOffset)v).DateTime);
}
if (vType == typeof(string))
{
return DateOnly.Parse((string)v); //DateOnly can parse 00001-01-01
}
if (vType == typeof(DateTime)) //when the object is from a property in POST body from a TS client
{
return DateOnly.FromDateTime((DateTime)v);
}
throw new NotSupportedException($"Not yet support {vType} in {this.GetType()}.");
}
}
在您的ASP.NET Core Startup代码中,将转换器注入控制器:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers()
.AddNewtonsoftJson(
options =>
{
options.SerializerSettings.DateParseHandling = Newtonsoft.Json.DateParseHandling.DateTimeOffset;
options.SerializerSettings.Converters.Add(new DateOnlyJsonConverter());
options.SerializerSettings.Converters.Add(new DateOnlyNullableJsonConverter());
}
);
在您的使用HttpClient的.NET客户端代码中,将转换器添加到JsonSerializerSettings:
var jsonSerializerSettings = new Newtonsoft.Json.JsonSerializerSettings()
{
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
};
jsonSerializerSettings.Converters.Add(new DateOnlyJsonConverter());
jsonSerializerSettings.Converters.Add(new DateOnlyNullableJsonConverter());
Api = new DemoWebApi.Controllers.Client.SuperDemo(httpClient, jsonSerializerSettings);
public partial class SuperDemo
{
private System.Net.Http.HttpClient client;
private JsonSerializerSettings jsonSerializerSettings;
public SuperDemo(System.Net.Http.HttpClient client,
JsonSerializerSettings jsonSerializerSettings = null)
{
if (client == null)
throw new ArgumentNullException("Null HttpClient.", "client");
if (client.BaseAddress == null)
throw new ArgumentNullException("HttpClient has no BaseAddress", "client");
this.client = client;
this.jsonSerializerSettings = jsonSerializerSettings;
}
public System.DateOnly PostDateOnly(System.DateOnly d,
Action handleHeaders = null)
{
var requestUri = "api/SuperDemo/DateOnly";
using (var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, requestUri))
{
using (var requestWriter = new System.IO.StringWriter())
{
var requestSerializer = JsonSerializer.Create(jsonSerializerSettings);
requestSerializer.Serialize(requestWriter, d);
var content = new StringContent(requestWriter.ToString(),
System.Text.Encoding.UTF8, "application/json");
httpRequestMessage.Content = content;
if (handleHeaders != null)
{
handleHeaders(httpRequestMessage.Headers);
}
var responseMessage = client.SendAsync(httpRequestMessage).Result;
try
{
responseMessage.EnsureSuccessStatusCodeEx();
var stream = responseMessage.Content.ReadAsStreamAsync().Result;
using (JsonReader jsonReader = new JsonTextReader
(new System.IO.StreamReader(stream)))
{
var serializer = JsonSerializer.Create(jsonSerializerSettings);
return serializer.Deserialize(jsonReader);
}
}
finally
{
responseMessage.Dispose();
}
}
}
}
现在DateOnly几乎在所有情况下都可以在ASP.NET Core 6中使用。
更多示例可以在测试套件中找到。
兴趣点 日期仅在URL中?到目前为止,使用自定义的JsonConverters,您可以使用HTTP POST正文中的DateOnly对象和返回的结果,作为独立对象或复杂对象的属性值,但是,使用DateOnly对象作为URL的段是不可能的,因为JsonConverter不涉及自定义,但“ Microsoft.AspNetCore.Routing.EndpointMiddleware”和Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinder可能正在使用System.Text.Json。
显然,微软的ASP.NET Core团队需要做一些事情来给予DateOnly与DateTimeOffset 相同的处理,而目前DateOnly在ASP.NET Core 6的模型绑定中并未将其列为简单类型。
尽管如此,对于可能只是使用string类型而不是DateOnly类型作为URL参数并传递ISO 8601日期字符串的应用程序开发人员来说,这并不是一个真正的大问题。例如:
[HttpGet]
[Route("DateOnlyStringQuery")]
public DateOnly QueryDateOnlyAsString([FromQuery] string d)
{
return DateOnly.Parse(d);
}
[Fact]
public async void TestQueryDateOnlyString()
{
DateOnly d = new DateOnly(2008, 12, 18);
var r = await api.QueryDateOnlyAsStringAsync(d.ToString("O"));
Assert.Equal(d, r);
}
或者,只需使用POST。
代码生成器怎么样?编写与Web API对话的客户端代码听起来既重复又无聊。如今,许多开发人员更愿意使用代码生成器来生成客户端API代码。您可以尝试WebApiClientGen和OpenApiClientGen,它们都可以生成与上面类似的客户端API代码。
Newtonsoft.Json还是System.Text.Json?从.NET 6开始,仍有一些情况System.Text.Json无法正确处理,但Newtonsoft.Json可以。
.NET Framework客户端怎么样?显然,微软没有计划向后移植DateOnly到.NET Framework。因此,如果您有一些.NET Framework客户端应用程序需要维护,并且想要与使用ASP.NET Core Web API服务通信DateOnly,您可以做什么?
您可以在Nuget包Fonlow.DateOnlyExtensionsNF中使用DateTimeOffsetJsonConverter和DateTimeJsonConverter 。在这个测试套件中可以找到使用DateOnly与ASP.NET Core Web API对话的示例。
WebApiClientGen生成C#客户端API代码,始终映射DateOnly到DateOnly。并且OpenApiClientGen有一个设置“DateToDateOnly”默认为True。如果您希望.NET Framework客户端和.NET客户端都使用生成的代码,您可以将“DateToDateOnly”保留为true。复制生成的代码并将所有“DateOnly”标识符替换为“DateTimeOffset”。您应该不难通过Powsershell脚本自动化生成代码的这种变体。
.NET Framework客户端应用程序代码:
var jsonSerializerSettings = new Newtonsoft.Json.JsonSerializerSettings()
{
NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore,
};
jsonSerializerSettings.DateParseHandling = Newtonsoft.Json.DateParseHandling.DateTimeOffset; //needed to make sure JSON serializers assume DateTimeOffset rather than DateTime.
jsonSerializerSettings.Converters.Add(new DateTimeOffsetJsonConverter()); //needed to handle DateOnly.MinValue
jsonSerializerSettings.Converters.Add(new DateTimeOffsetNullableJsonConverter()); //needed to handle DateOnly.MinValue
Api = new DemoWebApi.Controllers.Client.DateTypes(httpClient, jsonSerializerSettings);
[Fact]
public void TestPostDateOnly()
{
var dateOnly = new DateTimeOffset(1988, 12, 23, 0, 0, 0, TimeSpan.Zero);
var r = api.PostDateOnly(dateOnly);
Assert.Equal(dateOnly.Date, r.Date);
Assert.Equal(DateTimeOffset.Now.Offset, r.Offset); //Local date start, because the return object is "1988-12-23". no matter the client sends "2022-03-12" or "2022-03-12T00:00:00+00:00" or "2022-03-12T00:00:00Z"
Assert.Equal(TimeSpan.Zero, r.TimeOfDay);
}
您可能会注意到需要额外的设置DateParseHandling。这可确保跨时区通信保留正确的时区信息,而NewtonSoft.Json JsonConverte.ReadJson() 默认情况下读取 DateTimeOffset为DateTime,从而丢失时区信息。相反,在.NET 6中,我们不需要在客户端中为服务器上的DateOnly使用DateTimeOffset,因此不需要转换器。
JavaScript或TypeScript客户端怎么样?您的JavaScript客户端只能使用Date对象与Web API对话,该API始终返回yyyy-MM-dd string仅适用于日期的数据。幸运的是,JavaScript可以很好地处理这个问题,可能是因为Date对象总是使用UTC在内部存储数据。WebApiClientGen和OpenApiClientGen可以为jQuery、Angular 2+、AXIOS、Aurelia和Fetch API生成客户端API。在这个类别为“DateTypes API”的测试套件中,您可能会看到TypeScript应用程序如何使用生成的客户端API代码进行DateOnly处理。
日期选择器您的客户端程序可能会使用一些日期选择器组件。在.NET中,您可能需要确保日期选择器组件与.NET DateOnly兼容,否则,您可能需要遵循当前的数据绑定实践。
如果您正在开发带有日期选择器组件的Web UI,您需要确保选择的日期,例如,“1980-01-01”在Date对象中存储为“1980-01-01T00:00:00.000Z”而不是“1979-12-31T14:00:00.000Z”(我在澳大利亚+10时区)。
比如在开发Angular SPA的时候,我使用的DatePicker是Angular Material Components的组件。为了确保Date对象获得“1980-01-01T00:00:00.000Z”,可以有两种方法。
在@NgModule/providers中,提供以下内容:
{ provide: DateAdapter, useClass: MomentDateAdapter,
deps: [MAT_DATE_LOCALE, MAT_MOMENT_DATE_ADAPTER_OPTIONS] },
{ provide: MAT_DATE_FORMATS, useValue: MAT_MOMENT_DATE_FORMATS },
{ provide: MAT_MOMENT_DATE_ADAPTER_OPTIONS, useValue: { useUtc: true, strict: true } },
在包含许多延迟模块的复杂业务应用程序中,您可以在每个延迟模块或组件中声明这些提供程序,而您可能有第三方组件,它们可能更喜欢DatePicker。
仅将DateTime对象映射到日期的约定将对象映射DateTime到仅日期信息的信号是设置 TimeZone=Zero和TimeOfDate=Zero。显然,Moment.JS团队和Angular Material Components团队使用了相同的协议。同样,.NET客户端和ASP.NET Core Web API都应该使用相同的转换器集来确保这样的协议处理DateTime和DateTimeOffset,否则,不仅只有日期的情况会出现问题,而且DateTime.Min和DateTimeOffset.Min也会跨时区陷入麻烦。
评论
如果您有一个只能存储DateTime日期信息的遗留数据库,您需要检查应用程序如何存储日期。
DateOnly和数据库显然,并非所有数据库引擎都支持仅日期列。据我所知, MS SQL Server 2016和MySql仅支持日期数据类型。
使用Entity Framework Code First,相应的数据库特定库应映射DateOnly到Date列类型。
集成测试在开发分布式应用程序时,在处理DateTime和DateOnly时测试跨时区问题很重要。在集成测试期间,您应该在位于不同时区的机器/VM上拥有服务和客户端。我在澳大利亚,我通常会在+10:00时区使用测试客户端或集成测试套件,在UTC或-10:00时区使用服务。
https://www.codeproject.com/Articles/5325820/DateOnly-in-NET-6-and-ASP-NET-Core-6