Mvc: Asp net core rc2. Abstract class model binding

Created on 21 May 2016  路  19Comments  路  Source: aspnet/Mvc

Following this StackOverflow post:
http://stackoverflow.com/questions/37318029/asp-net-core-rc2-abstract-class-model-binding

In the RC1 I use the following code for abstract classes or interfaces binding:

public class MessageModelBinder : IModelBinder {

    public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext) {
        if(bindingContext.ModelType == typeof(ICommand)) {
            var msgTypeResult = bindingContext.ValueProvider.GetValue("messageType");
            if(msgTypeResult == ValueProviderResult.None) {
                return ModelBindingResult.FailedAsync(bindingContext.ModelName);
            }
            var type = Assembly.GetAssembly(typeof(MessageModelBinder )).GetTypes().SingleOrDefault(t => t.FullName == msgTypeResult.FirstValue);
            if(type == null) {
                return ModelBindingResult.FailedAsync(bindingContext.ModelName);
            }
            var metadataProvider = (IModelMetadataProvider)bindingContext.OperationBindingContext.HttpContext.RequestServices.GetService(typeof(IModelMetadataProvider));
            bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(type);
        }
        return ModelBindingResult.NoResultAsync;
    }
}

This binder only reads model type (messageType parameter) from query string and overrides metadata type. And the rest of the work performed by standard binders such as BodyModelBinder.

In Startup.cs I just add first binder:

services.AddMvc().Services.Configure<MvcOptions>(options => {
    options.ModelBinders.Insert(0, new MessageModelBinder());
});

Controller:

[Route("api/[controller]")]
public class MessageController : Controller {
    [HttpPost("{messageType}")]
    public ActionResult Post(string messageType, [FromBody]ICommand message) {
    } 
}

How can I perform this in RC2?

As far as I understand, now I have to use IModelBinderProvider. OK, I tried.
Startup.cs:

services.AddMvc().Services.Configure<MvcOptions>(options => {
    options.ModelBinderProviders.Insert(0, new MessageModelBinderProvider());
});

ModelBinderProvider:

public class MessageModelBinderProvider : IModelBinderProvider {
    public IModelBinder GetBinder(ModelBinderProviderContext context) {
        if(context == null) {
            throw new ArgumentNullException(nameof(context));
        }
        return context.Metadata.ModelType == typeof(ICommand) ? new MessageModelBinder() : null;
    }
}

ModelBinder:

public class MessageModelBinder : IModelBinder {
    public Task BindModelAsync(ModelBindingContext bindingContext) {
        if(bindingContext.ModelType == typeof(ICommand)) {
            var msgTypeResult = bindingContext.ValueProvider.GetValue("messageType");
            if(msgTypeResult == ValueProviderResult.None) {
                bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
                return Task.FromResult(0);
            }
            var type = typeof(MessageModelBinder).GetTypeInfo().Assembly.GetTypes().SingleOrDefault(t => t.FullName == msgTypeResult.FirstValue);
            if(type == null) {
                bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
                return Task.FromResult(0);
            }
            var metadataProvider = (IModelMetadataProvider)bindingContext.OperationBindingContext.HttpContext.RequestServices.GetService(typeof(IModelMetadataProvider));
            bindingContext.ModelMetadata = metadataProvider.GetMetadataForType(type);
            bindingContext.Result = ModelBindingResult.Success(bindingContext.ModelName, Activator.CreateInstance(type));
        }
        return Task.FromResult(0);
    }
}

But I cannot specify NoResult. If I do not specify bindingContext.Result, I get null model in controller.
If I specify bindingContext.Result, I get empty model without setting model fields.

question

Most helpful comment

:ok: with the restrictions you have in place, I recommend something like:

``` c#
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (context.Metadata.ModelType != typeof(ICommand))
{
    return null;
}

var binders = new Dictionary<string, IModelBinder>();
foreach (var type in typeof(MessageModelBinderProvider).GetTypeInfo().Assembly.GetTypes())
{
    var typeInfo = type.GetTypeInfo();
    if (typeInfo.IsAbstract || typeInfo.IsNested)
    {
        continue;
    }

    if (!(typeInfo.IsClass && typeInfo.IsPublic))
    {
        continue;
    }

    if (!typeof(ICommand).IsAssignableFrom(type))
    {
        continue;
    }

    var metadata = context.MetadataProvider.GetMetadataForType(type);
    var binder = context.CreateBinder(metadata);
    binders.Add(type.FullName, binder);
}

return new MessageModelBinder(context.MetadataProvider, binders);

}

public class MessageModelBinder : IModelBinder
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly Dictionary _binders;

public MessageModelBinder(
    IModelMetadataProvider metadataProvider,
    Dictionary<string, IModelBinder> binders)
{
    _metadataProvider = metadataProvider;
    _binders = binders;
}

public async Task BindModelAsync(ModelBindingContext bindingContext)
{
    var messageTypeModelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "messageType");
    var messageTypeResult = bindingContext.ValueProvider.GetValue(messageTypeModelName);
    if (messageTypeResult == ValueProviderResult.None)
    {
        bindingContext.Result = ModelBindingResult.Failed();
        return;
    }

    IModelBinder binder;
    if (!_binders.TryGetValue(messageTypeResult.FirstValue, out binder))
    {
        bindingContext.Result = ModelBindingResult.Failed();
        return;
    }

    // Now know the type exists in the assembly.
    var type = Type.GetType(messageTypeResult.FirstValue);
    var metadata = _metadataProvider.GetMetadataForType(type);

    ModelBindingResult result;
    using (bindingContext.EnterNestedScope(
        metadata,
        bindingContext.FieldName,
        bindingContext.ModelName,
        model: null))
    {
        await binder.BindModelAsync(bindingContext);
        result = bindingContext.Result;
    }

    bindingContext.Result = result;
}

}
```

Note the above code does not activate the instance. The ComplexTypeModelBinder (a partial model for the above code) will do that.

All 19 comments

@dougbu @rynowak ?

@AR1ES it appears your RC1 code allows clients to create objects of any requested type on the server. Please see @blowdart's comments here: https://github.com/aspnet/Mvc/issues/4477#issuecomment-210176928

The model binding system in RC2 is optimized for the usual case where type information is available up front. Binders such as you've outlined were not on the radar because they are unsafe and perform poorly. But please let us know if your actual scenario is more constrained, something we can help you implement w/o @blowdart saying "Please don't do this." again.

@dougbu the types created by that model binder are restricted to those coming from a particular app-defined assembly. It certainly doesn't allow creating _any_ type on the server. Having said that, it's still not a good practice, because it is to easy to have a bug there.

@dougbu, @Eilon thanks for the answers.
This code is used only in private API. We have a number of commands executed by the various internal services. Each service has a built-in message dispatcher with a single entry point (Action) for receiving messages (Maybe a little strange architecture :smile:). The use of standard features has been very useful in this case. Of course we can use manual serialization / deserialization. But I still try to find a way to fix an existing solution.

:ok: with the restrictions you have in place, I recommend something like:

``` c#
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

if (context.Metadata.ModelType != typeof(ICommand))
{
    return null;
}

var binders = new Dictionary<string, IModelBinder>();
foreach (var type in typeof(MessageModelBinderProvider).GetTypeInfo().Assembly.GetTypes())
{
    var typeInfo = type.GetTypeInfo();
    if (typeInfo.IsAbstract || typeInfo.IsNested)
    {
        continue;
    }

    if (!(typeInfo.IsClass && typeInfo.IsPublic))
    {
        continue;
    }

    if (!typeof(ICommand).IsAssignableFrom(type))
    {
        continue;
    }

    var metadata = context.MetadataProvider.GetMetadataForType(type);
    var binder = context.CreateBinder(metadata);
    binders.Add(type.FullName, binder);
}

return new MessageModelBinder(context.MetadataProvider, binders);

}

public class MessageModelBinder : IModelBinder
{
private readonly IModelMetadataProvider _metadataProvider;
private readonly Dictionary _binders;

public MessageModelBinder(
    IModelMetadataProvider metadataProvider,
    Dictionary<string, IModelBinder> binders)
{
    _metadataProvider = metadataProvider;
    _binders = binders;
}

public async Task BindModelAsync(ModelBindingContext bindingContext)
{
    var messageTypeModelName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, "messageType");
    var messageTypeResult = bindingContext.ValueProvider.GetValue(messageTypeModelName);
    if (messageTypeResult == ValueProviderResult.None)
    {
        bindingContext.Result = ModelBindingResult.Failed();
        return;
    }

    IModelBinder binder;
    if (!_binders.TryGetValue(messageTypeResult.FirstValue, out binder))
    {
        bindingContext.Result = ModelBindingResult.Failed();
        return;
    }

    // Now know the type exists in the assembly.
    var type = Type.GetType(messageTypeResult.FirstValue);
    var metadata = _metadataProvider.GetMetadataForType(type);

    ModelBindingResult result;
    using (bindingContext.EnterNestedScope(
        metadata,
        bindingContext.FieldName,
        bindingContext.ModelName,
        model: null))
    {
        await binder.BindModelAsync(bindingContext);
        result = bindingContext.Result;
    }

    bindingContext.Result = result;
}

}
```

Note the above code does not activate the instance. The ComplexTypeModelBinder (a partial model for the above code) will do that.

var metadata = context.MetadataProvider.GetMetadataForType(type);
var binder = context.CreateBinder(metadata);

This creates complex type binder obviously but the parameter ICommand has [FromBody. Don't we need to use a body model binder?

At least, this code does not work for me. Inside the using block, bindingContext.Result is null. If I use a body model binder, I do get a result.

You might need the following inside the following in the MessageModelBinderProvider to ensure all of the created binders are BodyModelBinders.

``` c#
var originalSource = context.BindingInfo.BindingSource;
context.BindingInfo.BindingSource = BindingSource.Body;
foreach (var type in typeof(MessageModelBinderProvider).GetTypeInfo().Assembly.GetTypes())
{
...
}

context.BindingInfo.BindingSource = originalSource;

```

When GetBinder get called, context.BindingInfo.BindingSource is already Body source.

Perhaps I don't understand this fully but consider this line.

var metadata = context.MetadataProvider.GetMetadataForType(type);

type is going to be a complex type and will it not always return a complex type binder?

type is going to be a complex type and will it not always return a complex type binder?

The BodyModelBinderProvider is much earlier in the list of providers than the ComplexTypeModelBinderProvider (see MvcCoreMvcOptionsSetup). It gets the first chance at creating a binder.

@dougbu I got the idea. However, there is a slight problem with where the binding source should be set. It has to be set earlier since the method returns before the source is set and for this reason, the complex binder is returned instead of body model binder. Thanks for your inputs.

Updated https://github.com/aspnet/Mvc/issues/4703#issuecomment-221054383 code to correct a couple of issues and use RC2 APIs.

@dougbu I implemented something similar to your comment above https://github.com/aspnet/Mvc/issues/4703#issuecomment-221054383 but using an abstract class. After binding, the properties of the abstract class are being validated, but the properties of the children classes aren't being validated leaving the ValidationState in an 'Unvalidated' state for the model. Is there a way to ensure that the child properties get automatically validated as well?

@scottste the MVC validation system does not use runtime types when validating. If you need to validate additional properties, you have a couple of options:

  1. Change the abstract class to implement IValidatableObject and override the Validate() method in your subclasses.
  2. Change your custom model binder to add a ValidationStateEntry with ModelMetadata for the chosen type -- metadata in the example above.

We are closing this issue because no further action is planned for this issue. If you still have any issues or questions, please log a new issue with any additional details that you have.

I am having the same issue with .Net Core 2 and none of the above solutions work...

I can't get this to work with .Net core 2 either...

@Eilon can we re-open this?

Please log a new issue with a detailed scenario if you would like us to consider a change. I think that the key issues described in this particular issue have been resolved, so let's use a new issue to discuss anything additional.

Was this page helpful?
0 / 5 - 0 ratings