WebApi - Bind from both Uri and Body

前端 未结 4 1534
小鲜肉
小鲜肉 2020-12-13 12:51

Is it possible to bind a model from both the Uri and Body?

For instance, given the following:

routes.MapHttpRoute(
    name: \"API Default\",
    rou         


        
相关标签:
4条回答
  • 2020-12-13 13:09

    You can define your own DefaultActionValueBinder. Then you can mix and match from body and uri. Here is a blog post with an example of an MvcActionValueBinder for Web Api. Making your own DefaultActionValueBinderis a preferred solution because it guarantees the binder will have finished before any other ActionFilterAttribute are executed.

    http://blogs.msdn.com/b/jmstall/archive/2012/04/18/mvc-style-parameter-binding-for-webapi.aspx

    UPDATE:

    I had some trouble with the implementation in the blog post and trying to get it to use my custom media formatters. Luckily all my request objects extend from a base class of Request so I made my own formatter.

    in WebApiConfig

    config.ParameterBindingRules.Insert(0, descriptor => descriptor.ParameterType.IsSubclassOf(typeof (Request)) ? new BodyAndUriParameterBinding(descriptor) : null);
    

    BodyAndUriParameterBinding.cs

    public class BodyAndUriParameterBinding : HttpParameterBinding
    {
        private IEnumerable<MediaTypeFormatter> Formatters { get; set; }
        private IBodyModelValidator BodyModelValidator { get; set; }
        public BodyAndUriParameterBinding(HttpParameterDescriptor descriptor)
            : base (descriptor)
        {
            var httpConfiguration = descriptor.Configuration;
            Formatters = httpConfiguration.Formatters;
            BodyModelValidator = httpConfiguration.Services.GetBodyModelValidator();
        }
    
        private Task<object> ReadContentAsync(HttpRequestMessage request, Type type,
            IEnumerable<MediaTypeFormatter> formatters, IFormatterLogger formatterLogger, CancellationToken cancellationToken)
        {
            var content = request.Content;
            if (content == null)
            {
                var defaultValue = MediaTypeFormatter.GetDefaultValueForType(type);
                return defaultValue == null ? Task.FromResult<object>(null) : Task.FromResult(defaultValue);
            }
    
            return content.ReadAsAsync(type, formatters, formatterLogger, cancellationToken);
        }
    
        public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,
            CancellationToken cancellationToken)
        {
            var paramFromBody = Descriptor;
            var type = paramFromBody.ParameterType;
            var request = actionContext.ControllerContext.Request;
            var formatterLogger = new ModelStateFormatterLogger(actionContext.ModelState, paramFromBody.ParameterName);
            return ExecuteBindingAsyncCore(metadataProvider, actionContext, paramFromBody, type, request, formatterLogger, cancellationToken);
        }
    
        // Perf-sensitive - keeping the async method as small as possible
        private async Task ExecuteBindingAsyncCore(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,
            HttpParameterDescriptor paramFromBody, Type type, HttpRequestMessage request, IFormatterLogger formatterLogger,
            CancellationToken cancellationToken)
        {
            var model = await ReadContentAsync(request, type, Formatters, formatterLogger, cancellationToken);
    
            if (model != null)
            {
                var routeParams = actionContext.ControllerContext.RouteData.Values;
                foreach (var key in routeParams.Keys.Where(k => k != "controller"))
                {
                    var prop = type.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);
                    if (prop == null)
                    {
                        continue;
                    }
                    var descriptor = TypeDescriptor.GetConverter(prop.PropertyType);
                    if (descriptor.CanConvertFrom(typeof(string)))
                    {
                        prop.SetValue(model, descriptor.ConvertFromString(routeParams[key] as string));
                    }
                }
            }
    
            // Set the merged model in the context
            SetValue(actionContext, model);
    
            if (BodyModelValidator != null)
            {
                BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName);
            }
        }
    }
    

    Request.cs

    public abstract class Request : IValidatableObject
    {
        public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            yield return ValidationResult.Success;
        }
    }
    
    0 讨论(0)
  • 2020-12-13 13:11

    If I understood you, this should work out of the box, e.g. this works for me:

        [HttpPost]
        public ActionResult Test(TempModel model)
        {
            ViewBag.Message = "Test: " + model.Id +", " + model.Name;
    
            return View("About");
        }
    
    public class TempModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
    
    routes.MapRoute(
                name: "Default",
                url: "{controller}/{action}/{id}",
                defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
            );
    

    and on the request: localhost:56329/Home/Test/22 with body:{"Name":"tool"}

    I have my model's properties set accordingly to 22 and "tool".

    0 讨论(0)
  • 2020-12-13 13:23

    Here's an improved version of odyth's answer that:

    1. Works for bodiless requests too, and
    2. Gets parameters from the query string in addition to from route values.

    For brevity I just post the ExecuteBindingAsyncCore method and a new auxiliary method, the rest of the class is the same.

    private async Task ExecuteBindingAsyncCore(ModelMetadataProvider metadataProvider, HttpActionContext actionContext,
            HttpParameterDescriptor paramFromBody, Type type, HttpRequestMessage request, IFormatterLogger formatterLogger,
            CancellationToken cancellationToken)
    {
        var model = await ReadContentAsync(request, type, Formatters, formatterLogger, cancellationToken);
    
        if(model == null) model = Activator.CreateInstance(type);
    
        var routeDataValues = actionContext.ControllerContext.RouteData.Values;
        var routeParams = routeDataValues.Except(routeDataValues.Where(v => v.Key == "controller"));
        var queryStringParams = new Dictionary<string, object>(QueryStringValues(request));
        var allUriParams = routeParams.Union(queryStringParams).ToDictionary(pair => pair.Key, pair => pair.Value);
    
        foreach(var key in allUriParams.Keys) {
            var prop = type.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);
            if(prop == null) {
                continue;
            }
            var descriptor = TypeDescriptor.GetConverter(prop.PropertyType);
            if(descriptor.CanConvertFrom(typeof(string))) {
                prop.SetValue(model, descriptor.ConvertFromString(allUriParams[key] as string));
            }
        }
    
        // Set the merged model in the context
        SetValue(actionContext, model);
    
        if(BodyModelValidator != null) {
            BodyModelValidator.Validate(model, type, metadataProvider, actionContext, paramFromBody.ParameterName);
        }
    }
    
    private static IDictionary<string, object> QueryStringValues(HttpRequestMessage request)
    {
        var queryString = string.Join(string.Empty, request.RequestUri.ToString().Split('?').Skip(1));
        var queryStringValues = System.Web.HttpUtility.ParseQueryString(queryString);
        return queryStringValues.Cast<string>().ToDictionary(x => x, x => (object)queryStringValues[x]);
    }
    
    0 讨论(0)
  • 2020-12-13 13:36

    Alright, I came up with a way to do it. Basically, I made an action filter which will run after the model has been populated from JSON. It will then look at the URL parameters, and set the appropriate properties on the model. Full source below:

    using System.ComponentModel;
    using System.Linq;
    using System.Net;
    using System.Net.Http;
    using System.Reflection;
    using System.Web.Http.Controllers;
    using System.Web.Http.Filters;
    
    
    public class UrlPopulatorFilterAttribute : ActionFilterAttribute
    {
        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            var model = actionContext.ActionArguments.Values.FirstOrDefault();
            if (model == null) return;
            var modelType = model.GetType();
            var routeParams = actionContext.ControllerContext.RouteData.Values;
    
            foreach (var key in routeParams.Keys.Where(k => k != "controller"))
            {
                var prop = modelType.GetProperty(key, BindingFlags.IgnoreCase | BindingFlags.Instance | BindingFlags.Public);
                if (prop != null)
                {
                    var descriptor = TypeDescriptor.GetConverter(prop.PropertyType);
                    if (descriptor.CanConvertFrom(typeof(string)))
                    {
                        prop.SetValueFast(model, descriptor.ConvertFromString(routeParams[key] as string));
                    }
                }
            }
        }
    }
    
    0 讨论(0)
提交回复
热议问题