#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