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?
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.
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());
});
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.
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.
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.
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;
}
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)
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ł.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