web-dev-qa-db-fra.com

Télécharger des fichiers et JSON dans l’API Web ASP.NET Core

Comment puis-je télécharger une liste de fichiers (images) et de données json sur le contrôleur API Web ASP.NET Core à l'aide du téléchargement multipart?

Je peux recevoir avec succès une liste de fichiers, chargés avec le type de contenu multipart/form-data comme celui-ci:

public async Task<IActionResult> Upload(IList<IFormFile> files)

Et bien sûr, je peux recevoir avec succès le corps de la requête HTTP formatée pour mon objet en utilisant le formateur JSON par défaut, comme ceci:

public void Post([FromBody]SomeObject value)

Mais comment puis-je combiner ces deux dans une seule action de contrôleur? Comment télécharger des images et des données JSON et les lier à mes objets?

24
Andrius

Apparemment, il n'y a pas de moyen construit pour faire ce que je veux. Alors j’ai fini par écrire ma propre ModelBinder pour gérer cette situation. Je n'ai trouvé aucune documentation officielle sur la liaison des modèles personnalisés, mais j'ai utilisé ce message comme référence.

Custom ModelBinder recherche les propriétés décorées avec l'attribut FromJson et désérialise la chaîne issue d'une demande multipart en JSON. J'enveloppe mon modèle dans une autre classe (wrapper) qui possède les propriétés model et 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;
    }
}

Utilisation:

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());
});
12
Andrius

Modèle simple, moins de code, pas de wrapper

Il y a une solution plus simple, fortement inspirée par la réponse d'Andrius . En utilisant ModelBinderAttribute vous n'avez pas à spécifier de fournisseur de modèle ou de classeur. Cela économise beaucoup de code. Votre action de contrôleur ressemblerait à ceci:

public IActionResult Upload(
    [ModelBinder(BinderType = typeof(JsonModelBinder))] SomeObject value,
    IList<IFormFile> files)
{
    // Use serialized json object 'value'
    // Use uploaded 'files'
}

La mise en oeuvre

Code derrière JsonModelBinder (ou utilisez le complet paquet 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;
    }
}

Exemple de demande

Voici un exemple de demande http brute telle qu'acceptée par l'action du contrôleur Upload ci-dessus.

Une demande multipart/form-data est divisée en plusieurs parties, chacune étant séparée par le boundary=12345 spécifié. Un nom a été attribué à chaque partie dans son en-tête Content-Disposition-. Avec ces noms, default ASP.Net-Core sait quelle partie est liée à quel paramètre dans l'action du contrôleur.

Les fichiers liés à IFormFile doivent en outre spécifier une filename comme dans la deuxième partie de la demande. Content-Type n'est pas requis.

Une autre chose à noter est que les parties JSON doivent être désérialisables dans les types de paramètres tels que définis dans l'action du contrôleur. Donc, dans ce cas, le type SomeObject devrait avoir une propriété key de type 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--

Test avec Postman

Postman peut être utilisé pour appeler l'action et tester le code côté serveur. C'est assez simple et principalement basé sur l'interface utilisateur. Créez une nouvelle demande et sélectionnez form-data dans l'onglet Body. Vous pouvez maintenant choisir entre texte et fichier pour chaque partie du dossier.

enter image description here

18
Bruno Zell

Suite à l’excellente réponse de @ bruno-zell, si vous n’avez qu’un seul fichier (je n’ai pas testé avec un IList<IFormFile>), vous pouvez également déclarer votre contrôleur ainsi: 

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

Vous pouvez ensuite utiliser la méthode Postman indiquée dans la réponse de Bruno pour appeler votre contrôleur. 

4
Patrice Cote

Je ne suis pas sûr si vous pouvez faire les deux choses en une seule étape. 

Par le passé, je l'ai fait en téléchargeant le fichier via ajax et en renvoyant l'URL du fichier dans la réponse, puis en le transmettant avec la demande de publication pour enregistrer l'enregistrement réel.

0
Chirdeep Tomar

J'ai eu un problème similaire et j'ai résolu le problème en utilisant l'attribut [FromForm] et FileUploadModelView dans la fonction comme suit: 

[HttpPost("Save")]
public async Task<IActionResult> Save([FromForm] ProfileEditViewModel model)
{          
  return null;
}
0
waqar iftikhar

Je travaille avec Angular 7 sur le front-end, donc je me sers de la classe FormData, qui vous permet d’ajouter des chaînes ou des blobs à un formulaire. Ils peuvent être extraits du formulaire dans l'action du contrôleur à l'aide de l'attribut [FromForm]. J'ajoute le fichier à l'objet FormData, puis je stringie les données que je souhaite envoyer avec le fichier, je les ajoute à l'objet FormData et je désérialise la chaîne dans l'action de mon contrôleur.

Ainsi:

//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<SomeType>(jsonString);

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

Vous avez maintenant une poignée sur le fichier et l'objet. Notez que le nom que vous fournissez dans la liste des paramètres de votre action de contrôleur doit correspond au nom que vous avez fourni lors de l’ajout à l’objet FormData au niveau de l’interface frontale.

0
andreisrob