简体   繁体   中英

ASP.NET Core : HttpPost : Multiple object, same interface. How to use polymorphisme with custom binding

I have multiple Object with different structure, but each time i make a save on these objects, i want to carry out common control actions because these objects are in fact "content objects" of a similar process.

imagine in UI user can do tasks, he must enter different data depend on task but we stay in the same "process".

i want to avoid multiple HttPut Route, so i want something like this:

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

i want to have an instance of my specific object, i can determine type with a property "ContentType"

i would like to have a custombinder to do something like this:

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
}

i dont want to map properties manually, i want to work as if I had put "ContentA" or "ContentB" in [FromBody] parameter.

i just want to avoid this:

    [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
    }

I can have 20+ different content, that's a lot of different routes (of course I have different services behind that do different actions depending on content A or B but I would like to avoid as many routes).

I looked at the side of the custombinding but I did not manage to do what I wanted.

Thank you.

I'm not so sure if I understand your requirement correctly. From what you shared, the IContent is some kind of a base interface which contains a common set of members (properties, ...) for all the derived types (which should be well-known). So it's actually a scenario of polymorphic model binding and can be implemented by using a customer IModelBinder as demonstrated in a basic example here https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-5.0#polymorphic-model-binding

I've adjusted that example a bit to make it cleaner with more separate responsibility and better naming. The following code is not tested at all. Because the main logic is basically the same as from the example in the given link above:

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

Here is the default implementation for a IPolymorphTypeResolver which basically depends on an IValueProvider from the ModelBindingContext (which is just like what used in the Microsoft's example):

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

If you store the contentType in header or some other sources, just implement it your own way, here for the content type stored in the request header:

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

Here is the default implementation of IPolymorphTypeProvider :

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

Now we configure it to register everything needed including the PolymorphModelBinderProvider and your specific implementation of IPolymorphTypeResolver and IPolymorphTypeProvider :

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM