【问题标题】:Asp.Net Core comma separated array in query string binder查询字符串活页夹中的 Asp.Net Core 逗号分隔数组
【发布时间】:2021-11-17 10:36:23
【问题描述】:

我正在尝试实现自定义绑定器以允许在查询字符串中使用逗号分隔列表。基于this blog postofficial documentation 我已经创建了一些解决方案。但是,我不想使用属性来装饰想要的属性,我想让这种行为默认为所有简单类型的集合IList<T>, List<T>, T[], IEnumerable<T>... 其中Tint, string, short...)

但由于手动创建ArrayModelBinderProviderCollectionModelBinderProvider 并将bindingContext.ValueProvider 替换为CommaSeparatedQueryStringValueProvider,因此该解决方案看起来非常h​​acky,我相信应该有更好的方法来实现相同的目标。

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 && 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
            && 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 result;
        }

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

Startup.cs

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

【问题讨论】:

  • 为什么不对列表和数组使用普通的活页夹,而不是创建自定义活页夹?
  • 我正在将 NancyFx 迁移到 Asp.Net Core。 NancyFx 支持将查询字符串中的列表作为逗号分隔值传递:?list=val1,val2,val3,而 Asp.Net Core 仅支持这种表示形式:?list=val1&amp;list=val2&amp;list=val3。所以为了保持系统向后兼容,我需要支持旧样式。

标签: c# asp.net-core custom-model-binder


【解决方案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 的答案的代码。请参阅代码/博客 cmets 的这些答案。

启动

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 && modelType.GetArrayRank() == 1
                && modelType.HasElementType
                && 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 result;

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

【讨论】:

猜你喜欢
  • 2019-10-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-10-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多