.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
相关产品推荐
相关产品推荐

