Контекст :
У меня есть угловое приложение, которое отправляет сложные данные в мой .net Core API.Отправляемыми данными могут быть изображение (ImageDto) или пара изображений (StackImageDto), а также заголовок и теги.Мы отправляем данные, используя многокомпонентную HTML-форму. Это означает, что данные не отформатированы в формате JSON.
Ошибка :
В настоящее время в .Net Core 2.2 имеется ошибка с BindModelAsync : https://github.com/aspnet/AspNetCore/issues/6616
Похожие проблемы :
Я пытаюсь следовать предложенным людям, но мне не удается заставить его работать правильно:
Код :
Поскольку мне нужно, чтобы API работал, я использую другой способ установки своего абстрактного класса DTO:в контроллере.
Вот классы DTO, MediaDto :
[ModelBinder(BinderType = typeof(AbstractModelBinder))]
public abstract class MediaDto
{
public Guid? Id { get; set; }
[Required]
public string Title { get; set; }
public string Note { get; set; }
public TagDto[] Tags { get; set; } = new TagDto[] { };
public ChallengeDetailsDto[] Challenges { get; set; }
[Required]
public string Discriminator { get; set; }
public DateTimeOffset? CreatedAt { get; set; }
public string CreatedBy { get; set; }
//public Guid CreatedByUserId { get; set; }
public DateTimeOffset? EditedAt { get; set; }
public string EditedBy { get; set; }
public DateTimeOffset? DeletedAt { get; set; }
public string DeletedBy { get; set; }
}
ImageDto :
[Serializable]
public class ImageDto : MediaDto
{
public IFormFile Image { get; set; }
public string Url { get; set; }
public ImageDto()
{
Discriminator = ImageDtoDiscriminator;
}
}
StackImageDto:
[Serializable]
public class StackImageDto : MediaDto
{
[Required]
[EnumDataType(typeof(StackImageType))]
public StackImageType Type { get; set; }
[Required]
public IEnumerable<StackFileDto> StackFiles { get; set; }
public StackImageDto()
{
Discriminator = StackImageDtoDiscriminator;
}
}
StackFileDto
[Serializable]
public class StackFileDto
{
public Guid? Id { get; set; }
public IFormFile Image { get; set; }
public string Url { get; set; }
public int Position { get; set; }
}
MediaLibraryConverter преобразовать Json в мой экземпляр MediaDto:
public class MediaLibraryConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(MediaDto).IsAssignableFrom(objectType);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
MediaDto result = null;
var jsonChallange = JObject.Load(reader);
var discriminator = jsonChallange.Properties().First(p => p.Name == "discriminator").Value.ToString();
if (discriminator.Equals(nameof(ImageDto), StringComparison.InvariantCultureIgnoreCase))
{
result = (ImageDto)JsonConvert.DeserializeObject(jsonChallange.ToString(), typeof(ImageDto));
}
else if (discriminator.Equals(nameof(StackImageDto), StringComparison.InvariantCultureIgnoreCase))
{
result = (StackImageDto)JsonConvert.DeserializeObject(jsonChallange.ToString(), typeof(StackImageDto));
}
else
{
throw new ApiException($"{nameof(MediaDto)} discriminator is not set properly.", HttpStatusCode.BadRequest);
}
return result;
}
public override bool CanWrite
{
get { return false; }
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
AbstractModelBinder выполняет привязку контекста, поскольку я получаю данные с использованием атрибута [FromForm] в действии контроллера:
public class AbstractModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));
// Fetch the value of the argument by name and set it to the model state
string fieldName = bindingContext.FieldName;
try
{
var data = ExtractDataFromContext(bindingContext)[0];
var jsonData = JsonConvert.SerializeObject(data);
bindingContext.ModelState.SetModelValue(fieldName, new ValueProviderResult(jsonData));
var discriminator = JObject.Parse(jsonData)
.Properties()
.First(p => p.Name == "discriminator")
.Value
.ToString();
// Deserialize the provided value and set the binding result
if (discriminator.Equals(nameof(ImageDto), StringComparison.InvariantCultureIgnoreCase))
{
object o = JsonConvert.DeserializeObject<ImageDto>(jsonData, new MediaLibraryConverter());
o = BindFormFiles(o, bindingContext);
bindingContext.Result = ModelBindingResult.Success(o);
}
else if (discriminator.Equals(nameof(StackImageDto), StringComparison.InvariantCultureIgnoreCase))
{
object o = JsonConvert.DeserializeObject<StackImageDto>(jsonData, new MediaLibraryConverter());
o = BindFormFiles(o, bindingContext);
bindingContext.Result = ModelBindingResult.Success(o);
}
}
catch (JsonException e)
{
bindingContext.Result = ModelBindingResult.Failed();
}
return Task.CompletedTask;
}
BindFormFiles isрекурсивная функция, помогающая связать json и IFormFile, получаемый из HttpContext, с экземпляром MediaDto
private object BindFormFiles(object tmp, ModelBindingContext bindingContext, string fieldName = null)
{
if (tmp == null)
{
return tmp;
}
var type = tmp.GetType();
if (type.IsPrimitive || typeof(string).IsAssignableFrom(type))
{
return tmp;
}
if (typeof(IEnumerable).IsAssignableFrom(type) && !typeof(IEnumerable<IFormFile>).IsAssignableFrom(type))
{
var array = (IList)tmp;
for (int i = 0; i < array.Count; i++)
{
array[i] = BindFormFiles(array[i], bindingContext, $"{fieldName}[{i}]");
}
}
else
{
foreach (var property in tmp.GetType().GetProperties())
{
var name = property.Name;
if (typeof(IFormFile).IsAssignableFrom(property.PropertyType))
{
var file = bindingContext.HttpContext.Request.Form.Files
.FirstOrDefault(f => string.IsNullOrEmpty(fieldName)
? f.Name.Equals($"{property.Name}", StringComparison.OrdinalIgnoreCase)
: f.Name.Equals($"{fieldName}.{property.Name}", StringComparison.OrdinalIgnoreCase)
);
if (file != null)
{
property.SetValue(tmp, file);
}
}
else if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
{
if (property.PropertyType == typeof(IEnumerable<IFormFile>))
{
var files = bindingContext.HttpContext.Request.Form.Files
.Where(f => string.IsNullOrEmpty(fieldName)
? f.Name.Equals($"{property.Name}", StringComparison.OrdinalIgnoreCase)
: f.Name.Equals($"{fieldName}.{property.Name}", StringComparison.OrdinalIgnoreCase)
);
if (files?.Any() == true)
{
property.SetValue(tmp, files);
}
}
else
{
property.SetValue(
tmp,
BindFormFiles(
property.GetValue(tmp),
bindingContext,
string.IsNullOrEmpty(fieldName)
? property.Name
: $"{fieldName}.{property.Name}"
)
);
}
}
else if (property.PropertyType.IsClass && !typeof(string).IsAssignableFrom(property.PropertyType))
{
property.SetValue(
tmp,
BindFormFiles(
property.GetValue(tmp),
bindingContext,
property.Name
)
);
}
}
}
return tmp;
}
ExtractDataFromContext - это рекурсивная функция, извлекающая данные из HttpContext
private List<Dictionary<string, object>> ExtractDataFromContext(ModelBindingContext bindingContext, string fieldName = null)
{
var i = 0;
var propertyName = string.IsNullOrEmpty(fieldName)
? string.Empty
: $"{fieldName}[{i}]";
var result = new List<Dictionary<string, object>>();
while (bindingContext.ActionContext.HttpContext.Request.Form
.Where(kv => string.IsNullOrEmpty(propertyName)
? true
: kv.Key.StartsWith(propertyName, StringComparison.OrdinalIgnoreCase)
)
.DistinctBy(kv => kv.Key.Substring(0, propertyName.Length))
.Any()
)
{
var values = bindingContext.ActionContext.HttpContext.Request.Form
.Where(kv => string.IsNullOrEmpty(propertyName)
? true
: kv.Key.StartsWith($"{fieldName}[{i}]", StringComparison.OrdinalIgnoreCase)
)
.ToDictionary(
kv => string.IsNullOrEmpty(propertyName)
? kv.Key
: kv.Key.Replace($"{propertyName}.", string.Empty, StringComparison.OrdinalIgnoreCase),
kv => (object)kv.Value[0]
);
var subValues = values
.Where(kv => kv.Key.Contains("]."))
.GroupBy(kv => kv.Key.Substring(0, kv.Key.IndexOf("[")))
.ToDictionary(
g => g.Key,
g => string.IsNullOrEmpty(propertyName)
? (object)ExtractDataFromContext(bindingContext, $"{g.Key}")
: (object)ExtractDataFromContext(bindingContext, $"{propertyName}.{g.Key}")
)
.Concat(values
.Where(kv => kv.Key.Contains("]") && !kv.Key.Contains("]."))
.GroupBy(kv => kv.Key.Substring(0, kv.Key.IndexOf("[")))
.ToDictionary(
g => g.Key,
g => (object)g.Select(kv => kv.Value)
)
);
result.Add(values
.Where(kv => !kv.Key.Contains("]"))
.Concat(subValues)
.ToList()
.ToDictionary(
kv => kv.Key,
kv => kv.Value
)
);
i++;
propertyName = $"{fieldName}[{i}]";
}
return result;
}
Для тех, кому интересно узнать, что содержит HttpContext.Request.Form:
Файлы :
Ключи :
ValidateModelStateAttribute является атрибутом ActionFilterAttribute, проверяющим current ModelState для всех действий контроллера
[AttributeUsage(AttributeTargets.All, AllowMultiple = false)]
public class ValidateModelStateAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
if (!context.ModelState.IsValid)
{
context.Result = new BadRequestObjectResult(context.ModelState);
}
}
}
Как видите, ModelState недействителен не из-за ошибок, а из-за того, что ValidationState не проверен.
Если я обойду ActionFilter и перейду в действие моего контроллера, я увижу Dto, правильно заполненный:
Вопрос:
Как мне сказать context.ModelState , что его ValidationState действителен?