Reputation: 23
I'm trying to use NodaTime
in my AspNetCore project.
JSON serialization works fine. But I can't get working for model binding of form/query/path
params.As I saw in similar questions there are no TypeConverter implementation in NodaTime
. Maybe there are some work around for this?
.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Razor.Design" Version="2.2.0" PrivateAssets="All" />
<PackageReference Include="NodaTime" Version="2.4.6" />
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="2.2.0" />
</ItemGroup>
</Project>
My Startup services config:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(o => o.SerializerSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));
}
Api controller to reproduce the problem:
[Route("api/[controller]")]
[ApiController]
public class ValuesController : ControllerBase
{
// works
[HttpGet("Serialize")]
public ActionResult Get()
{
return Ok(new { Now = SystemClock.Instance.GetCurrentInstant() });
}
// error
[HttpGet("Ping")]
public ActionResult Get([FromQuery] Instant t)
{
return Ok(new { Pong = t });
}
}
URL:
http://localhost:55555/api/values/ping?t=2019-09-02T06:55:52.7077495Z
Stack trace:
System.InvalidOperationException: Could not create an instance of type 'NodaTime.Instant'. Model bound complex types must not be abstract or value types and must have a parameterless constructor. Alternatively, give the 't' parameter a non-null default value. at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.CreateModel(ModelBindingContext bindingContext) at Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.BindModelCoreAsync(ModelBindingContext bindingContext) at Microsoft.AspNetCore.Mvc.ModelBinding.ParameterBinder.BindModelAsync(ActionContext actionContext, IModelBinder modelBinder, IValueProvider valueProvider, ParameterDescriptor parameter, ModelMetadata metadata, Object value)
at Microsoft.AspNetCore.Mvc.Internal.ControllerBinderDelegateProvider.<>c__DisplayClass0_0.<g__Bind|0>d.MoveNext() --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync() at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
Upvotes: 2
Views: 1352
Reputation: 1503290
I'm far from an expert on ASP.NET Core model binders, but I've managed to hack up a way of doing it, as shown below.
We do now have TypeConverter support in NodaTime 3.0, which is present in the 3.0.0-beta01 build. I'm not expecting breaking changes between now and the 3.0 GA release, but I'd prefer not to make any guarantees :) The implementation code is pretty stable though - I wouldn't be worried on that front.
To bind manually, you can create a model binder provider and a model binder:
public class InstantModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context) =>
context.Metadata.ModelType == typeof(Instant) ? new InstantModelBinder(context.Metadata.ParameterName) : null;
}
public class InstantModelBinder : IModelBinder
{
private readonly string parameterName;
public InstantModelBinder(string parameterName)
{
this.parameterName = parameterName;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
var text = bindingContext.ActionContext.HttpContext.Request.Query[parameterName];
if (text.Count != 1)
{
bindingContext.Result = ModelBindingResult.Failed();
}
else
{
var result = InstantPattern.ExtendedIso.Parse(text);
bindingContext.Result = result.Success
? ModelBindingResult.Success(result.Value)
: ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
}
Then register the provider as the first provider in MVC options:
services.AddMvc(options => options.ModelBinderProviders.Insert(0, new InstantModelBinderProvider()))
.SetCompatibilityVersion(CompatibilityVersion.Version_2_2)
.AddJsonOptions(o => o.SerializerSettings.ConfigureForNodaTime(DateTimeZoneProviders.Tzdb));
If you need to do this for multiple Noda Time types, I'd expect you do be able to use a single provider that works out which binder to use based on the ModelBinderProviderContext.Metadata.ModelType
.
Currently the binder assumes that you're binding from the query string. I haven't investigated how easy it is to make that more general.
Upvotes: 3