Reputation: 358
I have an abstract object, BaseObject (example name), that has 2 country-specific implementations, UsaObject, and CanadaObject, both of which extend BaseObject.
public abstract class BaseObject...
public class UsaObject : BaseObject...
public class CanadaObject : BaseObject...
In my controller, I have an action
public void UpdateObject(BaseObject object) {
...
}
The action only calls methods on BaseObject, and doesn't need to cast.
I'm using Json.NET to perform serialization, and when I serialize objects, I am using JsonSerializationSettings.TypeNameHandling.Objects to add the type of my class to the objects. This allows me to easily send the data correctly in to the UI and change what is shown based on the country, without having to worry about the exact time until I need to.
However, I am having issues posting this data back in the UpdateObject method. It fails on deserialization with the following error (note that we are using Glimpse to perform performance analysis on the application, though I don't suspect this as the cause at all), which is caught on an IExceptionFilter.OnException call:
System.MissingMethodException: Cannot create an abstract class.
at System.RuntimeTypeHandle.CreateInstance(RuntimeType type, Boolean publicOnly, Boolean noCheck, Boolean& canBeCached, RuntimeMethodHandleInternal& ctor, Boolean& bNeedSecurityCheck)
at System.RuntimeType.CreateInstanceSlow(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)
at System.RuntimeType.CreateInstanceDefaultCtor(Boolean publicOnly, Boolean skipCheckThis, Boolean fillCache, StackCrawlMark& stackMark)
at System.Activator.CreateInstance(Type type, Boolean nonPublic)
at System.Activator.CreateInstance(Type type)
at System.Web.Mvc.DefaultModelBinder.CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
at System.Web.Mvc.DefaultModelBinder.BindComplexModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.DefaultModelBinder.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at Castle.Proxies.DefaultModelBinderProxy.BindModel_callback(ControllerContext controllerContext, ModelBindingContext bindingContext)
at Castle.Proxies.Invocations.DefaultModelBinder_BindModel.InvokeMethodOnTarget()
at Castle.DynamicProxy.AbstractInvocation.Proceed()
at Glimpse.Core.Extensibility.CastleInvocationToAlternateMethodContextAdapter.Proceed()
at Glimpse.Core.Extensions.AlternateMethodContextExtensions.TryProceedWithTimer(IAlternateMethodContext context, TimerResult& timerResult)
at Glimpse.Core.Extensibility.AlternateMethod.NewImplementation(IAlternateMethodContext context)
at Glimpse.Core.Extensibility.AlternateTypeToCastleInterceptorAdapter.Intercept(IInvocation invocation)
at Castle.DynamicProxy.AbstractInvocation.Proceed()
at Castle.Proxies.DefaultModelBinderProxy.BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
at System.Web.Mvc.ControllerActionInvoker.GetParameterValue(ControllerContext controllerContext, ParameterDescriptor parameterDescriptor)
at System.Web.Mvc.ControllerActionInvoker.GetParameterValues(ControllerContext controllerContext, ActionDescriptor actionDescriptor)
at System.Web.Mvc.Async.AsyncControllerActionInvoker.<>c__DisplayClass1e.<BeginInvokeAction>b__16(AsyncCallback asyncCallback, Object asyncState)
Is there a way to modify how MVC 5 is handling deserialization, and if so, at which point in the page lifecycle should I accomplish this?
Upvotes: 1
Views: 1296
Reputation: 358
Turns out I made a bad assumption that MVC uses Json.NET for deserialization (shame on me). MVC has its own ModelBinder, which is DefaultModelBinder by default. Bear with me on the solution, it was a little rushed and needs testing. I was able to extend the DefaultModelBinder,
public class AbstractModelBinder : DefaultModelBinder
{
protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType)
{
if (modelType.IsAbstract)
{
var typeName = bindingContext.ValueProvider.GetValue("$type");
if (typeName == null)
throw new Exception("Cannot create abstract model");
var type = Type.GetType(typeName);
if (type == null)
throw new Exception("Cannot create abstract model");
if (!type.IsSubclassOf(modelType))
throw new Exception("Incorrect model type specified");
var model = Activator.CreateInstance(type);
// this line is very important.
// It updates the metadata (and type) of the model on the binding context,
// which allows MVC to properly reflect over the properties
// and assign their values. Without this,
// it will keep the type as specified in the parameter list,
// and only the properties of the base class will be populated
bindingContext.ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType(() => model, type);
return model;
}
return base.CreateModel(controllerContext, bindingContext, modelType);
}
}
and registered it as my default binder on startup.
// in Global.asax.cs on application start
ModelBinders.Binders.DefaultBinder = new AbstractModelBinder();
I only change the behavior when the model type is abstract, and default otherwise. If it's abstract, but I can't create an instance of the type, or the type name is unavailable, etc., then for now I'm throwing an exception, but that's a detail that I can figure out later. This way, I don't need to use any attributes on my Actions' parameters, and the Json.NET type specifiers are being used as best as I can figure out at the moment. It would be nice to just have Json.NET deserialize the model for me, but this is the best solution I've found thus far.
I'd like to reference posts that helped me find my answer. Change the default model binder in asp.net MVC, ASP.NET MVC 3: DefaultModelBinder with inheritance/polymorphism
Upvotes: 1