Saltar al contenido

Cargar archivos y JSON en ASP.NET Core Web API

Por fin después de tanto trabajar ya hallamos el arreglo de este atolladero que muchos de nuestros usuarios de nuestro sitio web presentan. Si quieres aportar alguna información no dudes en dejar tu conocimiento.

Solución:

Modelo simple, menos código, sin envoltorio

Existe una solución más simple, fuertemente inspirada en la respuesta de Andrius. Usando el ModelBinderAttribute no es necesario especificar un modelo o un proveedor de carpetas. Esto ahorra mucho código. La acción de su controlador se vería así:

public IActionResult Upload(
    [ModelBinder(BinderType = typeof(JsonModelBinder))] SomeObject value,
    IList files)

    // Use serialized json object 'value'
    // Use uploaded 'files'

Implementación

Código detrás JsonModelBinder (vea GitHub o use el paquete 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

Aquí hay un ejemplo de una solicitud http sin procesar aceptada por la acción del controlador Upload encima.

A multipart/form-data la solicitud se divide en varias partes, cada una separada por el especificado boundary=12345. Cada parte tiene un nombre asignado en su Content-Disposition-encabezamiento. Con estos nombres predeterminados ASP.Net-Core sabe qué parte está vinculada a qué parámetro en la acción del controlador.

Archivos que están vinculados a IFormFile Además, es necesario especificar un filename como en la segunda parte de la solicitud. Content-Type no es requerido.

Otra cosa a tener en cuenta es que las partes json deben ser deserializables en los tipos de parámetros como se define en la acción del controlador. Entonces en este caso el tipo SomeObject debería tener una propiedad key 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--

Prueba con cartero

Postman se puede usar para llamar a la acción y probar el código del lado del servidor. Esto es bastante simple y principalmente impulsado por la interfaz de usuario. Cree una nueva solicitud y seleccione formulario-datos en el Cuerpo-Pestaña. Ahora puedes elegir entre texto y expediente para cada parte del requerimiento.

ingrese la descripción de la imagen aquí

Aparentemente, no hay una forma integrada de hacer lo que quiero. Así que terminé escribiendo el mío ModelBinder para manejar esta situación. No encontré ninguna documentación oficial sobre el enlace de modelos personalizados, pero utilicé esta publicación como referencia.

Personalizado ModelBinder buscará propiedades decoradas con FromJson attribute y deserializar string que provino de una solicitud de varias partes a JSON. Envuelvo mi modelo dentro de otra clase (envoltorio) que tiene modelo y IFormFile propiedades.

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)
         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 Files  get; set; 
    [FromJson]
    public MyModel Model  get; set;  // <-- JSON will be deserialized to this object


// Controller action:
public async Task Upload(MyModelWrapper modelWrapper)



// Add custom binder provider in Startup.cs ConfigureServices
services.AddMvc(properties => 

    properties.ModelBinderProviders.Insert(0, new JsonModelBinderProvider());
);

Estoy trabajando con Angular 7 en el front-end, así que utilizo el FormData class, que le permite agregar cadenas o blobs a un formulario. Se pueden sacar del formulario en la acción del controlador utilizando el [FromForm] attribute. Agrego el archivo al FormData objeto, y luego clasifico los datos que deseo enviar junto con el archivo, lo agrego al FormData objeto y deserializar el string en mi acción de controlador.

Al igual que:

//front-end:
let formData: FormData = new FormData();
formData.append('File', fileToUpload);
formData.append('jsonString', JSON.stringify(myObject));

//request using a var of type HttpClient
http.post(url, formData);

//controller action
public Upload([FromForm] IFormFile File, [FromForm] string jsonString)

    SomeType myObj = JsonConvert.DeserializeObject(jsonString);

    //do stuff with 'File'
    //do stuff with 'myObj'

Ahora tiene un identificador en el archivo y el objeto. Tenga en cuenta que el nombre que proporciona en la lista de parámetros de la acción de su controlador debe coincidir con el nombre que proporciona al agregar al FormData objeto en el front-end.

Aquí puedes ver las comentarios y valoraciones de los usuarios

Más adelante puedes encontrar las ilustraciones de otros programadores, tú incluso tienes la opción de dejar el tuyo si lo deseas.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *