5

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).

onefootswill
  • 3,707
  • 6
  • 47
  • 101

1 Answers1

9

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>();
CodeFuller
  • 30,317
  • 3
  • 63
  • 79
  • Tremendous! I don't know how I missed that! It's one of the few classes that I did not scrutinize. Perhaps I'll build in some smarts so that the caching stays in tact for all ModelTypes except the one which needs the run-every-time logic. Thanks very much. – onefootswill Nov 17 '17 at 07:32
  • They've made things so much harder to figure out. In the old version, you just set IsRequired to false and done. That's a getter only now and all the attribute lists are Readonly collections. Figuring out how to dynamically set whether a Property or not is required is proving to be a massive time-sink. – onefootswill Nov 18 '17 at 01:27
  • Think I have figured that part out. Need to implement IValidationMetadataProvider and I can set IsRequired in that. Hopefully that gets me all the way now. – onefootswill Nov 18 '17 at 01:56
  • @onefootswill Would be nice if you'd post your final solution as an answer, for posterity. – Ian Kemp Oct 22 '20 at 15:55
  • @IanKemp Hi Ian. I'll have to try and dig that code up. I'd be happy to post it here if I can find it. I'll need a bit of time though, as we are in the midst of a looming deadline. – onefootswill Oct 23 '20 at 01:33
  • @onefootswill so if I want to rebuild metadata on every request for [Display(...)] I should override DefaultModelMEtadataProvider and If I want to override [Required(ErrorMessage="")] I should use IValidationMetaDataProvider ? I am trying to use this for localization. Can't use default provider cause it caches it and reuse everytime and if a client customize the string it won't take effect.... – user2058413 Nov 20 '21 at 04:34
  • Mad how many differently-worded Google searches it took before I found this! The link to the .NET source code for `DefaultModelMetadataProvider` is broken. You can now find it [here](https://source.dot.net/#Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs,d40147c1660b8a5b). – Philip Stratford Mar 15 '23 at 12:22
  • If I only want to rebuild the metadata sometimes - i.e. if _a thing_ has changed - is there a way to clear the cached metadata, rather than have to implement a custom `DefaultModelMetadataProvider` and rebuild the metadata every time, even if nothing has changed? – Philip Stratford Mar 15 '23 at 12:51