ASP.NET Core: сложная модель со списком значений через запятую - PullRequest
0 голосов
/ 24 апреля 2019

Наши модели запросов растут в соответствии с растущей сложностью наших API, и мы решили использовать сложные типы вместо простых типов для параметров действий.

Один типичный тип - IEnumerable для значений, разделенных запятыми, например items=1,2,3,5..., и мы решили проблему преобразования из строки в IEnumerable, используя обходной путь, предоставленный в https://www.strathweb.com/2017/07/customizing-query-string-parameter-binding-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
 }

пока этот вариант не работает

 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 голосов
/ 25 апреля 2019

Исходя из ответа 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 страница = 2 & размер = 12 & фильтр = Eq (а, б) и порядок = (по возрастанию (а) ) и включают в себя = все, ни один & DS = 12,34,45 и экстра-включают = все, ни один и дополнительные идентификаторы = 12,34,45

enter image description here

Если кому-то нужен полный код, пожалуйста, дайте мне знать. Еще раз спасибо itminus за его ценную помощь

0 голосов
/ 25 апреля 2019

Причина

Это потому, что вы пытаетесь обнаружить существование атрибутов [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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...