From bca16de8d41d18e281407fac4e03d6cebbf4b3e0 Mon Sep 17 00:00:00 2001 From: Bill Boga Date: Sat, 19 Jan 2019 18:45:45 -0500 Subject: [PATCH] Add Header ValueProvider New behavior mimics existing ASP.NET MVC behavior of QueryString and Form value providers. Not really sure why this isn't included in that package. Didn't do thorough testing, but did notice one gotcha if identical header-keys are submitted: the values are combined as a single comma-separated string. Implements https://github.com/billbogaiv/hybrid-model-binding/issues/23 --- .../DefaultHybridModelBinder.cs | 6 +- .../DefaultPassthroughHybridModelBinder.cs | 6 +- .../HybridModelBinding.csproj | 2 +- .../ModelBinding/HeaderValueProvider.cs | 82 +++++++++++++++++++ .../HeaderValueProviderFactory.cs | 49 +++++++++++ src/HybridModelBinding/Source.cs | 1 + 6 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 src/HybridModelBinding/ModelBinding/HeaderValueProvider.cs create mode 100644 src/HybridModelBinding/ModelBinding/HeaderValueProviderFactory.cs diff --git a/src/HybridModelBinding/DefaultHybridModelBinder.cs b/src/HybridModelBinding/DefaultHybridModelBinder.cs index 97b0de3..4e30d62 100755 --- a/src/HybridModelBinding/DefaultHybridModelBinder.cs +++ b/src/HybridModelBinding/DefaultHybridModelBinder.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc.Formatters; +using HybridModelBinding.ModelBinding; +using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using System.Collections.Generic; @@ -23,7 +24,8 @@ public DefaultHybridModelBinder( .AddModelBinder(Body, new BodyModelBinder(formatters, readerFactory)) .AddValueProviderFactory(Form, new FormValueProviderFactory()) .AddValueProviderFactory(Route, new RouteValueProviderFactory()) - .AddValueProviderFactory(QueryString, new QueryStringValueProviderFactory()); + .AddValueProviderFactory(QueryString, new QueryStringValueProviderFactory()) + .AddValueProviderFactory(Header, new HeaderValueProviderFactory()); } } } diff --git a/src/HybridModelBinding/DefaultPassthroughHybridModelBinder.cs b/src/HybridModelBinding/DefaultPassthroughHybridModelBinder.cs index 05cee3f..3b36722 100644 --- a/src/HybridModelBinding/DefaultPassthroughHybridModelBinder.cs +++ b/src/HybridModelBinding/DefaultPassthroughHybridModelBinder.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc.Formatters; +using HybridModelBinding.ModelBinding; +using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using System.Collections.Generic; @@ -23,7 +24,8 @@ public DefaultPassthroughHybridModelBinder( .AddModelBinder(Body, new BodyModelBinder(formatters, readerFactory)) .AddValueProviderFactory(Form, new FormValueProviderFactory()) .AddValueProviderFactory(Route, new RouteValueProviderFactory()) - .AddValueProviderFactory(QueryString, new QueryStringValueProviderFactory()); + .AddValueProviderFactory(QueryString, new QueryStringValueProviderFactory()) + .AddValueProviderFactory(Header, new HeaderValueProviderFactory()); } } } diff --git a/src/HybridModelBinding/HybridModelBinding.csproj b/src/HybridModelBinding/HybridModelBinding.csproj index 2015462..5cd1069 100644 --- a/src/HybridModelBinding/HybridModelBinding.csproj +++ b/src/HybridModelBinding/HybridModelBinding.csproj @@ -12,7 +12,7 @@ false false true - 0.12.0 + 0.13.0 diff --git a/src/HybridModelBinding/ModelBinding/HeaderValueProvider.cs b/src/HybridModelBinding/ModelBinding/HeaderValueProvider.cs new file mode 100644 index 0000000..6d32935 --- /dev/null +++ b/src/HybridModelBinding/ModelBinding/HeaderValueProvider.cs @@ -0,0 +1,82 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc.Internal; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System; +using System.Collections.Generic; +using System.Globalization; + +namespace HybridModelBinding.ModelBinding +{ + /// + /// Modified from https://github.com/aspnet/Mvc/blob/8d66f104f7f2ca42ee8b21f75b0e2b3e1abe2e00/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/QueryStringValueProvider.cs. + /// + public class HeaderValueProvider : BindingSourceValueProvider, IEnumerableValueProvider + { + public HeaderValueProvider( + BindingSource bindingSource, + IHeaderDictionary values, + CultureInfo culture) + : base(bindingSource) + { + if (bindingSource == null) + { + throw new ArgumentNullException(nameof(bindingSource)); + } + + this.values = values ?? throw new ArgumentNullException(nameof(values)); + Culture = culture; + } + + public CultureInfo Culture { get; private set; } + + private readonly IHeaderDictionary values; + private PrefixContainer prefixContainer; + + protected PrefixContainer PrefixContainer + { + get + { + if (prefixContainer == null) + { + prefixContainer = new PrefixContainer(values.Keys); + } + + return prefixContainer; + } + } + + public override bool ContainsPrefix(string prefix) + { + return PrefixContainer.ContainsPrefix(prefix); + } + + public virtual IDictionary GetKeysFromPrefix(string prefix) + { + if (prefix == null) + { + throw new ArgumentNullException(nameof(prefix)); + } + + return PrefixContainer.GetKeysFromPrefix(prefix); + } + + public override ValueProviderResult GetValue(string key) + { + if (key == null) + { + throw new ArgumentNullException(nameof(key)); + } + + var values = this.values[key]; + + if (values.Count == 0) + { + return ValueProviderResult.None; + } + else + { + return new ValueProviderResult(values, Culture); + } + } + } +} diff --git a/src/HybridModelBinding/ModelBinding/HeaderValueProviderFactory.cs b/src/HybridModelBinding/ModelBinding/HeaderValueProviderFactory.cs new file mode 100644 index 0000000..2f807ad --- /dev/null +++ b/src/HybridModelBinding/ModelBinding/HeaderValueProviderFactory.cs @@ -0,0 +1,49 @@ +using Microsoft.AspNetCore.Mvc.ModelBinding; +using System; +using System.Globalization; +using System.Threading.Tasks; + +namespace HybridModelBinding.ModelBinding +{ + public class HeaderValueProviderFactory : IValueProviderFactory + { + /// + /// We need to re-create a Header BindingSource since the default ASP.NET MVC version + /// is greedy and that won't work as a ValueProvider. + /// + /// Ref. https://github.com/aspnet/Mvc/blob/8d66f104f7f2ca42ee8b21f75b0e2b3e1abe2e00/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/BindingSource.cs + /// Ref. https://github.com/aspnet/Mvc/blob/8d66f104f7f2ca42ee8b21f75b0e2b3e1abe2e00/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/BindingSourceValueProvider.cs#L41 + /// ArgumentException: The provided binding source 'Header' is a greedy data source. 'BindingSourceValueProvider' does not support greedy data sources. /// Parameter name: bindingSource + /// + private BindingSource Header = new BindingSource( + BindingSource.Header.Id, + BindingSource.Header.DisplayName, + isGreedy: false, + isFromRequest: BindingSource.Header.IsFromRequest); + + public Task CreateValueProviderAsync(ValueProviderFactoryContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var headers = context.ActionContext.HttpContext.Request.Headers; + if (headers != null && headers.Count > 0) + { + var valueProvider = new HeaderValueProvider( + Header, + headers, + CultureInfo.InvariantCulture); + + context.ValueProviders.Add(valueProvider); + } + +#if NET451 + return Task.FromResult(0); +#else + return Task.CompletedTask; +#endif + } + } +} diff --git a/src/HybridModelBinding/Source.cs b/src/HybridModelBinding/Source.cs index bd8cc3f..ecb7da6 100755 --- a/src/HybridModelBinding/Source.cs +++ b/src/HybridModelBinding/Source.cs @@ -4,6 +4,7 @@ public static class Source { public const string Body = nameof(Body); public const string Form = nameof(Form); + public const string Header = nameof(Header); public const string QueryString = nameof(QueryString); public const string Route = nameof(Route); }