Asp.Net Основной массив, разделенный запятыми, в связывателе строк запроса

#c# #asp.net-core #custom-model-binder

#c# #asp.net-ядро #custom-model-binder

Вопрос:

Я пытаюсь реализовать пользовательский связующий файл, чтобы разрешить список, разделенный запятыми, в строке запроса. На основе этого сообщения в блоге и официальной документации я создал некоторое решение. Но вместо использования атрибутов для украшения требуемых свойств я хочу сделать это поведение по умолчанию для всех коллекций простых типов ( IList<T>, List<T>, T[], IEnumerable<T> … где T int, string, short …)

Но это решение выглядит очень хакерским из-за ручного создания ArrayModelBinderProvider CollectionModelBinderProvider и замены bindingContext.ValueProvider , CommaSeparatedQueryStringValueProvider и я считаю, что должен быть лучший способ достичь той же цели.

 public class CommaSeparatedQueryBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context is null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var bindingSource = context.BindingInfo.BindingSource;

        if (bindingSource != null amp;amp; bindingSource != BindingSource.Query)
        {
            return null;
        }

        if (!context.Metadata.IsEnumerableType)
        {
            return null;
        }

        if (context.Metadata.ElementMetadata.IsComplexType)
        {
            return null;
        }

        IModelBinderProvider modelBinderProvider;

        if (context.Metadata.ModelType.IsArray)
        {
            modelBinderProvider = new ArrayModelBinderProvider();
        }
        else
        {
            modelBinderProvider = new CollectionModelBinderProvider();
        }

        var binder = modelBinderProvider.GetBinder(context);

        return new CommaSeparatedQueryBinder(binder);
    }
}

public class CommaSeparatedQueryBinder : IModelBinder
{
    private readonly IModelBinder _modelBinder;

    public CommaSeparatedQueryBinder(IModelBinder modelBinder)
    {
        _modelBinder = modelBinder;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var valueProviderLazy = new Lazy<CommaSeparatedQueryStringValueProvider>(() =>
            new CommaSeparatedQueryStringValueProvider(bindingContext.HttpContext.Request.Query));

        if (bindingContext.ValueProvider is CompositeValueProvider composite
            amp;amp; composite.Any(provider => provider is QueryStringValueProvider))
        {
            var queryStringValueProvider = composite.First(provider => provider is QueryStringValueProvider);

            var index = composite.IndexOf(queryStringValueProvider);

            composite.RemoveAt(index);

            composite.Insert(index, valueProviderLazy.Value);

            await _modelBinder.BindModelAsync(bindingContext);

            composite.RemoveAt(index);

            composite.Insert(index, queryStringValueProvider);
        }
        else if(bindingContext.ValueProvider is QueryStringValueProvider)
        {
            var originalValueProvider = bindingContext.ValueProvider;

            bindingContext.ValueProvider = valueProviderLazy.Value;

            await _modelBinder.BindModelAsync(bindingContext);

            bindingContext.ValueProvider = originalValueProvider;
        }
        else
        {
            await _modelBinder.BindModelAsync(bindingContext);
        }
    }
}

public class CommaSeparatedQueryStringValueProvider : QueryStringValueProvider
{
    private const string Separator = ",";

    public CommaSeparatedQueryStringValueProvider(IQueryCollection values)
        : base(BindingSource.Query, values, CultureInfo.InvariantCulture)
    {
    }

    public override ValueProviderResult GetValue(string key)
    {
        var result = base.GetValue(key);

        if (result == ValueProviderResult.None)
        {
            return resu<
        }

        if (result.Values.Any(x => x.IndexOf(Separator, StringComparison.OrdinalIgnoreCase) > 0))
        {
            var splitValues = new StringValues(result.Values
                .SelectMany(x => x.Split(Separator))
                .ToArray());

            return new ValueProviderResult(splitValues, result.Culture);
        }

        return resu<
    }
}
 

Startup.cs

 services.AddControllers(options =>
{
    options.ModelBinderProviders.Insert(0, new CommaSeparatedQueryBinderProvider());
})
 

Комментарии:

1. Почему бы не использовать обычную связку для списков и массивов вместо создания пользовательской связки?

2. Я переношу NancyFX на Asp.Net Ядро. NancyFX поддерживает передачу списка в строке запроса в виде значений, разделенных запятыми: ?list=val1,val2,val3 , в то время как Asp.Net Ядро поддерживает только это представление: ?list=val1amp;list=val2amp;list=val3 . Поэтому, чтобы сохранить обратную совместимость системы, мне нужно поддерживать старый стиль.

Ответ №1:

Я обнаружил, что это полезно, хотя оно привязывается только к массивам. Это код, который объединяет ответы из https://damieng.com/blog/2018/04/22/comma-separated-parameters-webapi / и https://raw.githubusercontent.com/sgjsakura/AspNetCore/master/Sakura .AspNetCore.Extensions/Sakura.AspNetCore.Mvc.TagHelpers/FlagsEnumModelBinderServiceCollectionExtensions.cs. Смотрите эти ответы для комментариев к коду / блогу.

Запуск

 services.AddMvc(options =>
{
    options.AddCommaSeparatedArrayModelBinderProvider();
})
 

Поставщик

 public class CommaSeparatedArrayModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context == null)
            throw new ArgumentNullException(nameof(context));

        return CommaSeparatedArrayModelBinder.IsSupportedModelType(context.Metadata.ModelType) ? new CommaSeparatedArrayModelBinder() : null;
    }
}
 

Связующее

 public class CommaSeparatedArrayModelBinder : IModelBinder
{
    private static Task CompletedTask => Task.CompletedTask;

    private static readonly Type[] supportedElementTypes = {
        typeof(int), typeof(long), typeof(short), typeof(byte),
        typeof(uint), typeof(ulong), typeof(ushort), typeof(Guid)
    };

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (!IsSupportedModelType(bindingContext.ModelType)) return CompletedTask;

        var providerValue = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);

        if (providerValue == ValueProviderResult.None) return CompletedTask;

        // Each value self may contains a series of actual values, split it with comma
        var strs = providerValue.Values.SelectMany(s => s.Split(',', StringSplitOptions.RemoveEmptyEntries)).ToList();

        if (!strs.Any() || strs.Any(s => String.IsNullOrWhiteSpace(s)))
            return CompletedTask;

        var elementType = bindingContext.ModelType.GetElementType();
        if (elementType == null) return CompletedTask;

        var realResult = CopyAndConvertArray(strs, elementType);

        bindingContext.Result = ModelBindingResult.Success(realResult);

        return CompletedTask;
    }

    internal static bool IsSupportedModelType(Type modelType)
    {
        return modelType.IsArray amp;amp; modelType.GetArrayRank() == 1
                amp;amp; modelType.HasElementType
                amp;amp; supportedElementTypes.Contains(modelType.GetElementType());
    }

    private static Array CopyAndConvertArray(IList<string> sourceArray, Type elementType)
    {
        var targetArray = Array.CreateInstance(elementType, sourceArray.Count);
        if (sourceArray.Count > 0)
        {
            var converter = TypeDescriptor.GetConverter(elementType);
            for (var i = 0; i < sourceArray.Count; i  )
                targetArray.SetValue(converter.ConvertFromString(sourceArray[i]), i);
        }
        return targetArray;
    }
}
 

Помощники

 public static class CommaSeparatedArrayModelBinderServiceCollectionExtensions
{
    private static int FirstIndexOfOrDefault<T>(this IEnumerable<T> source, Func<T, bool> predicate)
    {
        var result = 0;

        foreach (var item in source)
        {
            if (predicate(item))
                return resu<

            result  ;
        }

        return -1;
    }

    private static int FindModelBinderProviderInsertLocation(this IList<IModelBinderProvider> modelBinderProviders)
    {
        var index = modelBinderProviders.FirstIndexOfOrDefault(i => i is FloatingPointTypeModelBinderProvider);
        return index < 0 ? index : index   1;
    }

    public static void InsertCommaSeparatedArrayModelBinderProvider(this IList<IModelBinderProvider> modelBinderProviders)
    {
        // Argument Check
        if (modelBinderProviders == null)
            throw new ArgumentNullException(nameof(modelBinderProviders));

        var providerToInsert = new CommaSeparatedArrayModelBinderProvider();

        // Find the location of SimpleTypeModelBinder, the CommaSeparatedArrayModelBinder must be inserted before it.
        var index = modelBinderProviders.FindModelBinderProviderInsertLocation();

        if (index != -1)
            modelBinderProviders.Insert(index, providerToInsert);
        else
            modelBinderProviders.Add(providerToInsert);
    }

    public static MvcOptions AddCommaSeparatedArrayModelBinderProvider(this MvcOptions options)
    {
        if (options == null)
            throw new ArgumentNullException(nameof(options));

        options.ModelBinderProviders.InsertCommaSeparatedArrayModelBinderProvider();
        return options;
    }

    public static IMvcBuilder AddCommaSeparatedArrayModelBinderProvider(this IMvcBuilder builder)
    {
        builder.AddMvcOptions(options => AddCommaSeparatedArrayModelBinderProvider(options));
        return builder;
    }
}
 

Комментарии:

1. Отличная работа! Это сработало отлично. Я только что опубликовал это как суть одного файла: gist.github.com/copernicus365/74aff0b560b985f7d2b9c61a608c0a64