Селектор CSS « General Sibling Combinator » может быть использован для того, что вы хотите:
E ~ F {
property: value;
}
Это соответствует любому элементу F
, которому предшествует E
].
По-видимому, нет встроенного способа делать то, что я хочу. Поэтому я решил написать свой собственный 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());
});
. Более простое решение, сильно вдохновленное ответом 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--
Почтальон может использоваться для вызова действия и проверки кода на стороне сервера. Это довольно просто и, в основном, пользовательский интерфейс. Создайте новый запрос и выберите данные формы в Body-Tab. Теперь вы можете выбрать между текстом и file для каждой части запроса.
FromForm
, но хаха ...
– Bruno Zell
5 June 2018 в 22:22
Я не уверен, что вы можете сделать две вещи за один шаг.
Как я это достиг в прошлом, это загрузить файл через ajax и вернуть URL-адрес файла обратно в ответ, а затем передать его вместе с почтовым запросом, чтобы сохранить фактическую запись.
После отличного ответа от @ 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, показанный в ответе Бруно, для вызова вашего контроллера.