c# - ihttpactionresult - WebApi-Enlace desde Uri y Body
web api get multiple parameters (4)
¿Es posible vincular un modelo tanto del Uri como del Cuerpo?
Por ejemplo, dado lo siguiente:
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; }
}
¿Es posible crear una carpeta personalizada para que un PUT
a
/ api / products / 1
con un cuerpo JSON de:
{
"Name": "Product Name"
}
dará como resultado el modelo UpdateProduct
poblado con Id = 1
y Name = "Product Name"
?
Actualizar
Entiendo que podría cambiar la firma de acción a
public HttpResponseMessage Put(int id, UpdateProduct model)
{
}
Sin embargo, como se indica en la pregunta, específicamente deseo vincularme a un solo objeto modelo
También publiqué esta pregunta en el foro de discusión de WebApi Codeplex
Aquí hay una versión mejorada de la respuesta de Odyth que:
- Funciona también para solicitudes sin cuerpo, y
- Obtiene los parámetros de la cadena de consulta además de los valores de ruta.
Para abreviar, simplemente publico el método ExecuteBindingAsyncCore y un nuevo método auxiliar, el resto de la clase es el mismo.
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]);
}
Bien, se me ocurrió una forma de hacerlo. Básicamente, hice un filtro de acción que se ejecutará después de que el modelo se haya completado desde JSON. Luego verá los parámetros de URL y establecerá las propiedades apropiadas en el modelo. Fuente completa a continuación:
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));
}
}
}
}
}
Puede definir su propia DefaultActionValueBinder. Entonces puedes mezclar y combinar desde el cuerpo y uri. Aquí hay una publicación de blog con un ejemplo de MvcActionValueBinder for Web Api. Hacer su propio DefaultActionValueBinder es una solución preferida porque garantiza que el encuadernador habrá terminado antes de que se ejecute cualquier otro ActionFilterAttribute.
blogs.msdn.com/b/jmstall/archive/2012/04/18/…
ACTUALIZAR:
Tuve algunos problemas con la implementación en la publicación del blog e intenté que utilizara mis formateadores multimedia personalizados. Afortunadamente, todos mis objetos de solicitud se extienden desde una clase base, así que creé mi propio formateador.
en 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);
}
}
}
Si te entendí, esto debería funcionar de la caja, por ejemplo, esto funciona para mí:
[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 }
);
y en la solicitud: localhost: 56329 / Inicio / Prueba / 22 con cuerpo: {"Nombre": "herramienta"}
Tengo las propiedades de mi modelo establecidas de acuerdo con 22 y "herramienta".