I'm trying to implement a single ModelBinder
for all my DTOs:
public class MyModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext) {
var queryDto = bindingContext.ModelType.GetConstructors()[0].Invoke([]);
// fill properties via Reflection
bindingContext.Result = ModelBindingResult.Success(queryDto);
return Task.CompletedTask;
}
}
This is an example of DTO:
public class Dto {
public int Id { get; set; }
public string Name { get; set; }
}
Now, if I try to set an endpoint like this:
app.MapGet("/get-dto", ([FromQuery] [ModelBinder(typeof(MyModelBinder))] Dto dto) => {
return CalculateResultSomehow(dot);
});
The compiler gives me the error:
error ASP0020: Parameter 'dto' of type Dto should define a bool TryParse(string, IFormatProvider, out Dto) method, or implement IParsable
If I remove the [FromQuery] attribute, the lambda has a warning:
ModelBinderAttribute should not be specified for a MapGet Delegate parameter
And the code breaks at runtime with the Exception:
An unhandled exception occurred while processing the request. InvalidOperationException: Body was inferred but the method does not allow inferred body parameters... Did you mean to register the "Body (Inferred)" parameter(s) as a Service or apply the [FromServices] or [FromBody] attribute?
Now, since I'm implementing a parsing logic based on Reflection, I don't want to implement the static TryParse()
on every single DTO of my application (I have 100 of them...). And I shouldn't: I already have the ModelBinder
.
A controller's action works perfectly using the same system:
[ApiController]
public class MyController
{
[HttpGet("/get-dto")]
public Dto GetDto([FromQuery] [ModelBinder(typeof(MyModelBinder))] Dto dto) {
return dto;
}
}
I'm lost here. What am I missing? Why isn't this working for Minimal APIs?
Since I already had a dynamically generated set of endpoints based on a few common generic methods, I declared the query string as a simple string parameter and I pushed the conversion "query string => typed dto" after the beginning of the method, like this:
// Simplified version:
public static Delegate ConfigureEndpoint<TQueryStringDto>()
{
return async ([FromQuery] string query /* other params omitted */) => {
var dto = ConvertToDto<TQueryStringDto>(query);
// Do something general, valid for every endpoint,
// like sending the dto to IMediator
};
}
ConvertToDto
is the general method I would have used in a early middleware or in the ModelBinder
, but that I can also use here, a little down below the chain processing the request.