.NET 6和ASP.NET Core 6中的DateOnly

介绍

DateOnly.NET 6新引入的原始数据类型。显然,它适用于呈现、传递和存储仅日期信息,例如DateOrBirthRegisterDateWhatEverEventDate

过去,.NETFramework or Core)开发者基本使用三种方式:

  1. 使用stringyyyy-MM-dd,或yyyyMMdd。并将对象转换为DateTime用于计算日期跨度。
  2. 使用DateTimeDateTimeOffset并确保TimeOfDay为零。在进行跨时区对话时要格外小心。
  3. 使用Noda时间库或类似的。但是,根据您的上下文,使用额外的库可能会带来一些负面影响。

因此,拥有一个仅用于日期信息的专用类型真的是一件幸事。但是,我发现ASP.NET Core或System.Text.Json尚未正确支持DateOnly。如果你在Web API中使用DateOnly,你很快就会在绑定和序列化方面遇到麻烦。

本文提供DateOnlyASP.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<T>。这种方法看起来更整洁,也更灵活。并且System.Text.Json一个类具有类似的接口

使用代码

DateOnlyJsonConverternugetFonlow.DateOnlyExtensions中的转换器之一。您应该在ASP.NET Core控制器和.NET客户端中使用DateOnlyJsonConverter

public sealed class DateOnlyJsonConverter : JsonConverter<DateOnly>
{
    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<System.Net.Http.Headers.HttpRequestHeaders> 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<System.DateOnly>(jsonReader);
                    }
                }
                finally
                {
                    responseMessage.Dispose();
                }
            }
        }
    }

现在DateOnly几乎在所有情况下都可以在ASP.NET Core 6中使用。

更多示例可以在测试套件中找到。

兴趣点

日期仅在URL中?

到目前为止,使用自定义的JsonConverters,您可以使用HTTP POST正文中的DateOnly对象和返回的结果,作为独立对象或复杂对象的属性值,但是,使用DateOnly对象作为URL的段是不可能的,因为JsonConverter不涉及自定义,但“ Microsoft.AspNetCore.Routing.EndpointMiddlewareMicrosoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexObjectModelBinder可能正在使用System.Text.Json

显然,微软的ASP.NET Core团队需要做一些事情来给予DateOnlyDateTimeOffset 相同的处理,而目前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代码。您可以尝试WebApiClientGenOpenApiClientGen,它们都可以生成与上面类似的客户端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,您可以做什么?

您可以在NugetFonlow.DateOnlyExtensionsNF中使用DateTimeOffsetJsonConverterDateTimeJsonConverter 在这个测试套件中可以找到使用DateOnlyASP.NET Core Web API对话的示例。

WebApiClientGen生成C#客户端API代码,始终映射DateOnlyDateOnly。并且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() 默认情况下读取 DateTimeOffsetDateTime,从而丢失时区信息。相反,在.NET 6中,我们不需要在客户端中为服务器上的DateOnly使用DateTimeOffset,因此不需要转换器。 

JavaScriptTypeScript客户端怎么样?

您的JavaScript客户端只能使用Date对象与Web API对话,该API始终返回yyyy-MM-dd string仅适用于日期的数据。幸运的是,JavaScript可以很好地处理这个问题,可能是因为Date对象总是使用UTC在内部存储数据。WebApiClientGenOpenApiClientGen可以为jQueryAngular 2+AXIOSAureliaFetch API生成客户端API。在这个类别为DateTypes API”测试套件中,您可能会看到TypeScript应用程序如何使用生成的客户端API代码进行DateOnly处理。

日期选择器

您的客户端程序可能会使用一些日期选择器组件。在.NET中,您可能需要确保日期选择器组件与.NET DateOnly兼容,否则,您可能需要遵循当前的数据绑定实践。

如果您正在开发带有日期选择器组件的Web UI,您需要确保选择的日期,例如,1980-01-01Date对象中存储为1980-01-01T00:00:00.000Z而不是1979-12-31T14:00:00.000Z(我在澳大利亚+10时区)。

比如在开发Angular SPA的时候,我使用的DatePickerAngular 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=ZeroTimeOfDate=Zero。显然,Moment.JS团队和Angular Material Components团队使用了相同的协议。同样,.NET客户端和ASP.NET Core Web API都应该使用相同的转换器集来确保这样的协议处理DateTimeDateTimeOffset,否则,不仅只有日期的情况会出现问题,而且DateTime.MinDateTimeOffset.Min也会跨时区陷入麻烦。

评论

如果您有一个只能存储DateTime日期信息的遗留数据库,您需要检查应用程序如何存储日期。

DateOnly和数据库

显然,并非所有数据库引擎都支持仅日期列。据我所知,  MS SQL Server 2016MySql仅支持日期数据类型。

使用Entity Framework Code First,相应的数据库特定库应映射DateOnlyDate列类型。

集成测试

在开发分布式应用程序时,在处理DateTimeDateOnly时测试跨时区问题很重要。在集成测试期间,您应该在位于不同时区的机器/VM上拥有服务和客户端。我在澳大利亚,我通常会在+10:00时区使用测试客户端或集成测试套件,在UTC-10:00时区使用服务。

https://www.codeproject.com/Articles/5325820/DateOnly-in-NET-6-and-ASP-NET-Core-6

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值