让 IMetadataDetailsProviders 在 ASP.NET 核心中 运行 不止一次

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

这是一个棘手的问题,需要对 ASP.NET 核心框架有一些深入的了解。我将首先解释我们的应用程序在 MVC 3 实现中发生了什么。

有一个复杂的要求需要解决,涉及特定视图上我们的 ViewModel 的 ModelMetaData。这是一个高度可配置的应用程序。因此,对于一个 "Journal Type",属性 可能是强制性的,而对于另一个,完全相同的 属性 可能是非强制性的。此外,它可能是一个 "Journal Type" 的单选按钮和另一个 select 列表。由于所有这些配置选项有大量的组合、混合和匹配,因此为每个可能的排列创建一个单独的 ViewModel 类型是不切实际的。因此,只有一种 ViewModel 类型,并且 ModelMetaData 是动态设置在该类型的属性上的。

这是通过创建自定义 ModelMetadataProvider(通过继承 DataAnnotationsModelMetadataProvider)完成的。

粉碎到现在,我们正在升级应用程序并在 ASP.NET Core 中编写服务器内容。我已经确定实现 IDisplayMetadataProvider 是在 ASP.NET Core 中修改模型元数据的等效方法。

问题是,该框架内置了缓存,任何 class 实现 IDisplayMetadataProvider 仅 运行 一次。我在调试 ASP.NET 核心框架时发现了这一点,this comment 证实了我的发现。这样的缓存将不再满足我们的要求,因为第一次访问 ViewModel 类型时,MetadataDetailsProvider 将 运行 并且结果将被缓存。但是,如上所述,由于高度动态的配置,我需要它在每个 ModelBinding 之前 运行。否则,我们将无法利用ModelState。第一次命中该端点时,元数据为所有未来的请求设置为石头。

而且我们有点需要利用使用反射遍历所有属性的递归过程来设置元数据,因为我们不想自己做这件事(超出我的薪水范围的巨大努力).

因此,如果有人认为我错过了新核心框架中的某些内容,请务必告诉我。即使它像删除 ModelBindersIDisplayMetadataProviders 的缓存功能一样简单(这就是我将在接下来的几天内通过 ASP.NET 源代码进行研究的内容) .

出于性能考虑,模型元数据被缓存。 Class DefaultModelMetadataProvider,这是 IModelMetadataProvider 接口的默认实现,负责此缓存。如果您的应用程序逻辑需要在每次请求时重建元数据,您应该用您自己的实现替换此实现。

如果您从 DefaultModelMetadataProvider 继承您的实现并覆盖最低限度以实现您的目标,您将使您的生活更轻松。似乎 GetMetadataForType(Type modelType) 应该足够了:

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);
    }
}

要用您的 CustomModelMetadataProvider 替换 DefaultModelMetadataProvider,请在 ConfigureServices() 中添加以下内容:

services.AddSingleton<IModelMetadataProvider, CustomModelMetadataProvider>();