ASP.NET Web API Operation with interfaces instead

2020-02-11 08:45发布

问题:

My team needs to develop a framework for our company and products using this framework. One of the requisites is that a product could be customized for a specific client, yet it should be easily updated with another version of the same product (not automatically).

We're using ASP.NET MVC 4 + Web API (for now, a desktop product will be created next year, based on our framework), NHibernate, heavily IoC using Autofac as DI container and N-Layers.

So, in some points of the WebApp, we're using ViewModels as Interfaces with one default implementation and using Autofac to link them, and that's is easy to change in futures customizations.

In ASP.NET MVC, we achieve this implementing IModelBinderProvider and creating a custom class inherating DefaultModelBinder:

Something like this:

public class MyCustomMVCModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(Type modelType)
    {
        if (modelType.IsInterface)
            return new MyCustomMVCModelBinder();

        return new DefaultModelBinder();
    }
}
public class MyCustomMVCModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        var context = new ModelBindingContext(bindingContext);
        var item = DependencyResolver.Current.GetService(bindingContext.ModelType);

        Func<object> modelAccessor = () => item;
        context.ModelMetadata = new ModelMetadata(new DataAnnotationsModelMetadataProvider(),
            bindingContext.ModelMetadata.ContainerType, modelAccessor, item.GetType(), bindingContext.ModelName);

        return base.BindModel(controllerContext, context);
    }
}

And in my controller I just use the interface as parameter. So, it works fine because the values are populated correctly.

But, how do I do the same in Web API with the values binded correctly? I tried implemeting IModelBinder and inheriting ModelBinderProvider. I can get the instance of interface's implementantion, but the values aren't populated.

Here's an attempt implementation in WebAPI:

public class MyCustomWebAPIModelBinderProvider : ModelBinderProvider
{
    public override IModelBinder GetBinder(System.Web.Http.HttpConfiguration configuration, Type modelType)
    {
        return new MyCustomWebAPIModelBinder();
    }
}

public class MyCustomWebAPIModelBinder : IModelBinder
{
    public bool BindModel(System.Web.Http.Controllers.HttpActionContext actionContext, ModelBindingContext bindingContext)
    {
        if (bindingContext.ModelType.IsInterface)
        {
            var item = GlobalConfiguration.Configuration.DependencyResolver.GetService(bindingContext.ModelType);

            if (item != null)
            {
                Func<object> modelAccessor = () => item;
                var a = bindingContext.ModelMetadata.ContainerType;
                var b = modelAccessor;
                var c = item.GetType();
                var d = bindingContext.ModelName;

                bindingContext.ModelMetadata = new ModelMetadata(new DataAnnotationsModelMetadataProvider(), a, b, c, d);

                bindingContext.Model = item;

                return true;
            }
        }

        return false;
    }
}

What am I missing? Is it possible to do what we want?

回答1:

Instead of using ModelBinder, use an implementation of MediaTypeFormatter.

You can overrides the method "ReadFromStreamAsync" and change the type to whatever you need.

In the example below, the type is changed by resolving the concrete type using the MVC's DependencyResolver, witch works fine for WebAPI.



    public class CustomFormUrlEncodedMediaTypeFormatter : FormUrlEncodedMediaTypeFormatter
    {
        public CustomFormUrlEncodedMediaTypeFormatter() : base() { }

        public override Task ReadFromStreamAsync(Type type, Stream readStream, HttpContent content, IFormatterLogger formatterLogger)
        {
            if (type.IsInterface)
                type = GetConcreteType(type);

            return base.ReadFromStreamAsync(type, readStream, content, formatterLogger);
        }

        public override Task WriteToStreamAsync(Type type, object value, Stream writeStream, HttpContent content, TransportContext transportContext)
        {
            if (type.IsInterface)
                type = GetConcreteType(type);

            return base.WriteToStreamAsync(type, value, writeStream, content, transportContext);
        }

        private Type GetConcreteType(Type type)
        {
            object concrete = System.Web.Mvc.DependencyResolver.Current.GetService(type);
            return concrete.GetType();
        }
    }

If you want to do that with the JsonFormatter, it's a better aproach to create a "CustomCreationConverter" for Json.NET, than they can resolve the depenencies for all child objects that you have in your interface.



    public class DomainConverter : CustomCreationConverter
    {
            public DomainConverter() { }

            public override bool CanConvert(Type objectType)
            {
                return objectType.IsInterface;
            }

            public override object Create(Type objectType)
            {
                return System.Web.Mvc.DependencyResolver.Current.GetService(objectType);
            }
    }