Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Getting IMetadataDetailsProviders to Run More than Once in ASP.NET Core

This is a tricky question which will require some deep knowledge of the ASP.NET Core framework. I'll first explain what is happening in our application in the MVC 3 implementation.

There was a complex requirement which needed to be solved involving the ModelMetaData for our ViewModels on a particular view. This is a highly configurable application. So, for one "Journal Type", a property may be mandatory, whereas for another, the exact same property may be non-mandatory. Moreover, it may be a radio-button for one "Journal Type" and a select list for another. As there was a huge number of combinations, mixing and matching for all these configuration options, it was not practical to create a separate ViewModel type for each and every possible permutation. So, there was one ViewModel type and the ModelMetaData was set on the properties of that type dynamically.

This was done by creating a custom ModelMetadataProvider (by inheriting DataAnnotationsModelMetadataProvider).

Smash-cut to now, where we are upgrading the application and writing the server stuff in ASP.NET Core. I have identified that implementing IDisplayMetadataProvider is the equivalent way of modifying Model Metadata in ASP.NET Core.

The problem is, the framework has caching built into it and any class which implements IDisplayMetadataProvider only runs once. I discovered this while debugging the ASP.NET Core framework and this comment confirms my finding. Our requirement will no longer be met with such caching, as the first time the ViewModel type is accessed, the MetadataDetailsProvider will run and the result will be cached. But, as mentioned above, owing to the highly dynamic configuration, I need it to run prior to every ModelBinding. Otherwise, we will not be able to take advantage of ModelState. The first time that endpoint is hit, the meta-data is set in stone for all future requests.

And we kinda need to leverage that recursive process of going through all the properties using reflection to set the meta-data, as we don't want to have to do that ourselves (a massive endeavour beyond my pay-scale).

So, if anyone thinks there's something in the new Core framework which I have missed, by all means let me know. Even if it is as simple as removing that caching feature of ModelBinders and IDisplayMetadataProviders (that is what I'll be looking into over the next couple of days by going through the ASP.NET source).

like image 257
onefootswill Avatar asked Nov 14 '17 23:11

onefootswill


1 Answers

Model Metadata is cached due to performance considerations. Class DefaultModelMetadataProvider, which is default implementation of IModelMetadataProvider interface, is responsible for this caching. If your application logic requires that metadata is rebuilt on every request, you should substitute this implementation with your own.

You will make your life easier if you inherit your implementation from DefaultModelMetadataProvider and override bare minimum for achieving your goal. Seems like GetMetadataForType(Type modelType) should be enough:

public class CustomModelMetadataProvider : DefaultModelMetadataProvider
{
    public CustomModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider)
        : base(detailsProvider)
    {
    }

    public CustomModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider, IOptions<MvcOptions> optionsAccessor)
        : base(detailsProvider, optionsAccessor)
    {
    }

    public override ModelMetadata GetMetadataForType(Type modelType)
    {
        //  Optimization for intensively used System.Object
        if (modelType == typeof(object))
        {
            return base.GetMetadataForType(modelType);
        }

        var identity = ModelMetadataIdentity.ForType(modelType);
        DefaultMetadataDetails details = CreateTypeDetails(identity);

        //  This part contains the same logic as DefaultModelMetadata.DisplayMetadata property
        //  See https://github.com/aspnet/Mvc/blob/dev/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs

        var context = new DisplayMetadataProviderContext(identity, details.ModelAttributes);
        //  Here your implementation of IDisplayMetadataProvider will be called
        DetailsProvider.CreateDisplayMetadata(context);
        details.DisplayMetadata = context.DisplayMetadata;

        return CreateModelMetadata(details);
    }
}

To replace DefaultModelMetadataProvider with your CustomModelMetadataProvider add following in ConfigureServices():

services.AddSingleton<IModelMetadataProvider, CustomModelMetadataProvider>();
like image 97
CodeFuller Avatar answered Sep 23 '22 22:09

CodeFuller