Skip to content
This repository has been archived by the owner on Dec 14, 2018. It is now read-only.

Commit

Permalink
[Fixes #5859] Consider allowing binding header values to types other …
Browse files Browse the repository at this point in the history
…than string and string collections
  • Loading branch information
kichalla committed Jan 30, 2018
1 parent 19c89db commit 0215740
Show file tree
Hide file tree
Showing 14 changed files with 1,067 additions and 163 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,19 @@ public abstract class ModelBinderProviderContext
/// <returns>An <see cref="IModelBinder"/>.</returns>
public abstract IModelBinder CreateBinder(ModelMetadata metadata);

/// <summary>
/// Creates an <see cref="IModelBinder"/> for the given <paramref name="metadata"/>
/// and <paramref name="bindingInfo"/>.
/// </summary>
/// <param name="metadata">The <see cref="ModelMetadata"/> for the model.</param>
/// <param name="bindingInfo">The <see cref="BindingInfo"/> that should be used
/// for creating the binder.</param>
/// <returns>An <see cref="IModelBinder"/>.</returns>
public virtual IModelBinder CreateBinder(ModelMetadata metadata, BindingInfo bindingInfo)
{
throw new NotSupportedException();
}

/// <summary>
/// Gets the <see cref="BindingInfo"/>.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal class MvcOptionsConfigureCompatibilityOptions : ConfigureCompatibilityO
if (Version >= CompatibilityVersion.Version_2_1)
{
values[nameof(MvcOptions.AllowCombiningAuthorizeFilters)] = true;
values[nameof(MvcOptions.AllowBindingHeaderValuesToNonStringModelTypes)] = true;
values[nameof(MvcOptions.InputFormatterExceptionPolicy)] = InputFormatterExceptionPolicy.MalformedInputExceptions;
values[nameof(MvcOptions.SuppressBindingUndefinedValueToEnumType)] = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ internal static class MvcCoreLoggerExtensions
private static readonly Action<ILogger, Type, Exception> _cannotBindToComplexType;
private static readonly Action<ILogger, string, Type, Exception> _cannotBindToFilesCollectionDueToUnsupportedContentType;
private static readonly Action<ILogger, Type, Exception> _cannotCreateHeaderModelBinder;
private static readonly Action<ILogger, Type, Exception> _cannotCreateHeaderModelBinderCompatVersion_2_0;
private static readonly Action<ILogger, Exception> _noFilesFoundInRequest;
private static readonly Action<ILogger, string, string, Exception> _noNonIndexBasedFormatFoundForCollection;
private static readonly Action<ILogger, string, string, string, string, string, string, Exception> _attemptingToBindCollectionUsingIndices;
Expand Down Expand Up @@ -490,7 +491,7 @@ static MvcCoreLoggerExtensions()
_cannotCreateHeaderModelBinder = LoggerMessage.Define<Type>(
LogLevel.Debug,
20,
"Could not create a binder for type '{ModelType}' as this binder only supports 'System.String' type or a collection of 'System.String'.");
"Could not create a binder for type '{ModelType}' as this binder only supports simple types (like string, int, bool, enum) or a collection of simple types.");

_noFilesFoundInRequest = LoggerMessage.Define(
LogLevel.Debug,
Expand Down Expand Up @@ -597,6 +598,11 @@ static MvcCoreLoggerExtensions()
LogLevel.Debug,
42,
"Done attempting to validate the bound property '{PropertyContainerType}.{PropertyName}' of type '{ModelType}'.");

_cannotCreateHeaderModelBinderCompatVersion_2_0 = LoggerMessage.Define<Type>(
LogLevel.Debug,
43,
"Could not create a binder for type '{ModelType}' as this binder only supports 'System.String' type or a collection of 'System.String'.");
}

public static void RegisteredOutputFormatters(this ILogger logger, IEnumerable<IOutputFormatter> outputFormatters)
Expand Down Expand Up @@ -1169,6 +1175,11 @@ public static void CannotBindToFilesCollectionDueToUnsupportedContentType(this I
_cannotBindToFilesCollectionDueToUnsupportedContentType(logger, bindingContext.ModelName, bindingContext.ModelType, null);
}

public static void CannotCreateHeaderModelBinderCompatVersion_2_0(this ILogger logger, Type modelType)
{
_cannotCreateHeaderModelBinderCompatVersion_2_0(logger, modelType, null);
}

public static void CannotCreateHeaderModelBinder(this ILogger logger, Type modelType)
{
_cannotCreateHeaderModelBinder(logger, modelType, null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Diagnostics;
using System.Globalization;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Internal;
Expand All @@ -21,11 +23,11 @@ public class HeaderModelBinder : IModelBinder

/// <summary>
/// <para>This constructor is obsolete and will be removed in a future version. The recommended alternative
/// is the overload that takes an <see cref="ILoggerFactory"/>.</para>
/// is the overload that takes an <see cref="ILoggerFactory"/> and an <see cref="IModelBinder"/>.</para>
/// <para>Initializes a new instance of <see cref="HeaderModelBinder"/>.</para>
/// </summary>
[Obsolete("This constructor is obsolete and will be removed in a future version. The recommended alternative"
+ " is the overload that takes an " + nameof(ILoggerFactory) + ".")]
+ " is the overload that takes an " + nameof(ILoggerFactory) + " and an " + nameof(IModelBinder) + ".")]
public HeaderModelBinder()
: this(NullLoggerFactory.Instance)
{
Expand All @@ -36,35 +38,117 @@ public HeaderModelBinder()
/// </summary>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public HeaderModelBinder(ILoggerFactory loggerFactory)
{
_logger = loggerFactory.CreateLogger<HeaderModelBinder>();
}

/// <summary>
/// Initializes a new instance of <see cref="HeaderModelBinder"/>.
/// </summary>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="innerModelBinder">The <see cref="IModelBinder"/> which does the actual
/// binding of values.</param>
public HeaderModelBinder(ILoggerFactory loggerFactory, IModelBinder innerModelBinder)
{
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}

if (innerModelBinder == null)
{
throw new ArgumentNullException(nameof(innerModelBinder));
}

_logger = loggerFactory.CreateLogger<HeaderModelBinder>();
InnerModelBinder = innerModelBinder;
}

// to enable unit testing
internal IModelBinder InnerModelBinder { get; }

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

var request = bindingContext.HttpContext.Request;
_logger.AttemptingToBindModel(bindingContext);

// Property name can be null if the model metadata represents a type (rather than a property or parameter).
var headerName = bindingContext.FieldName;

_logger.AttemptingToBindModel(bindingContext);

// Do not set ModelBindingResult to Failed on not finding the value in the header as we want the inner
// modelbinder to do that. This would give a chance to the inner binder to add more useful information.
// For example, SimpleTypeModelBinder adds a model error when binding to let's say an integer and the
// model is null.
var request = bindingContext.HttpContext.Request;
if (!request.Headers.ContainsKey(headerName))
{
_logger.FoundNoValueInRequest(bindingContext);
}

if (InnerModelBinder == null)
{
BindWithoutInnerBinder(bindingContext);
return;
}

var headerValueProvider = GetHeaderValueProvider(headerName, bindingContext);

// Capture the top level object here as entering nested scope would make it 'false'.
var isTopLevelObject = bindingContext.IsTopLevelObject;

// Create a new binding scope in order to supply the HeaderValueProvider so that the binders like
// SimpleTypeModelBinder can find values from header.
ModelBindingResult result;
using (bindingContext.EnterNestedScope(
bindingContext.ModelMetadata,
fieldName: bindingContext.FieldName,
modelName: bindingContext.ModelName,
model: bindingContext.Model))
{
bindingContext.IsTopLevelObject = isTopLevelObject;
bindingContext.ValueProvider = headerValueProvider;

await InnerModelBinder.BindModelAsync(bindingContext);
result = bindingContext.Result;
}

bindingContext.Result = result;

_logger.DoneAttemptingToBindModel(bindingContext);
}

private HeaderValueProvider GetHeaderValueProvider(string headerName, ModelBindingContext bindingContext)
{
var request = bindingContext.HttpContext.Request;

// Prevent breaking existing users in scenarios where they are binding to a 'string' property
// and expect the whole comma separated string, if any, as a single string and not as a string array.
var values = Array.Empty<string>();
if (request.Headers.ContainsKey(headerName))
{
if (bindingContext.ModelMetadata.IsEnumerableType)
{
values = request.Headers.GetCommaSeparatedValues(headerName);
}
else
{
values = new[] { (string)request.Headers[headerName] };
}
}

return new HeaderValueProvider(values);
}

private void BindWithoutInnerBinder(ModelBindingContext bindingContext)
{
var headerName = bindingContext.FieldName;
var request = bindingContext.HttpContext.Request;

object model;
if (bindingContext.ModelType == typeof(string))
{
Expand Down Expand Up @@ -101,7 +185,6 @@ public Task BindModelAsync(ModelBindingContext bindingContext)
}

_logger.DoneAttemptingToBindModel(bindingContext);
return Task.CompletedTask;
}

private static object GetCompatibleCollection(ModelBindingContext bindingContext, string[] values)
Expand All @@ -126,5 +209,34 @@ private static object GetCompatibleCollection(ModelBindingContext bindingContext

return collection;
}

private class HeaderValueProvider : IValueProvider
{
private readonly string[] _values;

public HeaderValueProvider(string[] values)
{
Debug.Assert(values != null);

_values = values;
}

public bool ContainsPrefix(string prefix)
{
return _values.Length != 0;
}

public ValueProviderResult GetValue(string key)
{
if (_values.Length == 0)
{
return ValueProviderResult.None;
}
else
{
return new ValueProviderResult(_values, CultureInfo.InvariantCulture);
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
Expand All @@ -21,26 +22,65 @@ public IModelBinder GetBinder(ModelBinderProviderContext context)
throw new ArgumentNullException(nameof(context));
}

if (context.BindingInfo.BindingSource != null &&
context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header))
var bindingInfo = context.BindingInfo;
if (bindingInfo.BindingSource == null ||
!bindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Header))
{
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<HeaderModelBinderProvider>();
return null;
}

var modelMetadata = context.Metadata;
var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger<HeaderModelBinderProvider>();

// We only support strings and collections of strings. Some cases can fail
// at runtime due to collections we can't modify.
if (context.Metadata.ModelType == typeof(string) ||
context.Metadata.ElementType == typeof(string))
var options = context.Services.GetRequiredService<IOptions<MvcOptions>>().Value;
if (!options.AllowBindingHeaderValuesToNonStringModelTypes)
{
if (modelMetadata.ModelType == typeof(string) ||
modelMetadata.ElementType == typeof(string))
{
return new HeaderModelBinder(loggerFactory);
}
else
{
logger.CannotCreateHeaderModelBinder(context.Metadata.ModelType);
logger.CannotCreateHeaderModelBinderCompatVersion_2_0(modelMetadata.ModelType);
}

return null;
}

return null;

if (!IsSimpleType(modelMetadata))
{
logger.CannotCreateHeaderModelBinder(modelMetadata.ModelType);
return null;
}

// Since we are delegating the binding of the current model type to other binders, modify the
// binding source of the current model type to a non-FromHeader binding source in order to avoid an
// infinite recursion into this binder provider.
var nestedBindingInfo = new BindingInfo(bindingInfo)
{
BindingSource = BindingSource.ModelBinding
};

var innerModelBinder = context.CreateBinder(
modelMetadata.GetMetadataForType(modelMetadata.ModelType),
nestedBindingInfo);

if (innerModelBinder == null)
{
return null;
}

return new HeaderModelBinder(loggerFactory, innerModelBinder);
}

// Support binding only to simple types or collection of simple types.
private bool IsSimpleType(ModelMetadata modelMetadata)
{
var metadata = modelMetadata.ElementMetadata ?? modelMetadata;
return !metadata.IsComplexType;
}
}
}
Loading

0 comments on commit 0215740

Please sign in to comment.