首页 > 解决方案 > ASP.NET Core:HttpPost:多个对象,相同的接口。如何在自定义绑定中使用多态性

问题描述

我有多个具有不同结构的对象,但每次我保存这些对象时,我都想执行常见的控制操作,因为这些对象实际上是类似过程的“内容对象”。

想象在 UI 中用户可以执行任务,他必须根据任务输入不同的数据,但我们停留在同一个“流程”中。

我想避免多个 HttPut Route,所以我想要这样的东西:

        [HttpPut("{processId}/content")]
        public ActionResult SaveContent(IContent content)
        {
            //Do something with the processID like security controll
            //Now i can do the different save 
        }

我想要一个我的特定对象的实例,我可以使用属性“ContentType”确定类型

我想要一个 custombinder 来做这样的事情:

if(content.GetContent==ContentA)
{
return ContentA with all properties binded from body
}

else if(content.GetContent==ContentB)
{
return ContentB with all properties binded from body
}

我不想手动映射属性,我想像在 [FromBody] 参数中放入“ContentA”或“ContentB”一样工作。

我只是想避免这种情况:

    [HttpPut("{processId}/content-a")]
    public ActionResult SaveContentA(ContentA contentA)
    {
        //Do something with the processID like security control
        //Now i can save contentA
    }

    [HttpPut("{processId}/content-b")]
    public ActionResult SaveContentB(ContentB contentb)
    {
        //Do something with the processID like security control
        //Now i can save contentB
    }

我可以有 20 多个不同的内容,这是很多不同的路线(当然,我有不同的服务,根据内容 A 或 B 执行不同的操作,但我想避免尽可能多的路线)。

我看着自定义绑定的一面,但我没有设法做我想做的事。

谢谢你。

标签: c#asp.netasp.net-core.net-core

解决方案


我不确定我是否正确理解了您的要求。从您共享的内容来看,这IContent是某种基本接口,其中包含所有派生类型(应该是众所周知的)的一组通用成员(属性,...)。所以它实际上是一个多态模型绑定的场景,可以通过使用客户来实现,IModelBinder如这里的基本示例所示https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model- binding?view=aspnetcore-5.0#polymorphic-model-binding

我对该示例进行了一些调整,以使其更清晰,具有更多独立的责任和更好的命名。以下代码根本没有经过测试。因为主要逻辑与上面给定链接中的示例基本相同:

//the marker interface to indicate the model should be polymorphically bound.
//you can replace this with your IContent
public interface IPolymorphModel { }
//a separate resolver to help resolve actual model type from ModelBindingContext
//(kind of separating responsibility
public interface IPolymorphTypeResolver
{
    Type ResolveType(ModelBindingContext modelBindingContext);
}
//a separate types provider to get all derived types from a base type/interface
public interface IPolymorphTypeProvider
{
    IEnumerable<Type> GetDerivedTypesFrom<T>();
}

//the custom model binder for polymorphism
public class PolymorphModelBinder : IModelBinder
{
    readonly IDictionary<Type, (ModelMetadata ModelMetadata, IModelBinder Binder)> _bindersByType;
    readonly IPolymorphTypeResolver _polymorphTypeResolver;
    public PolymorphModelBinder(IDictionary<Type, (ModelMetadata,IModelBinder)> bindersByType,
        IPolymorphTypeResolver polymorphTypeResolver)
    {
        _bindersByType = bindersByType;
        _polymorphTypeResolver = polymorphTypeResolver;
    }        
    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelType = _polymorphTypeResolver.ResolveType(bindingContext);
        if(modelType == null || 
          !_bindersByType.TryGetValue(modelType, out var binder))
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }            
        //create new binding context with the concrete/actual model type
        var actualBindingContext = DefaultModelBindingContext.CreateBindingContext(bindingContext.ActionContext,
            bindingContext.ValueProvider,
            binder.ModelMetadata,
            null,
            bindingContext.ModelName);
        await binder.Binder.BindModelAsync(actualBindingContext);
        //set back the result to the original bindingContext
        bindingContext.Result = actualBindingContext.Result;
        if (actualBindingContext.Result.IsModelSet)
        {
            bindingContext.ValidationState[bindingContext.Result] = new ValidationStateEntry {
                Metadata = binder.ModelMetadata
            };
        }
    }
}

//the custom model binder provider for polymorphism
public class PolymorphModelBinderProvider<T> : IModelBinderProvider
{
    readonly IPolymorphTypeResolver _polymorphTypeResolver;
    readonly IPolymorphTypeProvider _polymorphTypeProvider;
    public PolymorphModelBinderProvider(IPolymorphTypeResolver polymorphTypeResolver,
           IPolymorphTypeProvider polymorphTypeProvider)
    {
        _polymorphTypeResolver = polymorphTypeResolver;
        _polymorphTypeProvider = polymorphTypeProvider;
    }
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (!typeof(T).IsAssignableFrom(context.Metadata.ModelType)) return null;
        //prepare the list of the well-known derived types
        var derivedTypes = _polymorphTypeProvider.GetDerivedTypesFrom<T>();
        var binders = derivedTypes.ToDictionary(e => e, 
                                                e =>
                                                {
                                                    var modelMetadata = context.MetadataProvider.GetMetadataForType(e);
                                                    return (modelMetadata, context.CreateBinder(modelMetadata));
                                                }
                                    );
        return new PolymorphModelBinder(binders, _polymorphTypeResolver);
    }
}

这是 a 的默认实现,IPolymorphTypeResolver它基本上依赖于IValueProviderfrom the ModelBindingContext(就像 Microsoft 示例中使用的一样):

public class DefaultPolymorphTypeResolver : IPolymorphTypeResolver
{
    public Type ResolveType(ModelBindingContext modelBindingContext)
    {
        var contentTypeValueKey = ModelNames.CreatePropertyModelName(modelBindingContext.ModelName, "ContentType");
        var contentType = modelBindingContext.ValueProvider.GetValue(contentTypeValueKey).FirstValue;
        switch (contentType)
        {
            case "whatever ...":
                return ...;                    
            //...
            default:
                return null;
        }
    }
}

如果存储contentTypein 标头或其他一些来源,只需按照自己的方式实现即可,这里针对存储在请求标头中的内容类型:

public class DefaultPolymorphTypeResolver : IPolymorphTypeResolver {
   public Type ResolveType(ModelBindingContext modelBindingContext) {
       var contentType = modelBindingContext.ActionContext.HttpContext.Request.ContentType;
       switch (contentType)
        {                                 
            //...
            default:
                return null;
        }
   }
}

这是的默认实现IPolymorphTypeProvider

public class PolymorphContentTypeProvider : IPolymorphTypeProvider
{
    public IEnumerable<Type> GetDerivedTypesFrom<T>()
    {
        return new List<Type> { /* your logic to populate the list ... */ };
    }
}

现在我们将其配置为注册所需的所有内容,包括and和PolymorphModelBinderProvider您的特定实现:IPolymorphTypeResolverIPolymorphTypeProvider

//in ConfigureServices
services.AddSingleton<IPolymorphTypeResolver, DefaultPolymorphTypeResolver>();
services.AddSingleton<IPolymorphTypeProvider, PolymorphContentTypeProvider>();

services.AddOptions<MvcOptions>()
        .Configure((MvcOptions o, 
                    IPolymorphTypeResolver typeResolver, 
                    IPolymorphTypeProvider typesProvider) =>
         {
             o.ModelBinderProviders.Insert(0, new PolymorphModelBinderProvider<IPolymorphModel>(typeResolver, typesProvider));
        });

推荐阅读