ASP.NET Core MVC:除了默认模型绑定之外,模型还将所有 POSTed 键值绑定到非名称匹配属性?

问题描述 投票:0回答:1

我正在尝试获取原始的

POST
ed 键值对,以便稍后在我的操作中获得额外的处理逻辑。

我有一堂这样的课:

public class ValidationRequest<T>
    where T : class
{
    public string Form { get; set; } = string.Empty;
    
    public required T Fields { get; set; }

    //[BindProperty(BinderType = typeof(MyPropertyBinder))]
    //[JsonConverter(typeof(MyCustomJsonConverter))]
    public Dictionary<string, string> RawPostedKeyValues { get; set; } = [];
}

如果我使用此模型作为输入发布操作:

[HttpPost]
public async Task<JsonResult> ValidateTestFormAsync([FromBody] ValidationRequest<PersonModel> personValidationRequest)
{
    //do things here
}

除了执行正常的默认模型绑定和验证之外,我还希望属性

RawPostedKeyValues
包含与模型绑定时任何类型字段的属性名称相匹配的所有原始发布键值(以防止过度发布滥用)。

我试图不在这里重新发明整个轮子,并尽可能多地使用默认的 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"},
  ]
}

键名称使用基于

ModelState
验证错误的默认命名约定;我想保留它。

现在看来处理这个问题的最佳方法是自定义 JSON 转换器或模型绑定器。我尝试了这个,如在

ValidationRequest
类中看到的那样,但这在属性级别不起作用,因为没有与 POST 数据中的属性名称
RawPostedKeyValues
匹配的键,因此我的自定义逻辑永远不会被调用。

看来我需要在顶级

ValidationRequest
类上使用自定义模型绑定器?我将如何读取主体来绑定该属性,同时仍然允许发生默认模型绑定?我将如何读取请求正文来填充这些属性?

asp.net-core asp.net-core-mvc model-binding custom-model-binder
1个回答
0
投票

看来我需要在顶层使用自定义模型绑定器 验证请求类?我将如何阅读正文以进行绑定 该属性同时仍然允许默认模型绑定 发生?我将如何读取请求正文来填充这些属性?

根据您的场景,您可以使用自定义 JSON 转换器。主要目标是填充

ValidationRequest<T>
对象,包括 Fields 属性和 RawPostedKeyValues 字典。

在 jsonConverter 类中,首先从请求中读取原始 json 数据,并使用

root.GetProperty("form")
提取属性,这将为我们提供 JSON 中表单属性的值并将其存储在字符串变量中。

然后我们必须提取并反序列化我们之前获得的 fields 属性。

最后,启动 Dictionary 并迭代

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

输出:

enter image description here

enter image description here

enter image description here

注意:如果您需要更多示例,请参考此官方文档。您也可以查看自定义模型绑定

© www.soinside.com 2019 - 2024. All rights reserved.