Я пытаюсь получить необработанные пары «ключ-значение» для дополнительной логики обработки в дальнейшем.
У меня есть такой класс:
public class ValidationRequest<T>
where T : class
{
public string Form { get; set; } = string.Empty;
public required T Fields { get; set; }
public Dictionary<string, string> RawPostedKeyValues { get; set; } = [];
}
Если я опубликую действие с этой моделью в качестве входных данных:
[HttpPost]
public async Task<JsonResult> ValidateTestFormAsync([FromBody] ValidationRequest<PersonModel> personValidationRequest)
{
//do things here
}
Где POST может быть любая «обычная» модель, которую вы обычно передаете по почте в действие MVC.
Я хотел бы, чтобы свойство PersonModel включало все необработанные опубликованные значения ключей, которые соответствуют именам свойств любого поля типа (чтобы предотвратить злоупотребление перепостом), когда модель привязана, в дополнение к выполнению обычной привязки и проверки модели по умолчанию.
Я стараюсь не изобретать здесь весь велосипед и использовать как можно больше соглашений MVC по умолчанию.
Итак, если бы я опубликовал это:
{
"form": "CreatePerson",
"fields": {
"Id": null,
"FirstName": "Joe",
"LastName": "Bob",
"Age": null,
"LikesChocolate": false,
"Hobbies": []
}
}
И я просто выплеснул опубликованную модель обратно в формате JSON в ответ, это было бы так:
{
"form": "CreatePerson",
"fields": {
"Id": null,
"FirstName": "Joe",
"LastName": "Bob",
"Age": null,
"LikesChocolate": false
}
"rawPostedKeyValues:[
{ "key": "Fields.Id", "value": null },
{ "key": "Fields.FirstName", "value": "Joe"},
{ "key": "Fields.LastName", "value": "Bob"},
{ "key": "Fields.Age", "value": null},
{ "key": "Fields.LikeChocolate", "value": "false"},
]
}
В именах ключей используется соглашение об именах по умолчанию, основанное на ошибках проверки RawPostedKeyValues; Я хочу сохранить это.
Теперь кажется, что лучший способ справиться с этим — специальный конвертер JSON или связующее устройство модели. Я попробовал это, как можно увидеть в классе ModelState, но это не сработает на уровне свойства, поскольку в данных POST нет ключа, соответствующего имени свойства ValidationRequest, поэтому моя пользовательская логика никогда не вызывается.
Кажется, мне нужно будет использовать специальную привязку модели в классе верхнего уровня RawPostedKeyValues? Как мне прочитать тело для привязки этого свойства, в то же время позволяя выполнять привязку модели по умолчанию? Как мне прочитать тело запроса, чтобы заполнить эти свойства?
Обновлено:
Мне удалось использовать ответ ниже и объединить его с фабричным шаблоном, чтобы он заработал. Вам необходимо использовать шаблон фабрики из-за универсального типа, который не будет известен до времени выполнения.
Вот полный конвертер:
public class ValidationRequestJsonConverter : JsonConverterFactory
{
public override bool CanConvert(Type typeToConvert)
{
if (!typeToConvert.IsGenericType)
return false;
Type generic = typeToConvert.GetGenericTypeDefinition();
return (generic == typeof(ValidationRequest<>));
}
public override JsonConverter CreateConverter(Type type, JsonSerializerOptions options)
{
Type argumentType = type.GetGenericArguments()[0];
JsonConverter converter = (JsonConverter)Activator.CreateInstance(
typeof(ValidationRequestInnerJsonConverter<>).MakeGenericType(new Type[] { argumentType }),
BindingFlags.Instance | BindingFlags.Public,
binder: null,
args: [options],
culture: null)!;
return converter;
}
}
internal class ValidationRequestInnerJsonConverter<T> : JsonConverter<ValidationRequest<T>>
where T : class
{
//Required or will throw erro a runtime error
public ValidationRequestInnerJsonConverter(JsonSerializerOptions options) { }
public override ValidationRequest<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var jsonDocument = JsonDocument.ParseValue(ref reader);
var root = jsonDocument.RootElement;
var form = root.GetProperty("form").GetString();
var fieldsJson = root.GetProperty("fields").GetRawText();
var fieldObject = JsonSerializer.Deserialize<T>(fieldsJson, options) ?? Activator.CreateInstance<T>();
var rawPostedKeyValues = new Dictionary<string, string?>();
foreach (var property in root.GetProperty("fields").EnumerateObject())
{
rawPostedKeyValues[$"Fields.{property.Name}"] = property.Value.ToString();
}
return new ValidationRequest<T>
{
Form = form,
Fields = fieldObject,
RawPostedKeyValues = rawPostedKeyValues
};
}
public override void Write(Utf8JsonWriter writer, ValidationRequest<T> value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("form", value.Form);
writer.WritePropertyName("fields");
JsonSerializer.Serialize(writer, value.Fields, options);
writer.WritePropertyName("rawPostedKeyValues");
JsonSerializer.Serialize(writer, value.RawPostedKeyValues, options);
writer.WriteEndObject();
}
}





Кажется, мне нужно будет использовать специальную привязку модели на верхнем уровне. Класс ValidationRequest? Как бы я прочитал тело, чтобы связать его? это свойство, в то же время позволяя привязку модели по умолчанию к происходить? Как мне прочитать тело запроса, чтобы заполнить эти свойства?
Ну, в соответствии с вашим сценарием, вы можете использовать Пользовательские конвертеры JSON. Основная цель — заполнить объект ValidationRequest<T>, включая свойство Fields и словарь RawPostedKeyValues.
В классе jsonConverter сначала прочитайте необработанные данные json из запроса и извлеките свойство, используя root.GetProperty("form"), который предоставит нам значение свойства формы из JSON и сохранит его в строковой переменной.
Затем нам нужно извлечь и десериализовать свойство полей, которое мы получили ранее.
Наконец, запустите словарь , выполните итерацию по root.GetProperty("fields").EnumerateObject() и привяжите объект ValidationRequest.
Кроме того, мы будем использовать еще одно промежуточное программное обеспечение, поскольку по умолчанию конвейер ASP.NET Core считывает тело запроса как поток. После прочтения потока он не сбрасывается автоматически и не может использоваться повторно. Это означает, что если какая-либо часть конвейера промежуточного программного обеспечения считывает тело запроса (например, для регистрации или проверки), оно не может быть прочитано снова связывателями модели или методами действий, если только это не будет специально обработано.
Поэтому, чтобы решить вышеуказанную проблему, мы будем использовать промежуточное программное обеспечение , которое обеспечивает буферизацию тела запроса, позволяя считывать данные запроса несколько раз.
Давайте посмотрим на практике:
Модель:
public class PersonModel
{
public int? Id { get; set; }
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public int? Age { get; set; }
public bool LikesChocolate { get; set; }
public List<string> Hobbies { get; set; } = new List<string>();
}
Запросить валидатор:
[JsonConverter(typeof(ValidationRequestJsonConverter<PersonModel>))]
public class ValidationRequest<T> where T : class
{
public string Form { get; set; } = string.Empty;
public required T Fields { get; set; }
public Dictionary<string, string?> RawPostedKeyValues { get; set; } = new Dictionary<string, string?>();
}
Конвертер запросов Json:
public class ValidationRequestJsonConverter<T> : JsonConverter<ValidationRequest<T>>
where T : class
{
public override ValidationRequest<T> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var jsonDocument = JsonDocument.ParseValue(ref reader);
var root = jsonDocument.RootElement;
var form = root.GetProperty("form").GetString();
var fieldsJson = root.GetProperty("fields").GetRawText();
var fields = JsonSerializer.Deserialize<T>(fieldsJson, options) ?? Activator.CreateInstance<T>();
var rawPostedKeyValues = new Dictionary<string, string?>();
foreach (var property in root.GetProperty("fields").EnumerateObject())
{
rawPostedKeyValues[$"Fields.{property.Name}"] = property.Value.ToString();
}
return new ValidationRequest<T>
{
Form = form,
Fields = fields,
RawPostedKeyValues = rawPostedKeyValues
};
}
public override void Write(Utf8JsonWriter writer, ValidationRequest<T> value, JsonSerializerOptions options)
{
writer.WriteStartObject();
writer.WriteString("form", value.Form);
writer.WritePropertyName("fields");
JsonSerializer.Serialize(writer, value.Fields, options);
writer.WritePropertyName("rawPostedKeyValues");
JsonSerializer.Serialize(writer, value.RawPostedKeyValues, options);
writer.WriteEndObject();
}
}
Промежуточное программное обеспечение для конвейера тела множественного запроса:
public class RawDataCaptureMiddleware
{
private readonly RequestDelegate _next;
public RawDataCaptureMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
context.Request.EnableBuffering();
var requestBodyStream = new StreamReader(context.Request.Body);
var requestBodyText = await requestBodyStream.ReadToEndAsync();
context.Request.Body.Position = 0;
if (string.IsNullOrWhiteSpace(requestBodyText))
{
context.Items["RawRequestData"] = null;
}
else
{
try
{
var requestData = JsonDocument.Parse(requestBodyText);
context.Items["RawRequestData"] = requestData;
}
catch (JsonException ex)
{
context.Items["RawRequestData"] = null;
}
}
await _next(context);
}
}
Программа.cs:
app.UseAuthorization();
app.UseMiddleware<RawDataCaptureMiddleware>();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
Выход:
Примечание. Если вам нужен дополнительный пример, обратитесь к этому официальному документу. Вы также можете проверить привязку индивидуальной модели.
На самом деле необходимость EnableBuffering зависит от того, как осуществляется доступ и обработка тела запроса. Если тело запроса считывается один раз и полностью используется JsonConverter без необходимости доступа к нему где-либо еще, EnableBuffering не требуется. Если тело запроса необходимо снова прочитать другим компонентом или промежуточным программным обеспечением, EnableBuffering становится необходимым для разрешения многократного чтения.
В ValidationRequestJsonConverter все тело запроса JSON десериализуется непосредственно в объект ValidationRequest<T> в методе Read. Этот подход обходит привязку модели по умолчанию и напрямую управляет процессом десериализации. Да, вы правы. Используя ValidationRequestJsonConverter, вы по существу переопределяете привязку модели по умолчанию для типа ValidationRequest<T>.
Вы можете избавиться от RawDataCaptureMiddleware, если вам не нужно читать тело запроса несколько раз, это тоже сработает.
Есть проблема с этим решением. Я не могу позволить атрибуту связывания явно объявлять PersonModel. Это был всего лишь пример использования запроса на проверку. Предполагается, что объект ValidationRequest обертывает типичную модель (через свойство Fields), которую вы обычно передаете действию MVC. И кажется, что вы не можете использовать универсальный тип в атрибуте. То, как сейчас происходит объявление атрибута, полностью противоречит цели использования универсального атрибута в ValidationRequest, и вам придется создавать собственный класс для каждого типа T для полей.
Я обновил свой вопрос, указав предполагаемое использование.
Я понял, что мне нужно использовать фабричный шаблон для валидатора из открытого универсального шаблона. Я обновил свой ответ, включив в него полный код. Если вы хотите добавить заводской шаблон в свой ответ, я отмечу его как ответ. Я бы не смог этого сделать, если бы ты не направил меня на правильный путь.
Хорошо, понял, спасибо за ваш ответ и рад помочь вам в этом.
Ух ты, вот что я пытался понять! Я читал официальную документацию и обдумывал решение, но так и не смог его понять. Будет ли этому решению вообще нужно использовать EnableBuffering в запросе? Похоже, что оно полностью переопределяет преобразование/привязку по умолчанию, поэтому будет ли в этом сценарии request.body читаться дважды? Он сериализует весь класс, а не только словарь, как в моем изображенном решении, поэтому не нужно будет запускать связующее по умолчанию?