Estoy usando ASP.NET Core 1.1 MVC para crear una API JSON. Dado el siguiente modelo y método de acción:

public class TestModel
{
    public int Id { get; set; }

    [Range(100, 999)]
    public int RootId { get; set; }

    [Required, MaxLength(200)]
    public string Name { get; set; }

    public string Description { get; set; }
}

[HttpPost("/test/{rootId}/echo/{id}")]
public IActionResult TestEcho([FromBody] TestModel data)
{
    return Json(new
    {
        data.Id,
        data.RootId,
        data.Name,
        data.Description,
        Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
    });
}

La [FromBody] en mi método de acción parámetro está causando el modelo que se compromete desde el JSON de carga que es enviado a la estación, sin embargo también previene la Id y RootId propiedades de ser enlazados a través de los parámetros de la ruta.

Podría romper esta en separar modelos, uno obligado de la ruta y uno del cuerpo o también podría obligar a los clientes a enviar el id & rootId como parte de la carga, pero las dos soluciones parecen complicar las cosas más de lo que me gustaría y no me permiten mantener la lógica de validación en un solo lugar. Es allí cualquier manera de conseguir esta situación de trabajo donde el modelo puede ser enlazado correctamente y puedo mantener mi modelo & la lógica de validación juntos?

  • ¿te las arreglas para encontrar una solución?
InformationsquelleAutor heavyd | 2017-08-03

4 Comentarios

  1. 9

    Puede quitar el [FromBody] decorador en su entrada y dejar MVC unión mapa de las propiedades:

    [HttpPost("/test/{rootId}/echo/{id}")]
    public IActionResult TestEcho(TestModel data)
    {
        return Json(new
        {
            data.Id,
            data.RootId,
            data.Name,
            data.Description,
            Errors = ModelState.IsValid ? null : ModelState.SelectMany(x => x.Value.Errors)
        });
    }

    Más info:
    Modelo de enlace en ASP.NET Núcleo MVC

    ACTUALIZACIÓN

    Pruebas

    ASP.NET Núcleo MVC Ruta Mixta/FromBody Modelo de Enlace & Validación

    ASP.NET Núcleo MVC Ruta Mixta/FromBody Modelo de Enlace & Validación

    ACTUALIZACIÓN 2

    @heavyd, tienes razón en que los datos JSON requiere [FromBody] atributo para obligar a su modelo. Así que lo que he dicho anteriormente se trabajará en forma de datos, pero no con los datos JSON.

    Como alternativa, puede crear un cuaderno de modelo personalizado que se une a la Id y RootId propiedades de la url, mientras que ella se une al resto de las propiedades del cuerpo de la solicitud.

    public class TestModelBinder : IModelBinder
    {
        private BodyModelBinder defaultBinder;
    
        public TestModelBinder(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory) //: base(formatters, readerFactory)
        {
            defaultBinder = new BodyModelBinder(formatters, readerFactory);
        }
    
        public async Task BindModelAsync(ModelBindingContext bindingContext)
        {
            //callinng the default body binder
            await defaultBinder.BindModelAsync(bindingContext);
    
            if (bindingContext.Result.IsModelSet)
            {
                var data = bindingContext.Result.Model as TestModel;
                if (data != null)
                {
                    var value = bindingContext.ValueProvider.GetValue("Id").FirstValue;
                    int intValue = 0;
                    if (int.TryParse(value, out intValue))
                    {
                        //Override the Id property
                        data.Id = intValue;
                    }
                    value = bindingContext.ValueProvider.GetValue("RootId").FirstValue;
                    if (int.TryParse(value, out intValue))
                    {
                        //Override the RootId property
                        data.RootId = intValue;
                    }
                    bindingContext.Result = ModelBindingResult.Success(data);
                }
    
            }
    
        }
    }

    Crear un cuaderno proveedor de:

    public class TestModelBinderProvider : IModelBinderProvider
    {
        private readonly IList<IInputFormatter> formatters;
        private readonly IHttpRequestStreamReaderFactory readerFactory;
    
        public TestModelBinderProvider(IList<IInputFormatter> formatters, IHttpRequestStreamReaderFactory readerFactory)
        {
            this.formatters = formatters;
            this.readerFactory = readerFactory;
        }
    
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context.Metadata.ModelType == typeof(TestModel))
                return new TestModelBinder(formatters, readerFactory);
    
            return null;
        }
    }

    Y decirle MVC a utilizar:

    services.AddMvc()
      .AddMvcOptions(options =>
      {
         IHttpRequestStreamReaderFactory readerFactory = services.BuildServiceProvider().GetRequiredService<IHttpRequestStreamReaderFactory>();
         options.ModelBinderProviders.Insert(0, new TestModelBinderProvider(options.InputFormatters, readerFactory));
      });

    A continuación, el controlador dispone de:

    [HttpPost("/test/{rootId}/echo/{id}")]
    public IActionResult TestEcho(TestModel data)
    {...}

    Pruebas

    ASP.NET Núcleo MVC Ruta Mixta/FromBody Modelo de Enlace & Validación
    ASP.NET Núcleo MVC Ruta Mixta/FromBody Modelo de Enlace & Validación

    Puede agregar un Id y RootId a su JSON, pero van a ser ignorados como estamos sobrescribiendo en nuestro modelo de cuaderno.

    ACTUALIZACIÓN 3

    El de arriba permite que usted utilice su modelo de datos de anotaciones para la validación de Id y RootId. Pero creo que puede confundir a otros desarrolladores que sería en su código de la API. Yo sugeriría a sólo simplificar la firma de la API para aceptar un modelo diferente para el uso con [FromBody] y separada de las otras dos propiedades que vienen de la uri.

    [HttpPost("/test/{rootId}/echo/{id}")]
    public IActionResult TestEcho(int id, int rootId, [FromBody]TestModelNameAndAddress testModelNameAndAddress)

    Y sólo podría derecho un validador para toda tu entrada, como:

    //This would return a list of tuples of property and error message.
    var errors = validator.Validate(id, rootId, testModelNameAndAddress); 
    if (errors.Count() > 0)
    {
        foreach (var error in errors)
        {
            ModelState.AddModelError(error.Property, error.Message);
        }
    }
    • Una buena respuesta, y tiene sentido que se unen a las propiedades basadas en el cuerpo y la ruta valores por quitar el atributo. Yo sugeriría el uso de la ruta limitaciones, pero mi esperanza es que ellos todavía se unen a las propiedades del modelo. Voy a tener que probarlo y ver.
    • mis pruebas muestran MVC obtiene los de la ruta.
    • Fresco, a continuación, debo haber hecho algo mal en mi final. Al menos fueron capaces de confirmar que funciona.
    • has probado con un JSON cuerpo, en lugar de los datos del formulario? Mi prueba muestra el cuaderno de modelo no se unen a un JSON cuerpo sin [FromBody] y su enlace soporta esa postura. Enlace de datos con el formato de la solicitud de cuerpo
    • He probado el mío con un JSON cuerpo y tiene el mismo resultado que @heavyd
    • Me he deshecho de mi respuesta por el momento. Pero todavía estoy buscando en la unión.
    • He añadido una actualización a mi respuesta.

  2. 13

    Después de investigar me encontré con una solución de creación de nuevo modelo de cuaderno de + origen de enlace + atributo que combina la funcionalidad de BodyModelBinder y ComplexTypeModelBinder. En primer lugar se utiliza BodyModelBinder a leer desde el cuerpo y, a continuación, ComplexModelBinder llena de otros campos. Aquí el código:

    public class BodyAndRouteBindingSource : BindingSource
    {
        public static readonly BindingSource BodyAndRoute = new BodyAndRouteBindingSource(
            "BodyAndRoute",
            "BodyAndRoute",
            true,
            true
            );
    
        public BodyAndRouteBindingSource(string id, string displayName, bool isGreedy, bool isFromRequest) : base(id, displayName, isGreedy, isFromRequest)
        {
        }
    
        public override bool CanAcceptDataFrom(BindingSource bindingSource)
        {
            return bindingSource == Body || bindingSource == this;
        }
    }

    [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    public class FromBodyAndRouteAttribute : Attribute, IBindingSourceMetadata
    {
        public BindingSource BindingSource => BodyAndRouteBindingSource.BodyAndRoute;
    }

    public class BodyAndRouteModelBinder : IModelBinder
    {
        private readonly IModelBinder _bodyBinder;
        private readonly IModelBinder _complexBinder;
    
        public BodyAndRouteModelBinder(IModelBinder bodyBinder, IModelBinder complexBinder)
        {
            _bodyBinder = bodyBinder;
            _complexBinder = complexBinder;
        }
    
        public async Task BindModelAsync(ModelBindingContext bindingContext)
        {
            await _bodyBinder.BindModelAsync(bindingContext);
    
            if (bindingContext.Result.IsModelSet)
            {
                bindingContext.Model = bindingContext.Result.Model;
            }
    
            await _complexBinder.BindModelAsync(bindingContext);
        }
    }

    public class BodyAndRouteModelBinderProvider : IModelBinderProvider
    {
        private BodyModelBinderProvider _bodyModelBinderProvider;
        private ComplexTypeModelBinderProvider _complexTypeModelBinderProvider;
    
        public BodyAndRouteModelBinderProvider(BodyModelBinderProvider bodyModelBinderProvider, ComplexTypeModelBinderProvider complexTypeModelBinderProvider)
        {
            _bodyModelBinderProvider = bodyModelBinderProvider;
            _complexTypeModelBinderProvider = complexTypeModelBinderProvider;
        }
    
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            var bodyBinder = _bodyModelBinderProvider.GetBinder(context);
            var complexBinder = _complexTypeModelBinderProvider.GetBinder(context);
    
            if (context.BindingInfo.BindingSource != null
                && context.BindingInfo.BindingSource.CanAcceptDataFrom(BodyAndRouteBindingSource.BodyAndRoute))
            {
                return new BodyAndRouteModelBinder(bodyBinder, complexBinder);
            }
            else
            {
                return null;
            }
        }
    }

    public static class BodyAndRouteModelBinderProviderSetup
    {
        public static void InsertBodyAndRouteBinding(this IList<IModelBinderProvider> providers)
        {
            var bodyProvider = providers.Single(provider => provider.GetType() == typeof(BodyModelBinderProvider)) as BodyModelBinderProvider;
            var complexProvider = providers.Single(provider => provider.GetType() == typeof(ComplexTypeModelBinderProvider)) as ComplexTypeModelBinderProvider;
    
            var bodyAndRouteProvider = new BodyAndRouteModelBinderProvider(bodyProvider, complexProvider);
    
            providers.Insert(0, bodyAndRouteProvider);
        }
    }
    • Esto es absolutamente genial! Gracias. Esto debería ser la norma out-of-the-box en el marco.
    • Funciona como un encanto!!! perfectamente solucionado mi problema, gracias! También, no es sólo la unión de la ruta y el cuerpo, es en realidad vinculante desde la ruta/del cuerpo/de la cadena de consulta/header. es hecho por el ComplexTypeModelBinderProvider?
    • No recuerdo exactamente, pero supongo que sí. Funciona de forma recursiva, después de modelo de cuerpo (_bodyBinder.BindModelAsync), a continuación, complejo modelo de cuaderno de ejecutar modelos de unión de todo otra vez, para cada propiedad definida en el modelo.
    • Yo no soy shure si esta solución funciona en todos los casos como se esperaba. Para shure anterior proveedor debe ser el último en providers colección, de lo contrario colección/de la matriz de los captores no se invoca. Sigo usando pero observador de cómo la unión está trabajando
  3. 10
    1. Instalar El Paquete De HybridModelBinding

    2. Añadir a Statrup:

      services.AddMvc()
          .AddHybridModelBinder();
    3. Modelo:

      public class Person
      {
          public int Id { get; set; }
          public string Name { get; set; }
          public string FavoriteColor { get; set; }
      }
    4. Controlador:

      [HttpPost]
      [Route("people/{id}")]
      public IActionResult Post([FromHybrid]Person model)
      { }
    5. Solicitud:

      curl -X POST -H "Accept: application/json" -H "Content-Type:application/json" -d '{
          "id": 999,
          "name": "Bill Boga",
          "favoriteColor": "Blue"
      }' "https://localhost/people/123?name=William%20Boga"
    6. Resultado:

      {
          "Id": 123,
          "Name": "William Boga",
          "FavoriteColor": "Blue"
      }
    7. Hay otras características avanzadas.

  4. 2

    No he probado esto, por su ejemplo, pero debería funcionar asp.net núcleo de apoyo modelo de enlace como este.

    Puede crear modelo como este.

    public class TestModel
    {
        [FromRoute]
        public int Id { get; set; }
    
        [FromRoute]
        [Range(100, 999)]
        public int RootId { get; set; }
    
        [FromBody]
        [Required, MaxLength(200)]
        public string Name { get; set; }
    
        [FromBody]
        public string Description { get; set; }
    }

    Actualización 1: Anterior no funcionará en caso de que cuando la corriente no es rebobinado. Principalmente en su caso a la hora de publicar los datos json.

    Cuaderno de Modelo personalizado es la solución, pero si usted todavía no desee que se cree que uno y sólo quiere gestionar con el Modelo, a continuación, puede crear dos modelos.

    public class TestModel
        {
            [FromRoute]
            public int Id { get; set; }
    
            [FromRoute]
            [Range(100, 999)]
            public int RootId { get; set; }        
    
            [FromBody]
            public ChildModel OtherData { get; set; }        
        }
    
    
        public class ChildModel
        {            
            [Required, MaxLength(200)]
            public string Name { get; set; }
    
            public string Description { get; set; }
        }

    Nota : Esto funciona perfectamente con application/json de unión, ya que está trabajando poco diferente, a continuación, otro tipo de contenido.

    • He intentado eso y no funciona. Por una parte, usted sólo puede tener un FromBody atributo por cada solicitud, y poner el FromBody en la clase/parámetro de acción y el uso de FromRoute en las propiedades no funciona bien
    • Por favor, ver a mi update 1.
    • el FromBody en la clase/parámetro de acción y el uso de FromRoute en las propiedades » ¿por qué lo harían? Deje su acción param attributeless.
    • ASP.NET Núcleo no enlazar el contenido del cuerpo de la solicitud, a menos que explícitamente a poner el [FromBody] atributo en su acción parámetro/modelo de propiedad.

Dejar respuesta

Please enter your comment!
Please enter your name here