wake-up-neo.com

Laden Sie Dateien und JSON in ASP.NET Core Web API hoch

Wie kann ich eine Liste von Dateien (Bildern) und Json-Daten mit mehrteiligem Upload in den ASP.NET Core Web API-Controller hochladen?

Ich kann erfolgreich eine Liste von Dateien empfangen, die mit dem Inhaltstyp multipart/form-data wie folgt hochgeladen wurden:

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

Und natürlich kann ich erfolgreich einen HTTP-Anfragetext empfangen, der mit einem Standard-JSON-Formatierer wie folgt formatiert ist:

public void Post([FromBody]SomeObject value)

Aber wie kann ich diese beiden in einer einzigen Controller-Aktion kombinieren? Wie kann ich Bilder und JSON-Daten hochladen und an meine Objekte binden?

24
Andrius

Anscheinend gibt es keinen eingebauten Weg, um zu tun, was ich will. Also habe ich am Ende meine eigene ModelBinder geschrieben, um mit dieser Situation fertig zu werden. Ich habe keine offizielle Dokumentation zur Bindung von benutzerdefinierten Modellen gefunden, aber ich habe diesen Beitrag als Referenz verwendet.

Custom ModelBinder sucht nach Eigenschaften, die mit dem Attribut FromJson versehen sind, und deserialisiert die Zeichenfolge, die von einer mehrteiligen Anforderung an JSON stammt. Ich wickle mein Modell in eine andere Klasse (Wrapper) ein, die über Modell- und IFormFile-Eigenschaften verfügt. 

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

Verwendungszweck:

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

Einfacher, weniger Code, kein Wrapper-Modell

Es gibt eine einfachere Lösung, die stark von Andrius 'Antwort inspiriert ist. Durch die Nutzung Mit der ModelBinderAttribute müssen Sie keinen Modell- oder Bindungsanbieter angeben. Das spart viel Code. Ihre Controller-Aktion würde folgendermaßen aussehen:

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

Implementierung

Code hinter JsonModelBinder (oder verwenden Sie das vollständige NuGet-Paket ):

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

Beispielanfrage

Hier ist ein Beispiel einer unbearbeiteten http-Anfrage, die von der Controller-Aktion Upload oben akzeptiert wurde.

Eine multipart/form-data-Anforderung wird in mehrere Teile aufgeteilt, die jeweils durch den angegebenen boundary=12345 getrennt sind. Jedem Teil wurde in seinem Content-Disposition- Header ein Name zugewiesen. Bei diesen Namen weiß ASP.Net-Core standardmäßig, welcher Teil an welchen Parameter in der Controller-Aktion gebunden ist.

Dateien, die an IFormFile gebunden sind, müssen zusätzlich eine filename wie im zweiten Teil der Anforderung angeben. Content-Type ist nicht erforderlich.

Zu beachten ist auch, dass die Json-Teile in die in der Controller-Aktion definierten Parametertypen deserialisiert werden müssen. In diesem Fall sollte der Typ SomeObject eine Eigenschaft key vom Typ string haben.

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--

Testen mit dem Postboten

Postman kann verwendet werden, um die Aktion aufzurufen und den serverseitigen Code zu testen. Dies ist ziemlich einfach und wird hauptsächlich von der Benutzeroberfläche gesteuert. Erstellen Sie eine neue Anfrage und wählen Sie form-data im Tab Body aus. Jetzt können Sie für jeden Teil der Anforderung zwischen Text und Datei wählen.

 enter image description here

18
Bruno Zell

Nach der hervorragenden Antwort von @ bruno-zell können Sie, wenn Sie nur eine Datei haben (ich habe es nicht mit einem IList<IFormFile> getestet), Ihren Controller auch so definieren: 

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

Dann können Sie die in Brunos Antwort dargestellte Postman-Methode verwenden, um Ihren Controller anzurufen. 

4
Patrice Cote

Ich bin nicht sicher, ob Sie die beiden Dinge in einem einzigen Schritt ausführen können. 

Wie ich dies in der Vergangenheit erreicht habe, ist das Hochladen der Datei über ajax und das Zurückgeben der Datei-URL in der Antwort und das Weiterleiten mit der Post-Anforderung zum Speichern des aktuellen Datensatzes.

0
Chirdeep Tomar

Ich hatte ein ähnliches Problem und löste das Problem mithilfe von [FromForm]-Attribut und FileUploadModelView in der Funktion wie folgt: 

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

Ich arbeite mit Angular 7 am Front-End. Ich verwende daher die FormData-Klasse, mit der Sie Strings oder Blobs an ein Formular anhängen können. Sie können in der Controller-Aktion mit dem Attribut [FromForm] aus dem Formular gezogen werden. Ich füge die Datei dem FormData-Objekt hinzu, und dann füge ich die Daten zusammen, die ich zusammen mit der Datei senden möchte, hängen sie an das FormData-Objekt an und deserialisieren die Zeichenfolge in meiner Controller-Aktion.

So wie:

//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'
}

Sie haben jetzt einen Griff für die Datei und das Objekt. Beachten Sie, dass der Name, den Sie in der Parameterliste Ihres Controllers angeben, muss mit dem Namen übereinstimmen, den Sie beim Anhängen an das FormData-Objekt im Frontend angeben.

0
andreisrob

Ich wollte dasselbe mit Vue Frontend und .net Core API. Aber aus irgendeinem seltsamen Grund IFormFile gab immer null zurück. Also musste ich es in IFormCollection ändern und habe es geklärt. Hier ist der Code für alle, die das gleiche Problem haben :)

public async Task<IActionResult> Post([FromForm]IFormCollection files)
0