Hi,
I have an interface (Or it could be a base class or an attribute) in my app which is used to mark some models. It is possible for models to have properties of the other marked classes (They are generated based on the user's inputs).
public class Model1 : IMarker
{
public PrimitiveOrNotMarkedClasses PrimitiveOrNotMarkedClasses { get; set; }
}
public class Model2 : IMarker
{
public Model1 Model1 { get; set; }
public PrimitiveOrNotMarkedClasses PrimitiveOrNotMarkedClasses { get; set; }
}
public class Model3 : IMarker
{
public Model1 Model1 { get; set; }
public Model2 Model2 { get; set; }
public PrimitiveOrNotMarkedClasses PrimitiveOrNotMarkedClasses { get; set; }
}
Now, I want to have a binder that ensures all the marked classes be not null even when the request does not provide any information about them.
There are several controllers and most of which simply take a model.
public interface IMarker
{
}
public class MyController1 : Controller
{
public ActionResult MyAction(Model3 model)
{
if( model.PrimitiveOrNotMarkedClasses )
{
//Some code
}
// PrimitiveOrNotMarkedClasses may have default values and I want to be sure about the null reference exceptions.
if(model.Model2.Model1.PrimitiveOrNotMarkedClasses)
{
//Some code
}
}
}
What is the signature of the controller that receives all data?.
If it expects an IMarker you can design an Interface binder model binder, but then just all properties contained in IMarker will be bound by model binder.
Please explain better. A lot of solutions are possible, but model binder MUST have all information needed to take a decision. Moreover, allowing the model binder to create arbitrary classes might cause security issues.
@frankabbruzzese Thank you for your response.
I have updated the question to provide required data. It is enough for me to have a new instance of the each marked classes if there is not any related data in the request.
Maybe you don't need a custom model binder at all. It is enough a constructor for your models that creates the models contained in al its IMarker properties. This way if model binder doesn't create one of these properties that property retains its initial model.
If you want a general solution that doesn't rely on the model constructor, you may add a model-binder that inherits from the ComplexTypeModelBinder and whose provider selects it whenever an implementation of IMarker is detected. There, you should override just the CreateModel method. In your override when you create an IMarker you should build the whole object tree by using reflection to list all IMarker properties. The implementation is similar to the model binder described in this post of my blog.
I have a similar situation, but with a collection of an abstract class (rather than interfaces), and I can't seem to get anything working, either using the method of inheriting ComplexTypeModelBinder in your blog post, or with a full IModelBinder implementation e.g. from issue #4703.
I have entity models similar to these simplified versions:
public class WidgetBase
{
public int Id { get; set; }
public DateTime Created { get; set; }
}
public class WidgetHtml : WidgetBase
{
public string Html { get; set; }
}
public class WidgetImage : WidgetBase
{
public string ImageUrl { get; set; }
}
public class Page
{
public List<WidgetBase> Widgets { get; set; }
}
The html is being rendered like this:
<input type="hidden" data-val="true" data-val-required="The Id field is required." id="Widgets_0__Id" name="Widgets[0].Id" value="1" />
<input type="hidden" data-val="true" data-val-required="The Created field is required." id="Widgets_0__Created" name="Widgets[0].Created" value="06/09/2017 17:38:31" />
<input type="hidden" name="Widgets[0].ModelType" value="AbstractModelBinding.Models.WidgetHtml" />
聽 聽
<div class="row">
<div class="col-3">
<label for="Widgets_0__Html">Html</label>
</div>
<div class="col-6">
<textarea id="Widgets_0__Html" name="Widgets[0].Html"></textarea>
</div>
</div>
However, when I create a binder that inherits from ComplexTypeModelBinder, and return an instance of the correct derived class in the CreateModel method, the derived properties are still not bound.
I realise that there's something that I'm missing, but the documentation is a little scant, and mostly seems to refer to .Net Core v1.1.
Is using ComplexTypeModelBinder the correct approach, or should I be using a full custom IModelBinder implementation, and if so, how?
Does anyone have a full sample of how to achieve this in Core v2?
@J0nKn1ght ,
The codein my blog post is not intended for binding derived properties. There, an interfaces is used just to decouple Mvc stuff from business stuffs, so only interface properties need to be bound.
Moreover passing from 1.1 to 2.0 doesnt affected that part of the model binding code.
The issue is that model binder can no way understand which class must be created (among all possible subclasses). Thus you need a way to render this information in the view an then to use it in your custom model binder.
Your custom model binde, then, must:
Use the class encoding information to undertsnd which class to create
Create the right class, instead of the abstract base class, thanks to an override of the CreateModel method.
Invoke standard ComplexTypeModelBinder code
Bind derived properies that are not boind in previous point
The whole plan is quite complex.
It is implemented in the Mvc Controls Toolkit Core
Next week we will release the 2.0 compatible version
Thanks for the reply.
As I mentioned in my original comment, I am instantiating the correct derived class in the CreateModel method, so I think that the bits I'm missing are 3 & 4, but I'm not sure what you mean by 'invoke standard ComplexTypeModelBinder code' and 'bind derived properties'.
I couldn't see any code from the link you sent to the toolkit that implemented model binders - I don't know if I'm looking in the wrong place.
Do you know of any code samples for .Net Core v2 that implement steps 3 & 4?
Edit: Ok, I've found that the binding code is in the Toolkit Core project - I was looking in the github project that the site links to. I'll have a look at your code in more detail.
@J0nKn1ght my code is quite complex, since my binder do also other stuffs. Moreover, when the application starts it classifies all subclasses and assign a code to each of them.
I substitute the ComplexTypeModelBinder with this one: https://github.com/MvcControlsToolkit/MvcControlsToolkit.Core/blob/master/src/MvcControlsToolkit.Core/ModelBinding/TransformationModelBinder.cs
Which is passed a copy of the ComplexTypeModelBinder. When I understand wich subclass to create I first invoke the ComplexTypeModelBinder to get all main class properties bound.
After I use a custom MetaDataProvider (DiffMetaData) to invoke model binding again. However, now my DiffMetaData computes metadata just for the properties of the chosen subclass that are not contained in the main class. Thus this step binds just the derived properies.
@frankabbruzzese I've looked through your code, and I think that the crux of it was what I'd tried already, based on issue #4477.
I've tried to strip the code down to the bare minimum, so here's my model binder:
public class WidgetBinder : IModelBinder
{
private readonly ComplexTypeModelBinder _baseBinder;
private readonly IModelMetadataProvider _metadataProvider;
public WidgetBinder(ComplexTypeModelBinder baseBinder, IModelMetadataProvider metadataProvider)
{
_baseBinder = baseBinder;
_metadataProvider = metadataProvider;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
var modelName = bindingContext.ModelName;
// The form contains a hidden input 'ModelType' with the fullname of the derived class...
var widgetTypeResult = bindingContext.ValueProvider.GetValue($"{modelName}.ModelType");
if (widgetTypeResult == ValueProviderResult.None)
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
var type = Type.GetType(widgetTypeResult.FirstValue);
bindingContext.ModelMetadata = _metadataProvider.GetMetadataForType(type);
ModelBindingResult result;
using (bindingContext.EnterNestedScope(
modelMetadata: bindingContext.ModelMetadata,
fieldName: bindingContext.FieldName,
modelName: bindingContext.ModelName,
model: null))
{
await _baseBinder.BindModelAsync(bindingContext);
result = bindingContext.Result;
}
bindingContext.Result = ModelBindingResult.Success(bindingContext.Result.Model);
return;
}
}
I'm passing the default ComplexTypeModelBinder into the constructor.
The data present in the form is as follows:
[Widgets[0].Id, 1]
[Widgets[0].Created, 07/09/2017 16:09:51]
[Widgets[0].ModelType, AbstractModelBinding.Models.WidgetHtml]
[Widgets[0].Html, test1]
[Widgets[1].Id, 2,2]
[Widgets[1].Created, 07/09/2017 16:09:51]
[Widgets[1].ModelType, AbstractModelBinding.Models.WidgetImage]
[Widgets[1].ImageUrl, test2]
[__RequestVerificationToken, CfDJ8AB4...]
If I leave the bindingContext.ModelMetadata as the default base class (WidgetBase, which has 'Id' and 'Created' properties), it works as expected (i.e. it populates the Id and Created properties).
If I populate bindingContext.ModelMetadata with the metadata from my derived class, e.g. WidgetHtml (which has 'Id', 'Created' and 'Html' properties), it throws an exception:
KeyNotFoundException: The given key was not present in the dictionary.
System.ThrowHelper.ThrowKeyNotFoundException()
System.Collections.Generic.Dictionary.get_Item(TKey key)
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder.BindProperty(ModelBindingContext bindingContext)
Microsoft.AspNetCore.Mvc.ModelBinding.Binders.ComplexTypeModelBinder+<BindModelCoreAsync>d__4.MoveNext()
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
AbstractModelBinding.Bindings.WidgetBinder+<BindModelAsync>d__3.MoveNext() in WidgetBinder.cs
+
await _baseBinder.BindModelAsync(bindingContext);
...
I'm obviously not an expert in this area, but I would have expected that if the metadata contains properties that are in the form values, it would have worked the same as the base class. Can anyone explain why setting the correct derived class metadata in the bindingContext causes the exception I'm getting? Is there some other property on bindingContext that I need to set before calling EnterNestedScope? I'm not sure how I find out what the key is that's missing from the dictionary.
I can zip up my test solution and attach it, if that would help.
Thanks.
@frankabbruzzese Ok - I admit that I'm a slow learner, but I think that I've actually found what I was doing wrong. The process has actually got to be:
In case it helps anyone else struggling with this, here's the code I've used.
IModelBinderProvider:
public class WidgetBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
Type baseClassType = typeof(WidgetBase);
if (context.Metadata.ModelType == baseClassType)
{
var binders = new Dictionary<string, IModelBinder>();
// Get all classes that inherit the target base class, and create a binder for them
var inheritedClasses = baseClassType.Assembly.GetTypes().Where(t => t.IsSubclassOf(baseClassType));
foreach (var inheritedClass in inheritedClasses)
{
var metadata = context.MetadataProvider.GetMetadataForType(inheritedClass);
var binder = context.CreateBinder(metadata);
binders.Add(inheritedClass.FullName, binder);
}
return new WidgetBinder(context.MetadataProvider, binders);
}
return null;
}
}
IModelBinder:
public class WidgetBinder : IModelBinder
{
private readonly IModelMetadataProvider _metadataProvider;
Dictionary<string, IModelBinder> _binders;
public WidgetBinder(IModelMetadataProvider metadataProvider, Dictionary<string, IModelBinder> binders)
{
_metadataProvider = metadataProvider;
_binders = binders;
}
public async Task BindModelAsync(ModelBindingContext bindingContext)
{
// Get the inherited type name from a hidden field with name 'ModelType' which contains the fullname of the type
var widgetTypeResult = bindingContext.ValueProvider.GetValue($"{bindingContext.ModelName}.ModelType");
if (widgetTypeResult == ValueProviderResult.None)
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
// Get the type of the inherited class
var type = Type.GetType(widgetTypeResult.FirstValue);
IModelBinder binder;
if (!_binders.TryGetValue(widgetTypeResult.FirstValue, out binder))
{
bindingContext.Result = ModelBindingResult.Failed();
return;
}
// Get the metadata for the inherited class
bindingContext.ModelMetadata = _metadataProvider.GetMetadataForType(type);
ModelBindingResult result;
// Call the binder
using (bindingContext.EnterNestedScope(
modelMetadata: bindingContext.ModelMetadata,
fieldName: bindingContext.FieldName,
modelName: bindingContext.ModelName,
model: null))
{
await binder.BindModelAsync(bindingContext);
// Store the result so we can get it outside of the scope
result = bindingContext.Result;
}
// Set the return value of the task
bindingContext.Result = ModelBindingResult.Success(result.Model);
return;
}
}
Startup - ConfigureServices:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc(o =>
{
o.ModelBinderProviders.Insert(0, new WidgetBinderProvider());
});
}
I've realised that I didn't cater for model validation in my IModelBinder implementation above. The BindModelAsync needs to include the following change to the end of the method:
// Add validation for the inherited class
bindingContext.ValidationState
.Add(result.Model, new ValidationStateEntry { Metadata = bindingContext.ModelMetadata });
// Set the return value of the task
bindingContext.Result = ModelBindingResult.Success(result.Model);
return;
I'm closing this issue because it appears to be a discussion and question that was all resolved.
Thanks @frankabbruzzese for working on this!
Most helpful comment
@frankabbruzzese Ok - I admit that I'm a slow learner, but I think that I've actually found what I was doing wrong. The process has actually got to be:
In case it helps anyone else struggling with this, here's the code I've used.
IModelBinderProvider:
IModelBinder:
Startup - ConfigureServices: