You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.NET Core中间件中HttpContext.Request.Form复杂类型模型绑定问题

问题

我有一个用于审计HttpRequest请求参数的自定义中间件,需要支持FromBody、FromForm、FromQuery三种参数获取方式,其中FromBody和FromQuery已正常工作,但FromForm的反序列化存在问题。

如何将传入的表单参数反序列化为端点对应的模型类型?即如何模拟默认模型绑定器的行为?

当前实现思路是:通过HttpContext.Request.Query和HttpContext.Request.Form构建Dictionary<string, object>,将其序列化为JSON字符串后再反序列化为目标模型类型,代码示例如下:

var requestParameters = new Dictionary<string, object>();
foreach(var formVariable in httpContext.Request.Form)
{
   requestParameters.Add(formVariable.Key, formVariable.Value.ToString());
}
var json = JsonConvert.SerializeObject(requestParameters);
var myModel = JsonConvert.DeserializeObject(json, typeof(MyModel));

对应的模型定义:

public class MyModel
{
   public int Id {get; set;}
   public string Name {get; set;}
   public List<Item> ListOfItems {get; set;}
}

public class Item
{
   public int ItemId {get; set;}
}

该方式对原始类型有效,但无法正确反序列化复杂类型。提交的参数格式如下:

"Id" = 1
"Name" = SomeName
"ListOfItems[0].ItemId" = 1 
"ListOfItems[1].ItemId" = 2

最后两个参数无法正确反序列化,且无法修改请求提交方式(如改为JSON提交),仅用于审计,不做功能变更,请问有哪些解决方案?


解决方案

方法一:复用ASP.NET Core内置模型绑定器

直接使用框架原生的模型绑定逻辑,完美匹配默认行为,无需自行实现复杂解析:

using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System.Globalization;

// 中间件构造函数注入IModelMetadataProvider
private readonly IModelMetadataProvider _modelMetadataProvider;

public AuditMiddleware(RequestDelegate next, IModelMetadataProvider modelMetadataProvider)
{
    _next = next;
    _modelMetadataProvider = modelMetadataProvider;
}

public async Task InvokeAsync(HttpContext context)
{
    // 从端点元数据中动态获取目标模型类型(示例:假设是控制器动作参数)
    var endpoint = context.GetEndpoint();
    var actionDescriptor = endpoint?.Metadata.GetMetadata<ControllerActionDescriptor>();
    var modelType = actionDescriptor?.Parameters.First(p => p.BindingInfo?.BindingSource == BindingSource.Form).ParameterType;
    if (modelType == null)
    {
        await _next(context);
        return;
    }

    var modelMetadata = _modelMetadataProvider.GetMetadataForType(modelType);
    // 创建绑定上下文
    var bindingContext = DefaultModelBindingContext.CreateBindingContext(
        context,
        new FormValueProvider(BindingSource.Form, context.Request.Form, CultureInfo.CurrentCulture),
        modelMetadata,
        bindingInfo: null,
        modelName: string.Empty
    );

    // 执行绑定
    var binder = new ComplexTypeModelBinder(_modelMetadataProvider);
    await binder.BindModelAsync(bindingContext);

    // 获取绑定后的模型用于审计
    if (bindingContext.Result.IsModelSet)
    {
        var boundModel = bindingContext.Result.Model;
        // 此处执行审计逻辑
    }

    await _next(context);
}

这种方式完全复用框架的绑定逻辑,支持所有表单参数格式(包括嵌套对象、集合、字典等),是最推荐的方案。

方法二:手动解析表单复合格式

如果不想依赖内置服务,可手动解析ListOfItems[0].ItemId这类格式的键,构建嵌套对象和集合:

public static object DeserializeFormToModel(IFormCollection form, Type modelType)
{
    var model = Activator.CreateInstance(modelType);
    foreach (var kvp in form)
    {
        SetNestedProperty(model, kvp.Key, kvp.Value.ToString());
    }
    return model;
}

private static void SetNestedProperty(object target, string propertyPath, string value)
{
    var pathParts = propertyPath.Split(new[] { '.', '[', ']' }, StringSplitOptions.RemoveEmptyEntries);
    object currentTarget = target;
    Type currentType = target.GetType();

    for (int i = 0; i < pathParts.Length; i++)
    {
        var part = pathParts[i];
        // 处理集合索引(如"0"对应ListOfItems[0])
        if (int.TryParse(part, out int index))
        {
            var collectionProperty = currentType.GetProperty(pathParts[i-1]);
            var collection = collectionProperty.GetValue(currentTarget) as IList;
            if (collection == null)
            {
                collection = (IList)Activator.CreateInstance(collectionProperty.PropertyType);
                collectionProperty.SetValue(currentTarget, collection);
            }
            var elementType = collectionProperty.PropertyType.GetGenericArguments()[0];
            // 补充集合元素至指定索引
            while (collection.Count <= index)
            {
                collection.Add(Activator.CreateInstance(elementType));
            }
            currentTarget = collection[index];
            currentType = elementType;
            continue;
        }
        // 处理普通属性
        var property = currentType.GetProperty(part);
        if (property == null) break;
        if (i == pathParts.Length - 1)
        {
            // 转换值类型并赋值
            var convertedValue = Convert.ChangeType(value, property.PropertyType, CultureInfo.CurrentCulture);
            property.SetValue(currentTarget, convertedValue);
        }
        else
        {
            // 初始化嵌套对象
            var nestedObj = property.GetValue(currentTarget) ?? Activator.CreateInstance(property.PropertyType);
            property.SetValue(currentTarget, nestedObj);
            currentTarget = nestedObj;
            currentType = property.PropertyType;
        }
    }
}

使用方式:

var myModel = (MyModel)DeserializeFormToModel(httpContext.Request.Form, typeof(MyModel));

该方法适合简单模型场景,复杂类型(如多层嵌套集合、字典)需要补充更多逻辑。


内容的提问来源于stack exchange,提问作者Jeremy

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 22:34:55