Upload plików i JSON w ASP.NET Core Web API

Jak mogę wgrać listę plików (obrazów) i danych json do ASP.NET Core Web API controller przy użyciu multipart upload?

Mogę z powodzeniem otrzymać listę plików, przesłanych z multipart/form-data typu content:

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

I oczywiście mogę z powodzeniem otrzymać ciało żądania HTTP sformatowane do mojego obiektu przy użyciu domyślnego formatera JSON w ten sposób:

public void Post([FromBody]SomeObject value)

Ale jak Mogę połączyć te dwa w jedną akcję kontrolera? Jak mogę przesłać oba obrazy i JSON danych i mają być powiązane z moimi obiektami?

Author: Andrius, 2016-12-28

8 answers

Prosty, mniej kodu, brak modelu wrappera

Istnieje prostsze rozwiązanie, mocno inspirowane odpowiedzią Andriusa. Za pomocą ModelBinderAttribute nie musisz podawać modelu ani dostawcy segregatora. To oszczędza dużo kodu. Twoja akcja kontrolera będzie wyglądać tak:

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

Realizacja

Kod ZA JsonModelBinder (patrz GitHub lub użyj pakietu 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;
    }
}

Przykładowe zapytanie

Oto przykład surowego żądania http jako akceptowane przez akcję kontrolera Upload powyżej.

A multipart/form-data żądanie jest podzielone na wiele części, z których każda oddzielona jest określonym boundary=12345. Każda część ma przypisaną nazwę w nagłówku Content-Disposition. Z tymi nazwami default ASP.Net-Core wie, która część jest związana z jakim parametrem w akcji kontrolera.

Pliki, które są powiązane z IFormFile dodatkowo muszą podać filename jak w drugiej części żądania. Content-Type nie jest wymagane.

Kolejną rzeczą do odnotowania jest to, że części json musi być deserializowalna do typów parametrów zdefiniowanych w akcji kontrolera. Tak więc w tym przypadku Typ SomeObject powinien mieć właściwość key typu 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--

Testy z listonoszem

Postman może być użyty do wywołania akcji i przetestowania kodu po stronie serwera. Jest to dość proste i głównie oparte na interfejsie użytkownika. Utwórz nowe żądanie i wybierz form-data w zakładce Body -. Teraz możesz wybrać pomiędzy tekstem a plikiem dla każda część wymagania.

Tutaj wpisz opis obrazka

 70
Author: Bruno Zell,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-11-26 08:15:37

Najwyraźniej nie ma sposobu, aby zrobić to, co chcę. Więc skończyłem pisząc własne ModelBinder, aby poradzić sobie z tą sytuacją. Nie znalazłem żadnej oficjalnej dokumentacji na temat custom model binding, ale użyłem tego postu jako odniesienia.

Custom ModelBinder wyszukuje właściwości ozdobione atrybutem FromJson i deserializuje ciąg znaków, który pochodzi z żądania wieloczęściowego do JSON. Zawijam mój model w inną klasę (wrapper), która ma właściwości model i IFormFile.

IJsonAttribute.cs:

public interface IJsonAttribute
{
    object TryConvert(string modelValue, Type targertType, out bool success);
}

od Jsonattribute.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;
    }
}

Użycie:

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());
});
 20
Author: Andrius,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-01-04 20:12:35

Pracuję z Angular 7 na front-endzie, więc korzystam z klasy FormData, która pozwala na dodawanie ciągów lub blobów do formularza. Można je wyciągnąć z formularza w akcji kontrolera za pomocą atrybutu [FromForm]. Dodaję plik do obiektu FormData, a następnie dodaję ciągi danych, które chcę wysłać razem z plikiem, dołączam go do obiektu FormData i deserializuję łańcuch w mojej akcji kontrolera.

Like so:

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

Masz teraz uchwyt na pliku i obiekt. Zauważ, że nazwa podana na liście params akcji kontrolera musi odpowiadać nazwie podanej podczas dodawania do obiektu FormData w interfejsie.

 14
Author: andreisrob,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-02-28 19:25:35

Po doskonałej odpowiedzi @bruno-zell, jeśli masz tylko jeden plik (nie testowałem z IList<IFormFile>), Możesz również zadeklarować swój kontroler jako taki:

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

Następnie możesz użyć metody listonosza pokazanej w odpowiedzi Bruno, aby wywołać kontroler.

 9
Author: Patrice Cote,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-06 14:05:33

Nie jestem pewien, czy można zrobić dwie rzeczy w jednym kroku.

Jak udało mi się to osiągnąć w przeszłości, to Przesyłanie pliku przez ajax i zwracanie adresu URL pliku z powrotem w odpowiedzi, a następnie przekazywanie go wraz z żądaniem post, aby zapisać rzeczywisty rekord.

 0
Author: Chirdeep Tomar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-12-29 02:38:53

Miałem podobny problem i rozwiązałem problem używając atrybutu [FromForm] i FileUploadModelView w funkcji w następujący sposób:

[HttpPost("Save")]
public async Task<IActionResult> Save([FromForm] ProfileEditViewModel model)
{          
  return null;
}
 0
Author: waqar iftikhar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-01-03 06:31:19

Chciałem zrobić to samo używając vue frontend i. Net Core api. Ale z jakiegoś dziwnego powodu IFormFile zawsze zwracało null. Więc musiałem zmienić go na IFormCollection i załatwiłem to. Oto kod dla każdego, kto ma do czynienia z tym samym problemem:)

public async Task<IActionResult> Post([FromForm]IFormCollection files)
 0
Author: Rukshan Dangalla,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2019-03-06 01:24:40

Miałem podobny problem przy wysyłaniu z angular do asp Core api.

Chrome: Dane Formularza

------WebKitFormBoundarydowgB6BX0wiwKeOk
Content-Disposition: form-data; name="file1"

undefined
------WebKitFormBoundarydowgB6BX0wiwKeOk
Content-Disposition: form-data; name="file2"

undefined
------WebKitFormBoundarydowgB6BX0wiwKeOk
Content-Disposition: form-data; name="reportData"; filename="blob"
Content-Type: application/json

{"id":2,"report":3,"code":"XX0013","business":"01","name":"Test","description":"Description"}
------WebKitFormBoundarydowgB6BX0wiwKeOk--

Oto Jak to robię:

Używam reportData jako przesłanych danych Pliku, a następnie odczytuję zawartość pliku.

[HttpPost]
public async Task<IActionResult> Set([FromForm] IFormFile file1, [FromForm] IFormFile file2, [FromForm] IFormFile reportData)
{
    try
    {
        ReportFormModel.Result result = default;

        if (reportData != null)
        {
            string reportJson = await reportData.ReadFormFileAsync();
            ReportFormModel.Params reportParams = reportJson.JsonToObject<ReportFormModel.Params>();

            if (reportParams != null)
            {
                //OK
            }
        }
        return Ok(result);
    }
    catch (Exception ex)
    {
        return BadRequest();
    }
}


public static class Utilities
{
    public static async Task<string> ReadFormFileAsync(this IFormFile file)
    {
        if (file == null || file.Length == 0)
        {
            return await Task.FromResult((string)null);
        }

        using var reader = new StreamReader(file.OpenReadStream());
        return await reader.ReadToEndAsync();
    }
}
Chociaż ten sposób nie jest doceniany, ale zadziałał.
 0
Author: user1076940,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2020-06-16 16:27:16