0

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?

4
  • "why"? For the same reason everybody uses Reflection: to write the conversion/parsing/algorithm one single time. I don't want to write <if query string has a key "name", set the Name property; if it has the "code", set the Code property ecc. ecc.> for every property of every DTO, it's a nightmare. I want to do it via Reflection on a single class: <foreach key in the query string, if the DTO has a property with the same name, try to set its value>. Commented May 8, 2024 at 23:11
  • Please check out update in the answer. Commented May 8, 2024 at 23:48
  • Hi @Massimiliano Kraus, ASP.NET Core supports [AsParameters] to bind complex model instead of nested model. For your requirement I am confused that why you bind the nested model from body or from form? I think from query will be limited with lengh by browser or web server, also it is not secure that sensitive information can be exposed. Commented May 9, 2024 at 8:01
  • What's the difference between "complex model" and "nested model"? A call that retrieves data should use the GET method, therefore you should pass parameters in the path and in the query because you can't use the body. [FromQuery] reads the parameters from the query string. [AsParameters] does not work if my DTO has nested classes (for example: a PaginationDto has property Filters, which is a list of FilterDto, and every FilterDto has a PropertyName and a PropertyValue...) Commented May 9, 2024 at 10:56

2 Answers 2

5

I'm lost here. What am I missing? Why isn't this working for Minimal APIs?

Because Minimal APIs do not support "ordinary" model binders. Model binders are part of the "full" framework (which can be indirectly concluded from the ModelBinderAttribute namespace - Microsoft.AspNetCore.Mvc).

For binding supported by Minimal APIs see the Parameter Binding in Minimal API apps doc.

since I'm implementing a parsing logic based on Reflection

Without seeing query strings and reflection logic it is hard to tell but usually you should not need such logic. But again - it is hard to tell.

I don't want to implement the static TryParse()

As workaround for Minimal APIs you can create some wrapper class like MinimalApiBinder<T> and implement binding logic in it once. See How to configure NewtonsoftJson with Minimal API in .NET 6.0 for examples.

UPD

From the comment:

I don't want to write <if query string has a key "name", set the Name property; if it has the "code", set the Code property ecc. ecc.>

This should work out of the box. For example with AsParameters attribute (see the previously linked docs):

app.MapGet("/get-dto", ([AsParameters] Dto dto) => dto);

Will be bound correctly from /get-dto?name=test&id=1 query string.

Sign up to request clarification or add additional context in comments.

6 Comments

[AsParameters] does work, but only if the DTO is "flat", i.e. has only first-level properties. If it has nested properties, an Exception is thrown: "System.InvalidOperationException: Body was inferred but the method does not allow inferred body parameters."... I don't know if I can solve this with attributes on the nested fields of the DTO, but I don't want to be forced to mark my DTOs with attributes everywhere. It should work out of the box. I'm evaluating the other solutions you proposed.
@MassimilianoKraus "DTO is "flat", i.e. has only first-level properties." - this is covered in the linked docs- "AsParametersAttribute enables simple parameter binding to types and not complex or recursive model binding.". "It should work out of the box" - it depends on what do you mean by "should". According to current docs/implementation - it should not, Minimal APIs are named this way for a reason. If you want "advanced" binding scenarios - I would recommend to stick with controllers.
@MassimilianoKraus Also one of the links in the linked answer about Newtonsoft links to a way to reuse the MVC model binders - ModelBinderOfT @github
@MassimilianoKraus "I don't think it would have been such a pain to include it in the Minimal APIs" - one of the main goals of Minimal APIs introduction as far as I understand was to vastly improve performance so they went with almost complete rewrite.
I found a solution that was suitable for my case, but +1 for all your support and useful information!
|
0

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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.