Подтвердить что ты не робот

WebApi - привязка от Uri и Body

Можно ли связать модель как с Uri, так и с телом?

Например, учитывая следующее:

routes.MapHttpRoute(
    name: "API Default",
    routeTemplate: "api/{controller}/{id}",
    defaults: new { id = RouteParameter.Optional }
);

public class ProductsController : ApiController
{
    public HttpResponseMessage Put(UpdateProduct model)
    {

    }
}

public class UpdateProduct 
{
    int Id { get; set;}
    string Name { get; set; }
}

Можно ли создать настраиваемое связующее, чтобы PUT to

/API/Продукты/1

с телом JSON:

{
    "Name": "Product Name"
}

приведет к модели UpdateProduct, заполненной Id = 1 и Name = "Product Name"?

Обновление

Я понимаю, что я могу изменить сигнатуру действия на

public HttpResponseMessage Put(int id, UpdateProduct model)
{

}

Однако, как указано в вопросе, я специально хочу привязать к одному объекту модели

Я также разместил этот вопрос на форуме WebApi Codeplex

4b9b3361

Ответ 1

Здесь улучшенная версия odyth отвечает, что:

  • Работает и для бестелесных запросов, и
  • Получает параметры из строки запроса в дополнение к значениям маршрута.

Для краткости я просто отправлю метод ExecuteBindingAsyncCore и новый вспомогательный метод, остальная часть этого же класса.

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

Ответ 2

Вы можете определить свой собственный DefaultActionValueBinder. Затем вы можете смешивать и комбинировать тело и ури. Вот сообщение в блоге с примером MvcActionValueBinder для Web Api. Создание собственного DefaultActionValueBinder является предпочтительным решением, поскольку оно гарантирует, что связующее будет закончено до того, как будет выполнен любой другой ActionFilterAttribute.

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

UPDATE:

У меня были некоторые проблемы с реализацией в сообщении в блоге и попытка заставить его использовать мои собственные медиаформаты. К счастью, все мои объекты запроса простираются от базового класса, поэтому я создал свой собственный форматировщик.

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

Ответ 3

Хорошо, я придумал способ сделать это. В принципе, я создал фильтр действий, который будет запущен после того, как модель будет заполнена из JSON. Затем он просмотрит параметры URL и задает соответствующие свойства в модели. Полный исходный код ниже:

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

Ответ 4

Если я понял вас, это должно работать из коробки, например. это работает для меня:

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

и по запросу: localhost: 56329/Главная/Тест/22 с телом: { "Имя": "инструмент" }

У меня есть мои свойства модели, установленные соответственно 22 и "инструмент".