diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs index 59c7de88408..a633edb37e3 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpMvcOptionsExtensions.cs @@ -4,6 +4,7 @@ using Volo.Abp.AspNetCore.Mvc.Conventions; using Volo.Abp.AspNetCore.Mvc.ExceptionHandling; using Volo.Abp.AspNetCore.Mvc.Features; +using Volo.Abp.AspNetCore.Mvc.ModelBinding; using Volo.Abp.AspNetCore.Mvc.Uow; using Volo.Abp.AspNetCore.Mvc.Validation; @@ -35,7 +36,7 @@ private static void AddFilters(MvcOptions options) private static void AddModelBinders(MvcOptions options) { - //options.ModelBinderProviders.Add(new AbpDateTimeModelBinderProvider()); + options.ModelBinderProviders.Insert(0, new AbpDateTimeModelBinderProvider()); } private static void AddMetadataProviders(MvcOptions options, IServiceCollection services) diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs new file mode 100644 index 00000000000..1916b9aa16e --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinder.cs @@ -0,0 +1,49 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Volo.Abp.Timing; + +namespace Volo.Abp.AspNetCore.Mvc.ModelBinding +{ + public class AbpDateTimeModelBinder : IModelBinder + { + private readonly Type _type; + private readonly SimpleTypeModelBinder _simpleTypeModelBinder; + private readonly IClock _clock; + + public AbpDateTimeModelBinder(ModelBinderProviderContext context) + { + _type = context.Metadata.ModelType; + _clock = context.Services.GetRequiredService(); + _simpleTypeModelBinder = new SimpleTypeModelBinder(context.Metadata.ModelType, + context.Services.GetRequiredService()); + } + + public async Task BindModelAsync(ModelBindingContext bindingContext) + { + await _simpleTypeModelBinder.BindModelAsync(bindingContext); + + if (!bindingContext.Result.IsModelSet) + { + return; + } + + if (_type == typeof(DateTime)) + { + var dateTime = (DateTime) bindingContext.Result.Model; + bindingContext.Result = ModelBindingResult.Success(_clock.Normalize(dateTime)); + } + else + { + var dateTime = (DateTime?) bindingContext.Result.Model; + if (dateTime != null) + { + bindingContext.Result = ModelBindingResult.Success(_clock.Normalize(dateTime.Value)); + } + } + } + } +} \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinderProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinderProvider.cs new file mode 100644 index 00000000000..fd55e2b61a1 --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ModelBinding/AbpDateTimeModelBinderProvider.cs @@ -0,0 +1,47 @@ +using System; +using System.Linq; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; +using Volo.Abp.Timing; + +namespace Volo.Abp.AspNetCore.Mvc.ModelBinding +{ + public class AbpDateTimeModelBinderProvider : IModelBinderProvider + { + public IModelBinder GetBinder(ModelBinderProviderContext context) + { + if (context.Metadata.ModelType != typeof(DateTime) && + context.Metadata.ModelType != typeof(DateTime?)) + { + return null; + } + + if (context.Metadata.ContainerType == null) + { + if (context.Metadata is DefaultModelMetadata defaultModelMetadata && + defaultModelMetadata.Attributes.Attributes.All(x => x.GetType() != typeof(DisableDateTimeNormalizationAttribute))) + { + return new AbpDateTimeModelBinder(context); + } + } + else + { + var dateNormalizationDisabledForClass = + context.Metadata.ContainerType.IsDefined(typeof(DisableDateTimeNormalizationAttribute), true); + + var dateNormalizationDisabledForProperty = context.Metadata.ContainerType + .GetProperty(context.Metadata.PropertyName) + ?.IsDefined(typeof(DisableDateTimeNormalizationAttribute), true); + + if (!dateNormalizationDisabledForClass && + dateNormalizationDisabledForProperty != null && + !dateNormalizationDisabledForProperty.Value) + { + return new AbpDateTimeModelBinder(context); + } + } + + return null; + } + } +} \ No newline at end of file diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs new file mode 100644 index 00000000000..2c5a69e7e73 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController.cs @@ -0,0 +1,61 @@ +using System; +using Microsoft.AspNetCore.Mvc; +using Volo.Abp.Timing; + +namespace Volo.Abp.AspNetCore.Mvc.ModelBinding +{ + [Route("api/model-Binding-test")] + public class ModelBindingController : AbpController + { + [HttpGet("DateTimeKind")] + public string DateTimeKind(DateTime input) + { + return input.Kind.ToString().ToLower(); + } + + [HttpGet("NullableDateTimeKind")] + public string NullableDateTimeKind(DateTime? input) + { + return input.Value.Kind.ToString().ToLower(); + } + + [HttpGet("DisableDateTimeNormalizationDateTimeKind")] + public string DisableDateTimeNormalizationDateTimeKind([DisableDateTimeNormalization]DateTime input) + { + return input.Kind.ToString().ToLower(); + } + + [HttpGet("DisableDateTimeNormalizationNullableDateTimeKind")] + public string DisableDateTimeNormalizationNullableDateTimeKind([DisableDateTimeNormalization]DateTime? input) + { + return input.Value.Kind.ToString().ToLower(); + } + + [HttpGet("ComplexTypeDateTimeKind")] + public string ComplexTypeDateTimeKind(GetDateTimeKindModel input) + { + return input.Time1.Kind.ToString().ToLower() + "_" + + input.Time2.Kind.ToString().ToLower() + "_" + + input.Time3.Value.Kind.ToString().ToLower() + "_" + + input.InnerModel.Time4.Kind.ToString().ToLower(); + } + } + + public class GetDateTimeKindModel + { + [DisableDateTimeNormalization] + public DateTime Time1 { get; set; } + + public DateTime Time2 { get; set; } + + public DateTime? Time3 { get; set; } + + public GetDateTimeKindInnerModel InnerModel { get; set; } + + [DisableDateTimeNormalization] + public class GetDateTimeKindInnerModel + { + public DateTime Time4 { get; set; } + } + } +} diff --git a/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs new file mode 100644 index 00000000000..632a02194f2 --- /dev/null +++ b/framework/test/Volo.Abp.AspNetCore.Mvc.Tests/Volo/Abp/AspNetCore/Mvc/ModelBinding/ModelBindingController_Tests.cs @@ -0,0 +1,100 @@ +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Shouldly; +using Volo.Abp.Timing; +using Xunit; + +namespace Volo.Abp.AspNetCore.Mvc.ModelBinding +{ + public abstract class ModelBindingController_Tests : AspNetCoreMvcTestBase + { + protected DateTimeKind DateTimeKind { get; set; } + + protected override void ConfigureServices(HostBuilderContext context, IServiceCollection services) + { + services.Configure(x => x.Kind = DateTimeKind); + } + + [Fact] + public async Task DateTimeKind_Test() + { + var response = await Client.GetAsync("/api/model-Binding-test/DateTimeKind?input=2010-01-01T00:00:00Z"); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + var resultAsString = await response.Content.ReadAsStringAsync(); + resultAsString.ShouldBe(DateTimeKind.ToString().ToLower()); + } + + [Fact] + public async Task NullableDateTimeKind_Test() + { + var response = + await Client.GetAsync("/api/model-Binding-test/NullableDateTimeKind?input=2010-01-01T00:00:00Z"); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + var resultAsString = await response.Content.ReadAsStringAsync(); + resultAsString.ShouldBe(DateTimeKind.ToString().ToLower()); + } + + [Fact] + public async Task DisableDateTimeNormalizationDateTimeKind_Test() + { + var response = + await Client.GetAsync( + "/api/model-Binding-test/DisableDateTimeNormalizationDateTimeKind?input=2010-01-01T00:00:00Z"); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + var resultAsString = await response.Content.ReadAsStringAsync(); + //Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is Local. + resultAsString.ShouldBe(DateTimeKind.Local.ToString().ToLower()); + } + + [Fact] + public async Task DisableDateTimeNormalizationNullableDateTimeKind_Test() + { + var response = + await Client.GetAsync( + "/api/model-Binding-test/DisableDateTimeNormalizationNullableDateTimeKind?input=2010-01-01T00:00:00Z"); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + var resultAsString = await response.Content.ReadAsStringAsync(); + //Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is Local. + resultAsString.ShouldBe(DateTimeKind.Local.ToString().ToLower()); + } + + [Fact] + public async Task ComplexTypeDateTimeKind_Test() + { + var response = await Client.GetAsync("/api/model-Binding-test/ComplexTypeDateTimeKind?" + + "Time1=2010-01-01T00:00:00Z&" + + "Time2=2010-01-01T00:00:00Z&" + + "Time3=2010-01-01T00:00:00Z&" + + "InnerModel.Time4=2010-01-01T00:00:00Z"); + + response.StatusCode.ShouldBe(HttpStatusCode.OK); + var resultAsString = await response.Content.ReadAsStringAsync(); + //Time parameter(2010-01-01T00:00:00Z) with time zone information, so the default Kind is Local. + resultAsString.ShouldBe( + $"local_{DateTimeKind.ToString().ToLower()}_{DateTimeKind.ToString().ToLower()}_local"); + } + } + + public class ModelBindingController_Utc_Tests : ModelBindingController_Tests + { + public ModelBindingController_Utc_Tests() + { + DateTimeKind = DateTimeKind.Utc; + } + } + + public class ModelBindingController_Local_Tests : ModelBindingController_Tests + { + public ModelBindingController_Local_Tests() + { + DateTimeKind = DateTimeKind.Local; + } + } +} \ No newline at end of file