ASP.NET Core:带有逗号分隔值列表的复杂模型

Emi*_*ioV 5 c# model asp.net-core

我们的请求模型随着 API 日益复杂的增长而不断增长,我们决定使用复杂类型而不是使用简单类型作为操作参数。

一种典型的类型是IEnumerable逗号分隔值,例如我们使用https://www.strathweb.com/2017/07/customizing-query-string-parameter-bindingitems=1,2,3,5...中提供的解决方法解决了从字符串转换为 IEnumerable 的问题-in-asp-net-core-mvc/其中关键点是实现IActionModelConvention接口来识别标有特定属性的参数[CommaSeparated]

一切工作正常,直到我们将简单参数移动到单个复杂参数中,现在我们无法检查实现中的复杂参数IActionModelConvention。使用时也会发生同样的情况IParameterModelConvention。请参阅下面的代码:

这工作正常:

 public async Task<IActionResult> GetByIds(
       [FromRoute]int day,
       [BindRequired][FromQuery][CommaSeparated]IEnumerable<int> ids,
       [FromQuery]string order)
 {
        // do something
 }
Run Code Online (Sandbox Code Playgroud)

虽然这个变体不起作用

 public class GetByIdsRequest
 {
    [FromRoute(Name = "day")]
    public int Day { get; set; }

    [BindRequired]
    [FromQuery(Name = "ids")]
    [CommaSeparated]
    public IEnumerable<int> Ids { get; set; }

    [FromQuery(Name = "order")]
    public string Order { get; set; }
 }

 public async Task<IActionResult> GetByIds(GetByIdsRequest request)
 {
        // do something
 }
Run Code Online (Sandbox Code Playgroud)

实现IActionModelConvention非常简单:

public void Apply(ActionModel action)
{
   SeparatedQueryStringAttribute attribute = null;
   for (int i = 0; i < action.Parameters.Count; i++)
   {
       var parameter = action.Parameters[i];
       var commaSeparatedAttr = parameter.Attributes.OfType<CommaSeparatedAttribute>().FirstOrDefault();
       if (commaSeparatedAttr != null)
       {
           if (attribute == null)
           {
                attribute = new SeparatedQueryStringAttribute(",", commaSeparatedAttr.RemoveDuplicatedValues);
                 parameter.Action.Filters.Add(attribute);
            }

            attribute.AddKey(parameter.ParameterName);
        }
    }
 } 
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,代码正在检查标有CommaSeparatedAttribute... 的参数,但它不适用于像我的第二个变体中使用的复杂参数。

注意:我对上面提到的帖子中提供的原始代码添加了一些细微的更改,例如CommaSeparatedAttribute不仅可以用于参数,还可以用于属性,但它仍然不起作用

Emi*_*ioV 5

根据 itminus 的回答,我可以制定出最终的解决方案。正如 itminus 指出的那样,窍门就在 IActionModelConvention 实现中。请参阅我的实现,它考虑了其他方面,例如嵌套模型以及分配给每个属性的真实名称:

public void Apply(ActionModel action)
{
    SeparatedQueryStringAttribute attribute = null;
    for (int i = 0; i < action.Parameters.Count; i++)
    {
        var parameter = action.Parameters[i];
        var commaSeparatedAttr = parameter.Attributes.OfType<CommaSeparatedAttribute>().FirstOrDefault();
        if (commaSeparatedAttr != null)
        {
            if (attribute == null)
            {
                attribute = new SeparatedQueryStringAttribute(",", commaSeparatedAttr.RemoveDuplicatedValues);
                parameter.Action.Filters.Add(attribute);
            }

            attribute.AddKey(parameter.ParameterName);
        }
        else
        {
            // here the trick to evaluate nested models
            var props = parameter.ParameterInfo.ParameterType.GetProperties();
            if (props.Length > 0)
            {
                // start the recursive call
                EvaluateProperties(parameter, attribute, props);
            }
        }
    }
 }
Run Code Online (Sandbox Code Playgroud)

评估属性方法:

private void EvaluateProperties(ParameterModel parameter, SeparatedQueryStringAttribute attribute, PropertyInfo[] properties)
{
    for (int i = 0; i < properties.Length; i++)
    {
        var prop = properties[i];
        var commaSeparatedAttr = prop.GetCustomAttributes(true).OfType<CommaSeparatedAttribute>().FirstOrDefault();
        if (commaSeparatedAttr != null)
        {
            if (attribute == null)
            {
                attribute = new SeparatedQueryStringAttribute(",", commaSeparatedAttr.RemoveDuplicatedValues);
                parameter.Action.Filters.Add(attribute);
            }

            // get the binding attribute that implements the model name provider
            var nameProvider = prop.GetCustomAttributes(true).OfType<IModelNameProvider>().FirstOrDefault(a => !IsNullOrWhiteSpace(a.Name));
            attribute.AddKey(nameProvider?.Name ?? prop.Name);
        }
        else
        {
            // nested properties
            var props = prop.PropertyType.GetProperties();
            if (props.Length > 0)
            {
               EvaluateProperties(parameter, attribute, props);
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我还更改了逗号分隔属性的定义

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter, Inherited = true, AllowMultiple = false)]
public class CommaSeparatedAttribute : Attribute
{
    public CommaSeparatedAttribute()
       : this(true)
    { }

    /// <summary>
    /// ctor
    /// </summary>
    /// <param name="removeDuplicatedValues">remove duplicated values</param>
    public CommaSeparatedAttribute(bool removeDuplicatedValues)
    {
        RemoveDuplicatedValues = removeDuplicatedValues;
    }

    /// <summary>
    /// remove duplicated values???
    /// </summary>
    public bool RemoveDuplicatedValues { get; set; }
}
Run Code Online (Sandbox Code Playgroud)

我也改变了其他一些活动部件......但这基本上是最重要的。现在,我们可以使用这样的模型:

public class GetByIdsRequest
{
    [FromRoute(Name = "day")]
    public int Day { get; set; }

    [BindRequired]
    [FromQuery(Name = "ids")]
    [CommaSeparated]
    public IEnumerable<int> Ids { get; set; }

    [FromQuery(Name = "include")]
    [CommaSeparated]
    public IEnumerable<IncludingOption> Include { get; set; }

    [FromQuery(Name = "order")]
    public string Order { get; set; }

    [BindProperty(Name = "")]
    public NestedModel NestedModel { get; set; }
}

public class NestedModel
{
    [FromQuery(Name = "extra-include")]
    [CommaSeparated]
    public IEnumerable<IncludingOption> ExtraInclude { get; set; }

    [FromQuery(Name = "extra-ids")]
    [CommaSeparated]
    public IEnumerable<long> ExtraIds { get; set; }
}

// the controller's action
public async Task<IActionResult> GetByIds(GetByIdsRequest request)
{
    // do something
}
Run Code Online (Sandbox Code Playgroud)

对于这样的请求(与上面定义的不完全相同,但非常相似):

http://.../vessels/algo/days/20190101/20190202/hours/1/2?page=2&size=12&filter=eq(a,b)&order=by(asc(a))&include=all,none&ids =12,34,45&extra-include=全部,无&extra-ids=12,34,45

在此输入图像描述

如果有人需要完整的代码,请告诉我。再次感谢 itminus 的宝贵帮助