How do I get a ModelMetaData from a ModelMetaDataIdentity? (asp.net core)

98 Views Asked by At

I have come across a very complicated problem. I will try and explain it in as much detail as I can give (without giving away any sensitive info).

I have a class structure similar to the following:

abstract class Parent 
{

}

class X : Parent
{
 Parent ParProp;
}

class Y : Parent
{
 List<Parent> ListParProp;
}

class Z : Parent
{
 //Other properties.
}

Now, I have a custom polymorphic recursive model binder (and an associated provider) attached to the parent class.

So far, it identifies each element type by a hidden input, binds the given element by the type (MetadataProvider.GetMetadataForType), and then specially bind any child elements of type Parent (or any nested depth of IEnumerable that has Parent as the final type), and finally attaches the child element bound to the parent element using reflection.

Now, while going through this recently I noticed something weird, it was cycling through the Parent model binder too many times. I realized that this is because, when it tries to bind the child elements by type, it doesn't know about the container instance, so once it's done binding it doesn't know to automatically set the bound instance to the container.

So, through experimenting and what not, I have realized that the thing that determines how the property is bound is the ModelMetaData. Now the ModelMetaDataProvider interface has 2 relevant methods, GetMetadataForType and GetMetadataForProperty. Naturally I tried using the GetMetaDataForProperty, but the property was (of course) of type parent, so I ended up in an infinite recursive loop that kept trying to bind elements of type Parent.

Next, I had a look around and realized that there exists a ModelMetaDataIdentity, which is supposed to point to a specific ModelMetaData, and that ModelMetaDataIdentity has a static method: ForProperty(PropertyInfo propertyInfo, Type modelType, Type containerType) and boom, I created the exact ModelMetaDataIdentity I needed. That meaning it has the appropriate ModelType, and the appropriate container type/info. In theory, this ModelMetaDataIdentity knows that the model is an implementation of a subclass of Parent, and that it should be a property of the container class's member of type Parent!

Now the only thing left is to get the actual ModelMetaData. And this is where the fun starts, because ModelMetaData is an abstract class with one constructor. That constructor happens to be ModelMetaData(ModelMetaDataIdentity identity)! Except that constructor is Protected.

Why is this an issue?

Well, while this system works, it leads to me having to make a lot of variables nullable. This is because after the inbuilt complex object binding, the properties of type Parent end up being null and the automatic validator really doesn't like that.

Solutions

While I could look into using some convoluted reflection to manually invoke the protected constructor, I refuse to do so as the chances that it breaks as soon as there's any update are far too great. Things are non public for a reason.

Alternatively, since performance isn't a big issue, I could just say screw it and stick to my previous approach of hacking it together in a rather crude fashion, and say that the nullability of a lot of the elements is just a limitation of the software.

Ideally, I would like to find a good way to acquire the ModelMetaData for the implementation of the parent class, while still retaining the information about it being a property so that the automatic binder can handle more of the work load.

As such, I turn to you good people. Do you have any idea how I can achieve this?

Edit:

I was requested to share my model binder. It's based on the polymorphic one given by microsoft, the main difference is that I need to handle nested polymorphic binding instead of plain polymorphic binding.

I will try again to share it, without giving out any secret details.

BinderProvider

public IModelBinder GetBinder(ModelBinderProviderContext context)
{
    if (context.Metadata.ModelType != typeof(ParentModel))
    {
        return null;
    }

    var subclasses = new[] {typeof(X), typeof(Y), typeof(Z) }; //Etc, in my code I all the actual implementations of the parent class.

    var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
    foreach (var type in subclasses)
    {
        var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
        binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
    }

    {
        Type type = typeof(Parent);
        var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
        binders[type] = (modelMetadata, null);
    }

    return new ParentModelBinder(binders);
}

Binder


        public async Task BindModelAsync(ModelBindingContext bindingContext)
        {
            var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Parent));
            var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

            IModelBinder modelBinder;
            ModelMetadata modelMetadata;

            Type? modelType = Type.GetType(modelsNamespace + modelTypeValue);

            if (modelType != null && binders.ContainsKey(modelType))
            {
                (modelMetadata, modelBinder) = binders[modelType];
            }
            else
            {
                bindingContext.Result = ModelBindingResult.Failed();
                return;
            }

            var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

            await modelBinder.BindModelAsync(newBindingContext);

            if (newBindingContext.Result.IsModelSet)
            {
                // If the result model is set, then we can recursively assign the properties.
                var qemProperties = ReflectorHelper.GetParentMembersFromType(modelType);

                foreach (var property in qemProperties)
                {
                    await BindProperty(property, newBindingContext);
                }

                // Setting the ValidationState ensures properties on derived types are correct 
                bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
                {
                    Metadata = modelMetadata,
                };
            }

            bindingContext.Result = newBindingContext.Result;
        }

To re-iterate, the model binder does technically work right now. It's just got some behaviours I am not satisfied with.

Some notes about differences here:

  • The provider gets a list of metadata/binder combos, and I use that list to get the appropriate metadata and binder.
  • It then binds the model of type Parent by type.
  • If this implementation of Parent has any properties of type Parent (or any IEnumerables with Parent as their final type) it will cycle through those, bind them again and attach the bound model to the parent type. This is, again, because binding of any implementation of Parent has to be done by type and as such the container details aren't included.
0

There are 0 best solutions below