c# - example - return json asp net core
Cargar archivos y JSON en ASP.NET Core Web API (4)
¿Cómo puedo cargar una lista de archivos (imágenes) y datos json en el controlador de la API web central de ASP.NET mediante la carga de varias partes?
Puedo recibir con éxito una lista de archivos, cargados con el tipo de contenido multipart/form-data
así:
public async Task<IActionResult> Upload(IList<IFormFile> files)
Y, por supuesto, puedo recibir con éxito el cuerpo de la solicitud HTTP formateado en mi objeto utilizando el formateador JSON predeterminado como ese:
public void Post([FromBody]SomeObject value)
Pero, ¿cómo puedo combinar estos dos en una sola acción de controlador? ¿Cómo puedo cargar tanto imágenes como datos JSON y hacer que se unan a mis objetos?
Simple, menos código, sin modelo de envoltura
Hay una solución más simple, fuertemente inspirada por la respuesta de Andrius . Al utilizar el ModelBinderAttribute
, no tiene que especificar un proveedor de modelo o carpeta. Esto ahorra mucho código. La acción de tu controlador se vería así:
public IActionResult Upload(
[ModelBinder(BinderType = typeof(JsonModelBinder))] SomeObject value,
IList<IFormFile> files)
{
// Use serialized json object ''value''
// Use uploaded ''files''
}
Implementación
Código detrás de JsonModelBinder
(o use el paquete completo de NuGet ):
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;
public class JsonModelBinder : IModelBinder {
public Task BindModelAsync(ModelBindingContext bindingContext) {
if (bindingContext == null) {
throw new ArgumentNullException(nameof(bindingContext));
}
// Check the value sent in
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None) {
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
// Attempt to convert the input value
var valueAsString = valueProviderResult.FirstValue;
var result = Newtonsoft.Json.JsonConvert.DeserializeObject(valueAsString, bindingContext.ModelType);
if (result != null) {
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}
Solicitud de ejemplo
Este es un ejemplo de una solicitud de http sin procesar aceptada por la acción del controlador Upload
arriba.
Una solicitud de multipart/form-data
se divide en varias partes, cada una separada por el boundary=12345
especificado boundary=12345
. Cada parte tiene un nombre asignado en su Content-Disposition
contenido. Con estos nombres, ASP.Net-Core
predeterminado sabe qué parte está vinculada a qué parámetro en la acción del controlador.
Los archivos que están vinculados a IFormFile
también necesitan especificar un filename
como en la segunda parte de la solicitud. No se requiere el Content-Type
.
Otra cosa a tener en cuenta es que las partes json deben ser deserializables en los tipos de parámetros definidos en la acción del controlador. Entonces, en este caso, el tipo SomeObject
debe tener una key
de propiedad de tipo string
.
POST http://localhost:5000/home/upload HTTP/1.1
Host: localhost:5000
Content-Type: multipart/form-data; boundary=12345
Content-Length: 218
--12345
Content-Disposition: form-data; name="value"
{"key": "value"}
--12345
Content-Disposition: form-data; name="files"; filename="file.txt"
Content-Type: text/plain
This is a simple text file
--12345--
Pruebas con el cartero
Postman se puede usar para activar la acción y probar el código del lado del servidor. Esto es bastante simple y en su mayoría impulsado por IU. Cree una nueva solicitud y seleccione datos de formulario en la pestaña Cuerpo . Ahora puede elegir entre texto y archivo para cada parte del requisito.
Aparentemente no hay una forma integrada de hacer lo que quiero. Así que terminé escribiendo mi propio ModelBinder
para manejar esta situación. No encontré ninguna documentación oficial sobre el enlace del modelo personalizado, pero utilicé esta publicación como referencia.
Custom ModelBinder
buscará las propiedades decoradas con el atributo FromJson
y deserializará la cadena que vino de la solicitud multiparte a JSON. Envuelvo mi modelo dentro de otra clase (contenedor) que tiene el modelo y IFormFile
propiedades de IFormFile
.
IJsonAttribute.cs:
public interface IJsonAttribute
{
object TryConvert(string modelValue, Type targertType, out bool success);
}
FromJsonAttribute.cs:
using Newtonsoft.Json;
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class FromJsonAttribute : Attribute, IJsonAttribute
{
public object TryConvert(string modelValue, Type targetType, out bool success)
{
var value = JsonConvert.DeserializeObject(modelValue, targetType);
success = value != null;
return value;
}
}
JsonModelBinderProvider.cs:
public class JsonModelBinderProvider : IModelBinderProvider
{
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
if (context.Metadata.IsComplexType)
{
var propName = context.Metadata.PropertyName;
var propInfo = context.Metadata.ContainerType?.GetProperty(propName);
if(propName == null || propInfo == null)
return null;
// Look for FromJson attributes
var attribute = propInfo.GetCustomAttributes(typeof(FromJsonAttribute), false).FirstOrDefault();
if (attribute != null)
return new JsonModelBinder(context.Metadata.ModelType, attribute as IJsonAttribute);
}
return null;
}
}
JsonModelBinder.cs:
public class JsonModelBinder : IModelBinder
{
private IJsonAttribute _attribute;
private Type _targetType;
public JsonModelBinder(Type type, IJsonAttribute attribute)
{
if (type == null) throw new ArgumentNullException(nameof(type));
_attribute = attribute as IJsonAttribute;
_targetType = type;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
// Check the value sent in
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult != ValueProviderResult.None)
{
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
// Attempt to convert the input value
var valueAsString = valueProviderResult.FirstValue;
bool success;
var result = _attribute.TryConvert(valueAsString, _targetType, out success);
if (success)
{
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
return Task.CompletedTask;
}
}
Uso:
public class MyModelWrapper
{
public IList<IFormFile> Files { get; set; }
[FromJson]
public MyModel Model { get; set; } // <-- JSON will be deserialized to this object
}
// Controller action:
public async Task<IActionResult> Upload(MyModelWrapper modelWrapper)
{
}
// Add custom binder provider in Startup.cs ConfigureServices
services.AddMvc(properties =>
{
properties.ModelBinderProviders.Insert(0, new JsonModelBinderProvider());
});
No estoy seguro de poder hacer las dos cosas en un solo paso.
La forma en que logré esto en el pasado es mediante la carga del archivo a través de ajax y la devolución de la url del archivo en la respuesta y luego pasarlo junto con la solicitud posterior para guardar el registro real.
Siguiendo la excelente respuesta de @ bruno-zell, si solo tiene un archivo (no IList<IFormFile>
con un IList<IFormFile>
), también puede declarar su controlador de la siguiente manera:
public async Task<IActionResult> Create([FromForm] CreateParameters parameters, IFormFile file)
{
const string filePath = "./Files/";
if (file.Length > 0)
{
using (var stream = new FileStream($"{filePath}{file.FileName}", FileMode.Create))
{
await file.CopyToAsync(stream);
}
}
// Save CreateParameters properties to database
var myThing = _mapper.Map<Models.Thing>(parameters);
myThing.FileName = file.FileName;
_efContext.Things.Add(myThing);
_efContext.SaveChanges();
return Ok(_mapper.Map<SomeObjectReturnDto>(myThing));
}
Luego puede usar el método de Postman que se muestra en la respuesta de Bruno para llamar a su controlador.