用于将带有查询字符串的复杂对象传递到Web API方法的自定义模型绑定器

目录

介绍  

查询复杂对象的字符串字段

使用和测试FieldValueModelBinder类

FieldValueModelBinder如何工作?

获取源字段和值 

将字段部分与对象属性匹配

解析枚举类型  

支持的集合类型

最大递归限制 

解决传递字符串列表或数组的问题

迁移到ASP.NET Core

摘要 


介绍  

具有字段值数据对的查询字符串是在URI或具有默认application/x-www-form-urlencoded内容类型的请求正文中传输消息的标准形式。当在URI或请求正文中使用查询字符串数据源时,最新的Web API 2ASP.NET MVC 5仅支持传递仅包含原始、非类或System.String类型属性的简单对象。对于任何包含嵌套对象或集合的复杂对象,唯一可用的选择是在请求正文中传递序列化的JSONXML数据。   

当我将现有的嵌套对象模型从WCF Web服务移植到Web API应用程序以进行搜索、分页和排序请求时,我想将此带有查询字符串的复杂对象传递给GET方法,但找不到任何可行的方法解。最后,我创建了自己的模型绑定器,该绑定器有效地用于通过URI或请求正文中的查询字符串传递复杂对象的所有实际模式。   

查询复杂对象的字符串字段

为了获得清晰的描述,我定义了这些术语并在本文中使用它们。   

  • 简单属性:具有原始、非类或System.String类型的任何属性。  
  • 复杂属性:具有类类型但不包含的任何System.String属性。
  • 简单对象:任何仅包含简单属性的对象。
  • 嵌套对象:包含一个或多个复杂属性但没有集合的任何对象。
  • 嵌套集合对象:具有一个或多个集合的任何嵌套对象。

对于嵌套对象,查询字符串的外观可以与简单对象的相同。字段名称不能使用父对象名称作为前缀,因为对象树中没有任何集合。即使没有来自不同对象的相同简单属性名称,模型绑定器也应该解析所有嵌套对象中的简单属性名称。

下面显示了用于搜索和分页请求的嵌套对象模型以及相应的查询字符串源数据的示例。这可能是Web应用程序最常用的方案之一。该结构还包括带有enum类型的嵌套对象。为了简化演示,我将CategoryId用作硬编码搜索字段。真正的搜索请求可以是另一个嵌套对象,其中包含一个枚举,该枚举SearchField包含其他项,例如CategoryNameProductNameProductStatus等,以及一个字符串SearchText属性。   

请求模型类的示例:    

public class NestSearchRequest
{
    public int CategoryId { get; set; }
    public PagingRequest PagingRequest { get; set; }        
}
public class PagingRequest
{        
    public int PageIndex { get; set; }
    public int PageSize { get; set; }
    public Sort Sort { get; set; }
}
public class Sort
{
    public string SortBy { get; set; }
    public SortDirection SortDirection { get; set; }  
}
public enum SortDirection
{
    Ascending,
    Descending
}

以上请求模型的查询字符串:

CategoryId=3&PageIndex=0&PageSize=8&SortBy=ProductName&SortDirection=Descending

对于嵌套集合对象,字段名称应以带有索引的复杂属性名称为前缀。我们也不想将JSONXML对象类结构嵌入任何值。而是,字段值对中每个字段名称的最后部分应始终指向简单属性。  

请求模型类以测试和演示嵌套集合对象:

public class ComplexSearchRequest
{
    public int CategoryId { get; set; }
    public List<PagingSortRequest> PagingRequest { get; set; }        
    public string Test { get; set; }
}  

public class PagingSortRequest
{
    public int PageIndex { get; set; }
    public int PageSize { get; set; }
    public Sort[] Sort { get; set; }               
}

以上请求模型的查询字符串数据:

CategoryId=3&PagingRequest[0]PageIndex=1&PagingRequest[0]PageSize=8&PagingRequest[0]Sort[0]SortBy=ProductName&PagingRequest[0]Sort[0]SortDirection=descending&PagingRequest[0]Sort[1]SortBy=CategoryID&PagingRequest[0]Sort[1]SortDirection=0&PagingRequest[1]PageIndex=2&PagingRequest[1]PageSize=5&PagingRequest[1]Sort[0]SortBy=CategoryID&PagingRequest[1]Sort[0]SortDirection=0&PagingRequest[1]Sort[1]SortBy=ProductName&PagingRequest[1]Sort[1]SortDirection=Descending&Test=OK

检索到模型绑定程序的字段名称对的列表如下所示: 

使用和测试FieldValueModelBinder

要查看自定义模型绑定器的运行情况,您需要下载代码并使用Visual Studio 20122013重新编译解决方案。请确保计算机上具有Internet连接,因为需要从NuGet自动下载所有程序包文件。要在其他项目中使用该FieldValueModelBinder类,可以在SM.General.Api项目中复制该类文件或使用程序集SM.General.Api.dll。在Web API控制器代码中,只需使用以下设置替换GETPUT方法中的[FromUri] [FromBody]属性: 

 [ModelBinder(typeof(SM.General.Api.FieldValueModelBinder))]

测试应用程序是本地IIS Express托管的Web API类库。当您运行测试应用程序以打开HTML页面时,在参数输入文本框中输入查询字符串,然后单击链接以将该字符串传递给API方法中的一个,模型绑定器会将查询字符串转换为基于对象树的对象。然后,对象将从响应中发回并显示在页面上。调用测试方法的代码很简单。  

jQuery代码:  

var input = $("#txaInput").val();
$.ajax({
    url: 'api/nvpstonestcollectionget?' + input,
    type: "GET",
    dataType: "json",    
    success: function (data) {
        //Display data on HTML page
        ... 
    },
    ...
});

AngularJS代码:

$scope.nvpsToNestCollectionGet = function () {
    $http({
       url: 'api/nvpstonestcollectionget?' + $scope.txaInput,
       method: "GET"
    }).
    success(function (data, status, headers, config) {
        //Display data on HTML page
        ...             
    }).
    error(function (data, status, headers, config) {
        ...
    });
}

服务器端API方法:

[Route("~/api/nvpstonestcollectionget")]
public ComplexSearchRequest Get_NvpsToNestCollection([ModelBinder(typeof(FieldValueModelBinder))] ComplexSearchRequest request)
{
    return request;
}

默认情况下,输入文本框中存在嵌套对象的测试数据字符串。可以通过单击加载默认测试输入字符串链接将嵌套集合对象的默认测试字符串加载到框中。输入字符串中的数据必须与为API输入参数设置的模型类型匹配。否则,仅将字段和属性名称匹配的数据填充到模型中。例如,如果对嵌套集合对象使用默认数据字符串,但是单击传递嵌套对象以获取链接,则将仅使用集合中的第一个对象项来获得模型,因为在模型类中定义的属性没有集合类型。 

下面是将查询字符串传递给API方法Get_NvpsToNestCollection()的演示屏幕截图: 

我们还可以在Visual Studio 2012/2013调试窗口中检查.NET模型对象的详细信息。

FieldValueModelBinder如何工作?

定制模型绑定器反序列化输入数据,并使用API​​方法参数中定义的类型填充对象。我们在代码中需要做的是在System.Web.Http.ModelBinding.IModelBinder中实现唯一的成员,BindModel方法。该方法接收数据反序列化所需的两种类型的类对象。  

  1. System.Web.Http.Controllers.HttpActionContext:包含所有源http数据信息。   
  2. System.Web.Http.ModelBinding.ModelBindingContext:包含所有目标对象模型信息。它还具有用于访问源数据的所有注册值提供程序的ValueProvider属性。   

这是FieldValueModelBinder类中的主要工作流程: 

  • 获取源字段值对字符串,并将数据转换为键值对项的工作列表。   
  • 遍历层次结构中对象的每个属性。  
  • 如果当前迭代的项目是一个复杂的属性,则递归地迭代其属性。
  • 如果complex属性是集合类型,请为源数据创建一个组工作列表。否则,对源数据使用单个工作列表。  
  • 遍历源数据的工作列表。
  • 如果源字段名称与属性名称匹配,请为简单或复杂属性设置值。  
  • 成功完成每次迭代后,从原始数据源列表中删除工作项,并刷新工作源数据列表。  
  • 最后,将顶级对象设置为目标模型并返回它。   

有关详细信息,请参见下载源中的代码和注释行。在以下各节中将进一步讨论一些特殊的内容。 

获取源字段和值 

FieldValueModelBinder类调用HttpActionContext以直接的获得源数据而无需使用值提供商,因为默认QueryStringValueProvider只得到从URI,而不是请求体中的数据。它也无法按递归迭代的要求处理集合。更重要的是,对于任何实际的迭代过程,我都需要使用有效的源数据列表(请参见下文)。尽管我可以创建自定义值提供程序,但使用我自己的值提供程序List<KeyValuePair<string, string>>更加灵活高效。

这是获取原始源数据的代码: 

//Define original source data list
List<KeyValuePair<string, string>> kvps;

//Check and get source data from uri 
if (!string.IsNullOrEmpty(actionContext.Request.RequestUri.Query))
{    
    kvps = actionContext.Request.GetQueryNameValuePairs().ToList(); 
}
//Check and get source data from body
else if (actionContext.Request.Content.IsFormData())
{                
    var bodyString = actionContext.Request.Content.ReadAsStringAsync().Result;
    kvps = ConvertToKVP(bodyString);
}
...

将为每个迭代过程创建kvps的工作副本。对于嵌套集合对象,还需要在集合中创建项目对象的列表: 

//Set KV Work List for each real iteration process
List<KeyValuePair<string, string>> kvpsWork = new List<KeyValuePair<string, string>>(kvps);

//KV Work For each object item in collection
List<KeyValueWork> kvwsGroup = new List<KeyValueWork>();            

//KV Work for collection
List<List<KeyValueWork>> kvwsGroups = new List<List<KeyValueWork>>();

使用工作源列表可以将原始源列表从迭代循环中释放出来,以便在项目成功完成后可以从原始列表中删除该项目,而只剩下未使用的项目用于其余流程。然后,在为下一个属性开始新的迭代之前,将从原始列表中刷新任何工作列表。  

kvps.Remove(item.SourceKvp); 

将字段部分与对象属性匹配

如前所述,我们可以将没有对象前缀的字段名称用于嵌套对象。模型绑定程序还处理此模式的两种情况。 

1、当层次结构中不同对象中存在相同的属性名称时,可以正确匹配项。使用刷新的工作源数据列表将使此功能受益。由于已删除任何有效的源字段/值对,因此候选列表中仅存在与下一个迭代属性匹配的未使用项。要对此进行测试,请将类SM.Store.Api.Sort中的SortDirection属性行更改为:

public int PageIndex { get; set; }

运行测试应用程序后,在源查询字符串输入框中将&SortDirection=Descending替换为&PageIndex=2,然后点击通行证嵌套对象来获得链接。结果如下所示。 

2、如果字段名称部分中存在任何父名称前缀,则忽略它们,例如PagingRequest[0]Sort[0]SortBy=ProductName。该代码使用正则表达式Split()函数仅获取字段名称的最后一部分。 

//Ignore any bracket in a name key 
var key = item.Key;
var keyParts = Regex.Split(key, @"\[\d*\]");
if (keyParts.Length > 1) key = keyParts[keyParts.Length - 1];

对于嵌套集合对象,正则表达式Match()方法用于提取最后一个父名称的括号和索引值。然后根据父括号将字段名称字符串分割为前缀的字段名称的最后一部分。   

//Get parts from current KV Work
regex = new Regex(parentProp.Name + @"\[([^}])\]");
match = regex.Match(item.Key);
var brackets = match.Value.Replace(parentProp.Name, "");
var objIdx = match.Groups[1].Value;

//Get parts array from Key
var keyParts = item.Key.Split(new string[] { brackets }, StringSplitOptions.RemoveEmptyEntries);

//Get last part from prefixed name
Key = keyParts[keyParts.Length - 1];

仅知道字段名称的最后一部分对于嵌套集合对象是不够的。如果没有正确的索引传递到子级别并在子级别进行检查,则字段值对将不会映射到正确的子对象属性。因此,pParentObjIndex参数中的父对象索引值将传递给递归方法以处理子对象。

RecurseNestedObj(tempObj, prop, pParentName: group[0].ParentName, pParentObjIndex: group[0].ObjIndex);

然后,用于处理子对象的方法将刷新工作源列表,该列表仅包括当前迭代的父索引值与传递的pParentObjIndex值匹配的项。 

//Get data only from parent-parent for linked child KV Work
if (pParentName != "" & pParentObjIndex != "")
{
    regex = new Regex(pParentName + RexSearchBracket);
    match = regex.Match(item.Key);
    if (match.Groups[1].Value != pParentObjIndex)
        break;
}

解析枚举类型  

使用关键字enum的枚举类型是一种特殊类型,由一个常量枚举器列表组成。具有enum类型的属性也是一个简单的属性,不需要递归过程。该代码首先搜索enum项目值。如果该值不匹配,则通过匹配enum索引位置来搜索默认int类型值。因此,输入数据适用于 索引位置的enum值文本或整数输入。该代码还使输入enum 值文本不区分大小写。 

if (prop.PropertyType.IsEnum)
{
    var enumValues = prop.PropertyType.GetEnumValues();
    object enumValue = null;
    bool isFound = false;
                
    //Try to match enum item name first
    for (int i = 0; i < enumValues.Length; i++)
    {                    
        if (item.Value.ToLower() == enumValues.GetValue(i).ToString().ToLower())
        {
            enumValue = enumValues.GetValue(i);
            isFound = true;
            break;
        }
    }
    //Try to match enum default underlying int value if not matched with enum item name
    if(!isFound)
    {
        for (int i = 0; i < enumValues.Length; i++)
        {
            if (item.Value == i.ToString())
            {
                enumValue = i;                            
                break;
            }
        }
    }                
    prop.SetValue(obj, enumValue, null);
}

支持的集合类型

NameValueModelBinder类支持泛型List<>System.Array类型。在测试示例中,可以使用以下任一形式定义模型中具有复杂类型PagingSortRequests的集合: 

1、直接声明泛型List<>类型

public List<PagingSortRequest> PagingRequest { get; set; }

2、声明一个继承List<>类型基础的类对象。

public PagingSortRequests PagingRequest { get; set; }

该类的代码: 

public class PagingSortRequests : List<PagingSortRequest> {}

3、声明对象的数组: 

public PagingSortRequest[] PagingRequest { get; set; };

当模型绑定器处理集合类型时,它需要动态实例化集合对象。对于数组类型,我们还需要在实例化数组之前知道元素个数。在我们的例子中,可以从工作组源列表的项目个数中获得个数信息。以下是代码行: 

//Initiate List or Array
IList listObj = null;
Array arrayObj = null;
if (parentProp.PropertyType.IsGenericType || parentProp.PropertyType.BaseType.IsGenericType)
{
    listObj = (IList)Activator.CreateInstance(parentProp.PropertyType);
}
else if (parentProp.PropertyType.IsArray)
{
    arrayObj = Array.CreateInstance(parentProp.PropertyType.GetElementType(), kvwsGroups.Count);
}

最大递归限制 

模型绑定程序在类级别将默认的最大递归限制设置为100

private int maxRecursionLimit = 100;

在模型联编程序中,对象树中的任何复杂属性都将其添加到递归计数器中。父对象下的任何嵌套集合都将使用一个递归,而与集合中的项目对象的数量无关,因为所有集合项都在同一PropertyInfo数组下处理并在一个递归中完成。但是,如果父对象是一个集合,则此父对象下的集合对象将根据父集合中的项目数进行多次递归。先前描述的嵌套收集对象的测试示例将具有三个递归个数,一个用于PagingRequest集合,两个用于Sort集合,因为PagingRequest集合中有两个item对象。 

您可以通过在调用项目的Web.configApp.config文件中设置项目来更改最大限制值。

<appSettings>
   <add key="MaxRecursionLimit" value="120"/> 
   . . .
</appSettings>

默认的最大递归限制设置通常可以满足常见应用程序的需求。增加限制数量并处理过多的嵌套对象或集合可能会耗尽机器内存并导致系统故障。另外,当将数据从URI传递到GET方法时,输入字符串的大小也会受到限制。因此,对于URI中的查询字符串,处理大量嵌套对象或集合是不可能且不合适的。

解决传递字符串列表或数组的问题

当将任何字符串列表或数组传递给Web API时,从原始文章下载的源代码将呈现未定义无参数的构造函数错误。原因是模型绑定器使用无参数构造函数为任何内容类型动态创建任何子级List<>Array对象,而System.String类没有无参数构造函数。为了解决该问题,由于字符串是已知的内容类型,因此模型绑定程序创建了一个临时的物理List<string>string[]对象,然后将字符串项目值直接添加到List<>Array对象。需要在顶级迭代和递归迭代中放置相似的代码段,以使传递的字符串列表或数组在根和/或嵌套级别中起作用。

//Check if List<string> or string[] type and assign string value directly to list or array item.    
if (prop.ToString().Contains("[System.String]") || prop.ToString().Contains("System.String[]"))
{
    var strList = new List<string>();
    foreach (var item in kvpsWork)
    {
        //Remove any brackets and enclosure from Key.
        var itemKey = Regex.Replace(item.Key, RexBrackets, "");
        if (itemKey == prop.Name)
        {
            strList.Add(item.Value);
            kvps.Remove(item);
        }
    }
    //Add list to parent property.                        
    if (prop.PropertyType.IsGenericType) prop.SetValue(obj, strList);
    else if (prop.PropertyType.IsArray) prop.SetValue(obj, strList.ToArray()); 
}

测试请求对象模型可以证明如下:

//For test of passing string list or array.
public class PagingSortRequest2
{
    public int PageIndex { get; set; }
    public int PageSize { get; set; }
    public string[] RootStrings { get; set; }
    public Sort2[] Sort2 { get; set; }
}
public class Sort2
{
    public string SortBy { get; set; }
    public SortDirection SortDirection { get; set; }
    public List<string> InStrings { get; set; }        
}

然后,测试输入字段值对参数应如下所示(显示为分割线):

PageIndex=1
&PageSize=8
&RootStrings[0]=OK
&RootStrings[1]=Yes
&RootStrings[2]=456
&Sort2[0]SortBy=ProductName
&Sort2[0]SortDirection=descending
&Sort2[0]InStrings[0]=Search
&Sort2[0]InStrings[1]=Find
&Sort2[1]SortBy=CategoryID
&Sort2[1]SortDirection=0
&Sort2[1]InStrings[0]=Here
&Sort2[1]InStrings[1]=Also

使用Get运行字符串列表或数组对象的测试项传递的结果如下所示:

请注意,List<>Array的内容类型只支持System.String,不支持基元类型。如果您尝试通过List<int>向请求模型传递某些内容,则不会呈现任何错误,但传递的值不正确。这也可以是固定的,但传递字符串列表或数组足以满足目标需求。如果需要的话,可以将List<>Array中任何其他内容类型的字符串内容类型传递给Web API,然后在那里转换类型。

迁移到ASP.NET Core

这篇文章附有FieldValueModelBinder源代码文件的ASP.NET Core 2.0版本。在ASP.NET Core中,IModelBinder接口类型来自 Microsoft.AspNetCore.Mvc.ModelBinding名称空间,而在ASP.NET Web API 2.0中,它是.NET System.Web.Http.ModelBinding的成员。这是一项重大更改,因为HttpContext现在它是通过Kestrel Web服务器由一组请求功能组成的,这破坏了与以前版本的兼容性。您可以通过更改命名空间将FieldValueModelBinder.cs文件添加到ASP.NET Core 2.0项目中,然后在方法参数中使用相同的属性类型。 

在我的其他文章ASP.NET Core:从ASP.NET Web API迁移来的多层数据服务应用程序中,也有详细的描述,模型绑定器测试用例,甚至是整个示例应用程序。您可以在那里下载带有测试用例文件TestCasesForModelBinder.txt的源代码,并在完整结构的ASP.NET Core数据服务应用程序中运行这些测试用例。 

摘要 

本文介绍的自定义FieldValueModelBinder类可以有效地用于将带有查询字符串的复杂对象传递给Web API方法。特别是对于将查询字符串作为嵌套对象的GET方法而言,使用起来非常简单。对于嵌套集合对象,在将查询字符串源用于任何GETPOSTPUT方法时,该FieldValueModelBinder类提供一个选项。

发布了69 篇原创文章 · 获赞 146 · 访问量 49万+

猜你喜欢

转载自blog.csdn.net/mzl87/article/details/105006491