Сообщение Asp.net Core webApi (raw и multipart) [дубликат]

Селектор CSS « General Sibling Combinator » может быть использован для того, что вы хотите:

E ~ F {
    property: value;
}

Это соответствует любому элементу F, которому предшествует E ].

18
задан Andrius 28 December 2016 в 19:19
поделиться

4 ответа

По-видимому, нет встроенного способа делать то, что я хочу. Поэтому я решил написать свой собственный ModelBinder, чтобы справиться с этой ситуацией. Я не нашел официальной документации по привязке пользовательских моделей, но я использовал этот пост в качестве ссылки.

Пользовательский ModelBinder будет искать свойства, украшенные атрибутом FromJson и десериализуем строку, которая поступает из многопрофильного запроса в JSON. Я обертываю свою модель внутри другого класса (обертки), у которого есть свойства модели и 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;
    }
}

Использование:

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());
});
9
ответ дан Andrius 16 August 2018 в 00:48
поделиться
  • 1
    Какой InputFormatter следует использовать для получения данных в виде multipart / form-data? получение ошибки 500, если тип контента является multipart / form-data. – Asatur Galstyan 18 July 2017 в 07:18

Простой, меньше кода, нет модели-обертки

. Более простое решение, сильно вдохновленное ответом Andrius . Используя ModelBinderAttribute, вам не нужно указывать поставщика модели или связующего. Это экономит много кода. Действие вашего контроллера будет выглядеть так:

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

Реализация

Код за JsonModelBinder (или использовать полный пакет 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;
    }
}

Пример запроса

Ниже приведен пример необработанного HTTP-запроса, принятого действием контроллера Upload выше.

Запрос multipart/form-data разделен на несколько частей, каждая из которых разделена указанным boundary=12345. Каждая часть получила имя, назначенное в Content-Disposition -header. С этими именами по умолчанию ASP.Net-Core знает, какая часть связана с каким параметром в действии контроллера.

Файлы, привязанные к IFormFile, дополнительно должны указать filename, как во второй части запрос. Content-Type не требуется.

Еще одна вещь, которую следует отметить, состоит в том, что части json должны быть десериализуемыми в типы параметров, как определено в действии контроллера. Поэтому в этом случае тип SomeObject должен иметь свойство key типа 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--

Тестирование с помощью Postman

Почтальон может использоваться для вызова действия и проверки кода на стороне сервера. Это довольно просто и, в основном, пользовательский интерфейс. Создайте новый запрос и выберите данные формы в Body-Tab. Теперь вы можете выбрать между текстом и file для каждой части запроса.

5
ответ дан Bruno Zell 16 August 2018 в 00:48
поделиться
  • 1
    Отличное решение спасибо! Только вопрос, который у меня есть сейчас, - как я могу назвать маршрут загрузки из Postman для теста интеграции? Как представить IFormFile в JSON? – Patrice Cote 5 June 2018 в 15:31
  • 2
    @PatriceCote У меня есть ответы на обновления. Взгляни, пожалуйста :) – Bruno Zell 5 June 2018 в 17:42
  • 3
    Большое спасибо за то, что я искал. Но тогда, возможно, простой FromForm вместо FromBody сделал бы трюк, я думаю. – Patrice Cote 5 June 2018 в 22:20
  • 4
    @PatriceCote Ах приятно. Я больше не в этом, но я не думаю, что пробовал FromForm, но хаха ... – Bruno Zell 5 June 2018 в 22:22

Я не уверен, что вы можете сделать две вещи за один шаг.

Как я это достиг в прошлом, это загрузить файл через ajax и вернуть URL-адрес файла обратно в ответ, а затем передать его вместе с почтовым запросом, чтобы сохранить фактическую запись.

0
ответ дан Chirdeep Tomar 16 August 2018 в 00:48
поделиться
  • 1
    Да, это было бы возможно, но я пытаюсь избежать двух разных подключений к серверу для одной задачи, просто чтобы все синхронизировалось между клиентами и сервером. Я думаю, что нашел решение проблемы. Я отправлю его здесь, когда у меня будет больше времени. – Andrius 29 December 2016 в 12:36

После отличного ответа от @ bruno-zell, если у вас есть только один файл (я не тестировал с помощью IList<IFormFile>), вы также можете просто объявить свой контроллер следующим:

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

Затем вы можете использовать метод Postman, показанный в ответе Бруно, для вызова вашего контроллера.

3
ответ дан Patrice Cote 16 August 2018 в 00:48
поделиться
  • 1
    Это здорово, что выглядит код клиента C #, если вы пытаетесь выполнить вызов HttpClient.PostAsync для загрузки файла? – Mark Redman 2 July 2018 в 18:18
  • 2
    Я думаю, все, что вам нужно сделать, это позвонить ему так, как будто это синхронизация, и просто добавляет «ожидание». перед вызовом или ".Result" в конце – Patrice Cote 10 July 2018 в 19:05
Другие вопросы по тегам:

Похожие вопросы: