I'm using MVC.NET 5.2.3 and try to post a model to a controller where the model contains several interfaces. This causes .NET to throw this exception:
Cannot create an instance of an interface
I understand that it's because I use interfaces in my model (ITelephone). My models are as such:
public class AddContactPersonForm
{
public ExternalContactDto ExternalContact { get; set; }
public OrganizationType OrganizationType { get; set; }
}
public class ExternalContactDto
{
public int? Id { get; set; }
public string Name { get; set; }
public string Title { get; set; }
public IList<ITelephone> TelephoneNumbers { get; set; }
}
public interface ITelephone
{
string TelephoneNumber { get; set; }
}
public class TelephoneDto : ITelephone
{
public string TelephoneNumber { get; set; }
}
It works fine if I use the TelephoneDto class instead of the ITelephone interface.
I understand that I need to use a ModelBinder, which is fine. But I really just want to say what kind of instance the modelbinder should create, instead of mapping the entire model manually.
The answer @jonathanconway gave in this question is close to what I want to do.
Custom model binder for a property
But I really would like to combine this with the simplicity of simply telling the defaultbinder what type to use for a specific interface. Sort of the same way you can use the KnownType-attribute. The defaultbinder obviously knows how to map the model as long as it knows which class it should create.
How can I tell the DefaultModelBinder what class it should use to deserialize the interface and then bind it? It currently crashes because the model that is posted (AddContactPersonForm) contains a "complex" model (ExternalContactDto) which has the interface ITelephone.
This is what I got so far.
public class ContactPersonController : Controller
{
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddContactPerson([ModelBinder(typeof(InterfaceModelBinder))] AddContactPersonForm addContactPersonForm)
{
// Do something with the model.
return View(addContactPersonForm);
}
}
public class InterfaceModelBinder : DefaultModelBinder
{
protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext,
PropertyDescriptor propertyDescriptor)
{
var propertyBinderAttribute = TryFindPropertyBinderAttribute(propertyDescriptor);
if (propertyBinderAttribute != null)
{
// Never occurs since the model is nested.
var type = propertyBinderAttribute.ActualType;
var model = Activator.CreateInstance(type);
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
return;
}
// Crashed here since because:
// Cannot create an instance of an interface. Object type 'NR.Delivery.Contract.Models.ITelephone'.
base.BindProperty(controllerContext, bindingContext, propertyDescriptor);
}
private InterfaceBinderAttribute TryFindPropertyBinderAttribute(PropertyDescriptor propertyDescriptor)
{
return propertyDescriptor.Attributes
.OfType<InterfaceBinderAttribute>()
.FirstOrDefault();
}
}
public class ExternalContactDto
{
public int? Id { get; set; }
public string Name { get; set; }
public string Title { get; set; }
[InterfaceBinder(typeof(List<TelephoneDto>))]
public IList<ITelephone> TelephoneNumbers { get; set; }
}
public class InterfaceBinderAttribute : Attribute
{
public Type ActualType { get; private set; }
public InterfaceBinderAttribute(Type actualType)
{
ActualType = actualType;
}
}