Optimize async code in model binders

Optimize the 'no-op' path for our model binders to return a cached task
where possible without going async.
This commit is contained in:
Ryan Nowak 2015-09-04 13:03:50 -07:00
parent ab08e27a4b
commit a6aaef0d63
19 changed files with 328 additions and 339 deletions

View File

@ -2,7 +2,6 @@
// 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.Threading.Tasks;
using Microsoft.Framework.Internal;

View File

@ -21,15 +21,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
private readonly ConcurrentDictionary<Type, ObjectFactory> _typeActivatorCache =
new ConcurrentDictionary<Type, ObjectFactory>();
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
if (bindingContext.BinderType == null)
{
// Return NoResult so that we are able to continue with the default set of model binders,
// if there is no specific model binder provided.
return ModelBindingResult.NoResult;
return ModelBindingResult.NoResultAsync;
}
return BindModelCoreAsync(bindingContext);
}
private async Task<ModelBindingResult> BindModelCoreAsync(ModelBindingContext bindingContext)
{
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
var createFactory = _typeActivatorCache.GetOrAdd(bindingContext.BinderType, _createFactory);
var instance = createFactory(requestServices, arguments: null);

View File

@ -1,91 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
/// <summary>
/// An <see cref="IModelBinder"/> which provides data from a specific <see cref="ModelBinding.BindingSource"/>.
/// </summary>
/// <remarks>
/// <para>
/// A <see cref="BindingSourceModelBinder"/> is an <see cref="IModelBinder"/> base-implementation which
/// can provide data for all parameters and model properties which specify the corresponding
/// <see cref="ModelBinding.BindingSource"/>.
/// </para>
/// <para>
/// <see cref="BindingSourceModelBinder"/> is greedy, meaning that a given instance expects to handle all
/// parameters and properties annotated with the corresponding <see cref="ModelBinding.BindingSource"/> and
/// will short-circuit the model binding process to prevent other binders from running.
/// <see cref="ModelBinding.BindingSource.IsGreedy"/> of <see cref="BindingSource"/> must be set to <c>true.</c>
/// </para>
/// </remarks>
public abstract class BindingSourceModelBinder : IModelBinder
{
/// <summary>
/// Creates a new <see cref="BindingSourceModelBinder"/>.
/// </summary>
/// <param name="bindingSource">
/// The <see cref="ModelBinding.BindingSource"/>. Must be a single-source (non-composite) with
/// <see cref="ModelBinding.BindingSource.IsGreedy"/> equal to <c>true</c>.
/// </param>
protected BindingSourceModelBinder([NotNull] BindingSource bindingSource)
{
// This class implements a pattern that's only useful for greedy model binders. If you need
// to implement something non-greedy then don't use the base class.
if (!bindingSource.IsGreedy)
{
var message = Resources.FormatBindingSource_MustBeGreedy(
bindingSource.DisplayName,
nameof(BindingSourceModelBinder));
throw new ArgumentException(message, nameof(bindingSource));
}
BindingSource = bindingSource;
}
/// <summary>
/// Gets the corresponding <see cref="ModelBinding.BindingSource"/>.
/// </summary>
protected BindingSource BindingSource { get; }
/// <summary>
/// Binds the model. Called when the model's supported binding-source matches <see cref="BindingSource"/>.
/// </summary>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <returns>
/// A <see cref="Task"/> which will complete when model binding has completed.
/// </returns>
/// <remarks>
/// Other model binders will never run if this method is called. Return <c>null</c> to skip other model binders
/// but allow higher-level handling e.g. falling back to empty prefix.
/// </remarks>
protected abstract Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext);
/// <inheritdoc />
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext context)
{
var allowedBindingSource = context.BindingSource;
if (allowedBindingSource == null || !allowedBindingSource.CanAcceptDataFrom(BindingSource))
{
// Binding Sources are opt-in. This model either didn't specify one or specified something
// incompatible so let other binders run.
return ModelBindingResult.NoResult;
}
var result = await BindModelCoreAsync(context);
var modelBindingResult = result != ModelBindingResult.NoResult ?
result :
ModelBindingResult.Failed(context.ModelName);
// This model binder is the only handler for its binding source.
// Always tell the model binding system to skip other model binders i.e. return non-null.
return modelBindingResult;
}
}
}

View File

@ -14,19 +14,35 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// An <see cref="IModelBinder"/> which binds models from the request body using an <see cref="IInputFormatter"/>
/// when a model has the binding source <see cref="BindingSource.Body"/>/
/// </summary>
public class BodyModelBinder : BindingSourceModelBinder
public class BodyModelBinder : IModelBinder
{
/// <summary>
/// Creates a new <see cref="BodyModelBinder"/>.
/// </summary>
public BodyModelBinder()
: base(BindingSource.Body)
/// <inheritdoc />
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
var allowedBindingSource = bindingContext.BindingSource;
if (allowedBindingSource == null ||
!allowedBindingSource.CanAcceptDataFrom(BindingSource.Body))
{
// Formatters are opt-in. This model either didn't specify [FromBody] or specified something
// incompatible so let other binders run.
return ModelBindingResult.NoResultAsync;
}
return BindModelCoreAsync(bindingContext);
}
/// <inheritdoc />
protected async override Task<ModelBindingResult> BindModelCoreAsync(
[NotNull] ModelBindingContext bindingContext)
/// <summary>
/// Attempts to bind the model using formatters.
/// </summary>
/// <param name="bindingContext">The <see cref="ModelBindingContext"/>.</param>
/// <returns>
/// A <see cref="Task{ModelBindingResult}"/> which when completed returns a <see cref="ModelBindingResult"/>.
/// </returns>
private async Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{
// For compatibility with MVC 5.0 for top level object we want to consider an empty key instead of
// the parameter name/a custom name. In all other cases (like when binding body to a property) we

View File

@ -15,22 +15,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// <inheritdoc />
public Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
return Task.FromResult(BindModel(bindingContext));
}
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
private ModelBindingResult BindModel(ModelBindingContext bindingContext)
{
// Check if this binder applies.
if (bindingContext.ModelType != typeof(byte[]))
{
return ModelBindingResult.NoResult;
return ModelBindingResult.NoResultAsync;
}
// Check for missing data case 1: There was no <input ... /> element containing this data.
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
{
return ModelBindingResult.Failed(bindingContext.ModelName);
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
@ -39,7 +38,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
var value = valueProviderResult.FirstValue;
if (string.IsNullOrEmpty(value))
{
return ModelBindingResult.Failed(bindingContext.ModelName);
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
try
@ -50,7 +49,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelMetadata,
model);
return ModelBindingResult.Success(bindingContext.ModelName, model, validationNode);
return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, validationNode);
}
catch (Exception ex)
{
@ -59,7 +58,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Matched the type (byte[]) only this binder supports. As in missing data cases, always tell the model
// binding system to skip other model binders i.e. return non-null.
return ModelBindingResult.Failed(bindingContext.ModelName);
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
}
}

View File

@ -18,13 +18,22 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class FormCollectionModelBinder : IModelBinder
{
/// <inheritdoc />
public async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
if (bindingContext.ModelType != typeof(IFormCollection))
{
return ModelBindingResult.NoResult;
return ModelBindingResult.NoResultAsync;
}
return BindModelCoreAsync(bindingContext);
}
private async Task<ModelBindingResult> BindModelCoreAsync(ModelBindingContext bindingContext)
{
object model;
var request = bindingContext.OperationBindingContext.HttpContext.Request;
if (request.HasFormContentType)

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
#if DNXCORE50
using System.Reflection;
@ -20,8 +21,23 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
public class FormFileModelBinder : IModelBinder
{
/// <inheritdoc />
public async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
if (bindingContext.ModelType != typeof(IFormFile) &&
!typeof(IEnumerable<IFormFile>).IsAssignableFrom(bindingContext.ModelType))
{
return ModelBindingResult.NoResultAsync;
}
return BindModelCoreAsync(bindingContext);
}
private async Task<ModelBindingResult> BindModelCoreAsync(ModelBindingContext bindingContext)
{
object value;
if (bindingContext.ModelType == typeof(IFormFile))
{
@ -36,6 +52,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
else
{
// This binder does not support the requested type.
Debug.Fail("We shouldn't be called without a matching type.");
return ModelBindingResult.NoResult;
}

View File

@ -14,33 +14,44 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class GenericModelBinder : IModelBinder
{
public async Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
var binderType = ResolveBinderType(bindingContext);
if (binderType != null)
if (binderType == null)
{
var binder = (IModelBinder)Activator.CreateInstance(binderType);
var collectionBinder = binder as ICollectionModelBinder;
if (collectionBinder != null &&
bindingContext.Model == null &&
!collectionBinder.CanCreateInstance(bindingContext.ModelType))
{
// Able to resolve a binder type but need a new model instance and that binder cannot create it.
return ModelBindingResult.NoResult;
}
var result = await binder.BindModelAsync(bindingContext);
var modelBindingResult = result != ModelBindingResult.NoResult ?
result :
ModelBindingResult.Failed(bindingContext.ModelName);
// Were able to resolve a binder type.
// Always tell the model binding system to skip other model binders.
return modelBindingResult;
return ModelBindingResult.NoResultAsync;
}
return ModelBindingResult.NoResult;
var binder = (IModelBinder)Activator.CreateInstance(binderType);
var collectionBinder = binder as ICollectionModelBinder;
if (collectionBinder != null &&
bindingContext.Model == null &&
!collectionBinder.CanCreateInstance(bindingContext.ModelType))
{
// Able to resolve a binder type but need a new model instance and that binder cannot create it.
return ModelBindingResult.NoResultAsync;
}
return BindModelCoreAsync(bindingContext, binder);
}
private async Task<ModelBindingResult> BindModelCoreAsync(ModelBindingContext bindingContext, IModelBinder binder)
{
Debug.Assert(binder != null);
var result = await binder.BindModelAsync(bindingContext);
var modelBindingResult = result != ModelBindingResult.NoResult ?
result :
ModelBindingResult.Failed(bindingContext.ModelName);
// Were able to resolve a binder type.
// Always tell the model binding system to skip other model binders.
return modelBindingResult;
}
private static Type ResolveBinderType(ModelBindingContext context)

View File

@ -7,7 +7,6 @@ using System.Reflection;
#endif
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
@ -15,19 +14,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// An <see cref="IModelBinder"/> which binds models from the request headers when a model
/// has the binding source <see cref="BindingSource.Header"/>/
/// </summary>
public class HeaderModelBinder : BindingSourceModelBinder
public class HeaderModelBinder : IModelBinder
{
/// <summary>
/// Creates a new <see cref="HeaderModelBinder"/>.
/// </summary>
public HeaderModelBinder()
: base(BindingSource.Header)
{
}
/// <inheritdoc />
protected override Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
var allowedBindingSource = bindingContext.BindingSource;
if (allowedBindingSource == null ||
!allowedBindingSource.CanAcceptDataFrom(BindingSource.Header))
{
// Headers are opt-in. This model either didn't specify [FromHeader] or specified something
// incompatible so let other binders run.
return ModelBindingResult.NoResultAsync;
}
var request = bindingContext.OperationBindingContext.HttpContext.Request;
var modelMetadata = bindingContext.ModelMetadata;

View File

@ -21,12 +21,12 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
typeof(MutableObjectModelBinder).GetTypeInfo().GetDeclaredMethod(nameof(CallPropertyAddRange));
/// <inheritdoc />
public virtual async Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync([NotNull] ModelBindingContext bindingContext)
{
ModelBindingHelper.ValidateBindingContext(bindingContext);
if (!CanBindType(bindingContext.ModelMetadata))
{
return ModelBindingResult.NoResult;
return ModelBindingResult.NoResultAsync;
}
var mutableObjectBinderContext = new MutableObjectBinderContext()
@ -37,9 +37,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
if (!(CanCreateModel(mutableObjectBinderContext)))
{
return ModelBindingResult.NoResult;
return ModelBindingResult.NoResultAsync;
}
return BindModelCoreAsync(bindingContext, mutableObjectBinderContext);
}
private async Task<ModelBindingResult> BindModelCoreAsync(
ModelBindingContext bindingContext,
MutableObjectBinderContext mutableObjectBinderContext)
{
// Create model first (if necessary) to avoid reporting errors about properties when activation fails.
var model = GetModel(bindingContext);

View File

@ -3,7 +3,6 @@
using System.Threading.Tasks;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Internal;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
@ -11,19 +10,24 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
/// An <see cref="IModelBinder"/> which binds models from the request services when a model
/// has the binding source <see cref="BindingSource.Services"/>/
/// </summary>
public class ServicesModelBinder : BindingSourceModelBinder
public class ServicesModelBinder : IModelBinder
{
/// <summary>
/// Creates a new <see cref="ServicesModelBinder"/>.
/// </summary>
public ServicesModelBinder()
: base(BindingSource.Services)
{
}
/// <inheritdoc />
protected override Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
var allowedBindingSource = bindingContext.BindingSource;
if (allowedBindingSource == null ||
!allowedBindingSource.CanAcceptDataFrom(BindingSource.Services))
{
// Services are opt-in. This model either didn't specify [FromService] or specified something
// incompatible so let other binders run.
return ModelBindingResult.NoResultAsync;
}
var requestServices = bindingContext.OperationBindingContext.HttpContext.RequestServices;
var model = requestServices.GetRequiredService(bindingContext.ModelType);
var validationNode =

View File

@ -11,22 +11,21 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
{
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
return Task.FromResult(BindModel(bindingContext));
}
// This method is optimized to use cached tasks when possible and avoid allocating
// using Task.FromResult. If you need to make changes of this nature, profile
// allocations afterwards and look for Task<ModelBindingResult>.
public ModelBindingResult BindModel(ModelBindingContext bindingContext)
{
if (bindingContext.ModelMetadata.IsComplexType)
{
// this type cannot be converted
return ModelBindingResult.NoResult;
return ModelBindingResult.NoResultAsync;
}
var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
if (valueProviderResult == ValueProviderResult.None)
{
// no entry
return ModelBindingResult.NoResult;
return ModelBindingResult.NoResultAsync;
}
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, valueProviderResult);
@ -54,16 +53,16 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
bindingContext.ModelName,
Resources.FormatCommon_ValueNotValidForProperty(model));
return ModelBindingResult.Failed(bindingContext.ModelName);
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
else
{
var validationNode = new ModelValidationNode(
bindingContext.ModelName,
bindingContext.ModelMetadata,
bindingContext.ModelMetadata,
model);
return ModelBindingResult.Success(bindingContext.ModelName, model, validationNode);
return ModelBindingResult.SuccessAsync(bindingContext.ModelName, model, validationNode);
}
}
catch (Exception ex)
@ -72,7 +71,7 @@ namespace Microsoft.AspNet.Mvc.ModelBinding
// Were able to find a converter for the type but conversion failed.
// Tell the model binding system to skip other model binders.
return ModelBindingResult.Failed(bindingContext.ModelName);
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
}
}

View File

@ -954,22 +954,6 @@ namespace Microsoft.AspNet.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_MustBeFromRequest"), p0, p1);
}
/// <summary>
/// The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources.
/// </summary>
internal static string BindingSource_MustBeGreedy
{
get { return GetString("BindingSource_MustBeGreedy"); }
}
/// <summary>
/// The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources.
/// </summary>
internal static string FormatBindingSource_MustBeGreedy(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("BindingSource_MustBeGreedy"), p0, p1);
}
/// <summary>
/// The property {0}.{1} could not be found.
/// </summary>

View File

@ -303,9 +303,6 @@
<data name="BindingSource_MustBeFromRequest" xml:space="preserve">
<value>The provided binding source '{0}' is not a request-based binding source. '{1}' requires that the source must represent data from an HTTP request.</value>
</data>
<data name="BindingSource_MustBeGreedy" xml:space="preserve">
<value>The provided binding source '{0}' is not a greedy data source. '{1}' only supports greedy data sources.</value>
</data>
<data name="Common_PropertyNotFound" xml:space="preserve">
<value>The property {0}.{1} could not be found.</value>
</data>

View File

@ -1,136 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.Framework.Internal;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class BindingSourceModelBinderTest
{
[Fact]
public void BindingSourceModelBinder_ThrowsOnNonGreedySource()
{
// Arrange
var expected =
"The provided binding source 'Test Source' is not a greedy data source. " +
"'BindingSourceModelBinder' only supports greedy data sources." + Environment.NewLine +
"Parameter name: bindingSource";
var bindingSource = new BindingSource(
"Test",
displayName: "Test Source",
isGreedy: false,
isFromRequest: true);
// Act & Assert
var exception = Assert.Throws<ArgumentException>(
() => new TestableBindingSourceModelBinder(bindingSource, isModelSet: false));
Assert.Equal(expected, exception.Message);
}
[Fact]
public async Task BindingSourceModelBinder_ReturnsNull_WithNoSource()
{
// Arrange
var context = new ModelBindingContext()
{
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(string)),
ModelName = "model",
};
var binder = new TestableBindingSourceModelBinder(BindingSource.Body, isModelSet: false);
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.Equal(ModelBindingResult.NoResult, result);
Assert.False(binder.WasBindModelCoreCalled);
}
[Fact]
public async Task BindingSourceModelBinder_ReturnsNull_NonMatchingSource()
{
// Arrange
var provider = new TestModelMetadataProvider();
provider.ForType<string>().BindingDetails(d => d.BindingSource = BindingSource.Query);
var context = new ModelBindingContext()
{
ModelMetadata = new EmptyModelMetadataProvider().GetMetadataForType(typeof(string)),
ModelName = "model",
};
var binder = new TestableBindingSourceModelBinder(BindingSource.Body, isModelSet: false);
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.Equal(ModelBindingResult.NoResult, result);
Assert.False(binder.WasBindModelCoreCalled);
}
[Theory]
[InlineData(false)]
[InlineData(true)]
public async Task BindingSourceModelBinder_ReturnsNonEmptyResult_MatchingSource(bool isModelSet)
{
// Arrange
var provider = new TestModelMetadataProvider();
provider.ForType<string>().BindingDetails(d => d.BindingSource = BindingSource.Body);
var modelMetadata = provider.GetMetadataForType(typeof(string));
var context = new ModelBindingContext()
{
BinderModelName = modelMetadata.BinderModelName,
BindingSource = modelMetadata.BindingSource,
ModelMetadata = modelMetadata,
ModelName = "model",
};
var binder = new TestableBindingSourceModelBinder(BindingSource.Body, isModelSet);
// Act
var result = await binder.BindModelAsync(context);
// Assert
Assert.NotEqual(ModelBindingResult.NoResult, result);
Assert.Equal(isModelSet, result.IsModelSet);
Assert.Null(result.Model);
Assert.True(binder.WasBindModelCoreCalled);
}
private class TestableBindingSourceModelBinder : BindingSourceModelBinder
{
bool _isModelSet;
public TestableBindingSourceModelBinder(BindingSource source, bool isModelSet)
: base(source)
{
_isModelSet = isModelSet;
}
public bool WasBindModelCoreCalled { get; private set; }
protected override Task<ModelBindingResult> BindModelCoreAsync([NotNull] ModelBindingContext bindingContext)
{
WasBindModelCoreCalled = true;
if (_isModelSet)
{
return ModelBindingResult.SuccessAsync(
bindingContext.ModelName,
model: null,
validationNode: null);
}
else
{
return ModelBindingResult.FailedAsync(bindingContext.ModelName);
}
}
}
}
}

View File

@ -72,6 +72,50 @@ namespace Microsoft.AspNet.Mvc.ModelBinding.Test
Assert.Equal(headerValue, result.Model);
}
[Fact]
public async Task HeaderBinder_ReturnsNoResult_ForNullBindingSource()
{
// Arrange
var type = typeof(string);
var header = "User-Agent";
var headerValue = "UnitTest";
var binder = new HeaderModelBinder();
var modelBindingContext = GetBindingContext(type);
modelBindingContext.BindingSource = null;
modelBindingContext.FieldName = header;
modelBindingContext.OperationBindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
// Act
var result = await binder.BindModelAsync(modelBindingContext);
// Assert
Assert.Equal(ModelBindingResult.NoResult, result);
}
[Fact]
public async Task HeaderBinder_ReturnsNoResult_ForNonHeaderBindingSource()
{
// Arrange
var type = typeof(string);
var header = "User-Agent";
var headerValue = "UnitTest";
var binder = new HeaderModelBinder();
var modelBindingContext = GetBindingContext(type);
modelBindingContext.BindingSource = BindingSource.Body;
modelBindingContext.FieldName = header;
modelBindingContext.OperationBindingContext.HttpContext.Request.Headers.Add(header, new[] { headerValue });
// Act
var result = await binder.BindModelAsync(modelBindingContext);
// Assert
Assert.Equal(ModelBindingResult.NoResult, result);
}
private static ModelBindingContext GetBindingContext(Type modelType)
{
var metadataProvider = new TestModelMetadataProvider();

View File

@ -0,0 +1,111 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.Framework.DependencyInjection;
using Xunit;
namespace Microsoft.AspNet.Mvc.ModelBinding
{
public class ServiceModelBinderTest
{
[Fact]
public async Task ServiceModelBinder_BindsService()
{
// Arrange
var type = typeof(IService);
var binder = new ServicesModelBinder();
var modelBindingContext = GetBindingContext(type);
// Act
var result = await binder.BindModelAsync(modelBindingContext);
// Assert
Assert.NotEqual(ModelBindingResult.NoResult, result);
Assert.True(result.IsModelSet);
Assert.NotNull(result.Model);
Assert.Equal("modelName", result.Key);
Assert.NotNull(result.ValidationNode);
Assert.Equal("modelName", result.ValidationNode.Key);
Assert.True(result.ValidationNode.SuppressValidation);
}
[Fact]
public async Task ServiceModelBinder_ReturnsNoResult_ForNullBindingSource()
{
// Arrange
var type = typeof(IService);
var binder = new ServicesModelBinder();
var modelBindingContext = GetBindingContext(type);
modelBindingContext.BindingSource = null;
// Act
var result = await binder.BindModelAsync(modelBindingContext);
// Assert
Assert.Equal(ModelBindingResult.NoResult, result);
}
[Fact]
public async Task ServiceModelBinder_ReturnsNoResult_ForNonServiceBindingSource()
{
// Arrange
var type = typeof(IService);
var binder = new ServicesModelBinder();
var modelBindingContext = GetBindingContext(type);
modelBindingContext.BindingSource = BindingSource.Body;
// Act
var result = await binder.BindModelAsync(modelBindingContext);
// Assert
Assert.Equal(ModelBindingResult.NoResult, result);
}
private static ModelBindingContext GetBindingContext(Type modelType)
{
var metadataProvider = new TestModelMetadataProvider();
metadataProvider.ForType(modelType).BindingDetails(d => d.BindingSource = BindingSource.Services);
var modelMetadata = metadataProvider.GetMetadataForType(modelType);
var services = new ServiceCollection();
services.AddInstance<IService>(new Service());
var bindingContext = new ModelBindingContext
{
ModelMetadata = modelMetadata,
ModelName = "modelName",
FieldName = "modelName",
ModelState = new ModelStateDictionary(),
OperationBindingContext = new OperationBindingContext
{
ModelBinder = new HeaderModelBinder(),
MetadataProvider = metadataProvider,
HttpContext = new DefaultHttpContext()
{
RequestServices = services.BuildServiceProvider(),
},
},
BinderModelName = modelMetadata.BinderModelName,
BindingSource = modelMetadata.BindingSource,
};
return bindingContext;
}
private interface IService
{
}
private class Service : IService
{
}
}
}

View File

@ -148,15 +148,19 @@ namespace Microsoft.AspNet.Mvc.IntegrationTests
}
}
private class AddressBinder : BindingSourceModelBinder
private class AddressBinder : IModelBinder
{
public AddressBinder()
: base(BindAddressAttribute.Source)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
}
var allowedBindingSource = bindingContext.BindingSource;
if (allowedBindingSource == null ||
!allowedBindingSource.CanAcceptDataFrom(BindAddressAttribute.Source))
{
// Binding Sources are opt-in. This model either didn't specify one or specified something
// incompatible so let other binders run.
return ModelBindingResult.NoResultAsync;
}
protected override Task<ModelBindingResult> BindModelCoreAsync(ModelBindingContext bindingContext)
{
return ModelBindingResult.SuccessAsync(bindingContext.ModelName, new Address(), validationNode: null);
}
}

View File

@ -10,15 +10,17 @@ using Microsoft.AspNet.Mvc.ModelBinding.Metadata;
namespace ModelBindingWebSite
{
public class TestBindingSourceModelBinder : BindingSourceModelBinder
public class TestBindingSourceModelBinder : IModelBinder
{
public TestBindingSourceModelBinder()
: base(FromTestAttribute.TestBindingSource)
public Task<ModelBindingResult> BindModelAsync(ModelBindingContext bindingContext)
{
}
var allowedBindingSource = bindingContext.BindingSource;
if (allowedBindingSource == null ||
!allowedBindingSource.CanAcceptDataFrom(FromTestAttribute.TestBindingSource))
{
return ModelBindingResult.NoResultAsync;
}
protected override Task<ModelBindingResult> BindModelCoreAsync(ModelBindingContext bindingContext)
{
var attributes = ((DefaultModelMetadata)bindingContext.ModelMetadata).Attributes;
var metadata = attributes.Attributes.OfType<FromTestAttribute>().First();
var model = metadata.Value;