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

时间:2019-04-24 20:10:45

标签: c# asp.net-core model

我们的请求模型随着API复杂性的增长而增长,因此我们决定使用复杂类型,而不是对操作参数使用简单类型。

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

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

这很好:

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

此变体不起作用

 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
 }

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

如您所见,代码正在检查标记为CommaSeparatedAttribute的参数...但是它不适用于复杂参数,例如第二个变量中使用的参数。

注意:我对上述文章中提供的原始代码进行了一些小的更改,例如使CommaSeparatedAttribute不仅可以用于参数,还可以用于属性,但仍然无法使用

2 个答案:

答案 0 :(得分:0)

原因

那是因为您试图检测存在 [CommaSeparated]属性的情况,该属性在参数上修饰(而不是参数的属性):

  

var commaSeparatedAttr = parameter.Attributes.OfType()。FirstOrDefault();

请注意,您的操作方法如下所示:

  

公共异步任务GetByIds(GetByIdsRequest请求)

换句话说,parameter.Attributes.OfType<CommaSeparatedAttribute>()仅会在request参数上修饰那些注释。但是,根本没有这样的[CommaSeparatedAttribute]

结果, SeparatedQueryStringAttribute过滤器从未添加到parameter.Action.Filters

如何修复

好像您在SeparatedQueryStringAttribute里度过了小两周。由于我们没有得到您的代码,请假设我们有一个SeparatedQueryStringAttribute过滤器(从您上面提到的博客复制):

public class SeparatedQueryStringAttribute : Attribute, IResourceFilter
{
    private readonly SeparatedQueryStringValueProviderFactory _factory;
    public SeparatedQueryStringAttribute() : this(",") { }

    public SeparatedQueryStringAttribute(string separator) {
        _factory = new SeparatedQueryStringValueProviderFactory(separator);
    }

    public SeparatedQueryStringAttribute(string key, string separator) {
        _factory = new SeparatedQueryStringValueProviderFactory(key, separator);
    }

    public void OnResourceExecuting(ResourceExecutingContext context) {
        context.ValueProviderFactories.Insert(0, _factory);
    }

    public void OnResourceExecuted(ResourceExecutedContext context) { }
}

实际上,根据您的GetByIdsRequest类,我们应该检测存在存在于参数属性上的 [CommaSeparated]属性

// CommaSeparatedQueryStringConvention::Apply(action) 
public void Apply(ActionModel action)
{
    for (int i = 0; i < action.Parameters.Count; i++)
    {
        var parameter = action.Parameters[i];
        var props = parameter.ParameterType.GetProperties()
            .Where(pi => pi.GetCustomAttributes<CommaSeparatedAttribute>().Count() > 0)
            ;
        if (props.Count() > 0)
        {
            var attribute = new SeparatedQueryStringAttribute(",");
            parameter.Action.Filters.Add(attribute);
            break;
        }
    }
}

现在对我来说很好。

演示

enter image description here

答案 1 :(得分:0)

基于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);
            }
        }
    }
 }

EvaluateProperties方法:

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

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

[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; }
}

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

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
}

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

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=all,none&extra-ids=12,34,45

enter image description here

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