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

Commit

Permalink
Fixing BindNever attribute and Type model binding
Browse files Browse the repository at this point in the history
Fixes #4505
  • Loading branch information
sebastienros committed Apr 22, 2016
1 parent d9aacd0 commit 2c639f8
Show file tree
Hide file tree
Showing 9 changed files with 302 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Options;

Expand Down Expand Up @@ -73,6 +74,11 @@ public void Configure(MvcOptions options)
options.ValueProviderFactories.Add(new JQueryFormValueProviderFactory());

// Set up metadata providers

// Don't bind the Type class by default as it's expensive. A user can override this behavior
// by altering the collection of providers.
options.ModelMetadataDetailsProviders.Add(new ExcludeBindingMetadataProvider(typeof(Type)));

options.ModelMetadataDetailsProviders.Add(new DefaultBindingMetadataProvider(messageProvider));
options.ModelMetadataDetailsProviders.Add(new DefaultValidationMetadataProvider());

Expand Down
19 changes: 19 additions & 0 deletions src/Microsoft.AspNetCore.Mvc.Core/Internal/NoOpBinder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// 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.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.ModelBinding;

namespace Microsoft.AspNetCore.Mvc.Internal
{
public class NoOpBinder : IModelBinder
{
public static readonly IModelBinder Instance = new NoOpBinder();

public Task BindModelAsync(ModelBindingContext bindingContext)
{
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return TaskCache.CompletedTask;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
{
/// <summary>
/// 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"/>/
/// when a model has the binding source <see cref="BindingSource.Body"/>.
/// </summary>
public class BodyModelBinder : IModelBinder
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// 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.Reflection;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
/// <summary>
/// An <see cref="IBindingMetadataProvider"/> which configures <see cref="ModelMetadata.IsBindingAllowed"/> to
/// <c>false</c> for matching types.
/// </summary>
public class ExcludeBindingMetadataProvider : IBindingMetadataProvider
{
private readonly Type _type;

/// <summary>
/// Creates a new <see cref="ExcludeBindingMetadataProvider"/> for the given <paramref name="type"/>.
/// </summary>
/// <param name="type">
/// The <see cref="Type"/>. All properties of this <see cref="Type"/> will have
/// <see cref="ModelMetadata.IsBindingAllowed"/> set to <c>false</c>.
/// </param>
public ExcludeBindingMetadataProvider(Type type)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}

_type = type;
}

/// <inheritdoc />
public void CreateBindingMetadata(BindingMetadataProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}

// No-op if the metadata is not for the target type
if (!_type.IsAssignableFrom(context.Key.ModelType))
{
return;
}

context.BindingMetadata.IsBindingAllowed = false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public IModelBinder CreateBinder(ModelBinderFactoryContext context)

private IModelBinder CreateBinderCore(DefaultModelBinderProviderContext providerContext, object token)
{
if (!providerContext.Metadata.IsBindingAllowed)
{
return NoOpBinder.Instance;
}

// A non-null token will usually be passed in at the the top level (ParameterDescriptor likely).
// This prevents us from treating a parameter the same as a collection-element - which could
// happen looking at just model metadata.
Expand Down Expand Up @@ -188,17 +193,6 @@ public override IModelBinder CreateBinder(ModelMetadata metadata)
}
}

private class NoOpBinder : IModelBinder
{
public static readonly IModelBinder Instance = new NoOpBinder();

public Task BindModelAsync(ModelBindingContext bindingContext)
{
bindingContext.Result = ModelBindingResult.Failed(bindingContext.ModelName);
return TaskCache.CompletedTask;
}
}

private struct Key : IEquatable<Key>
{
private readonly ModelMetadata _metadata;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// 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 Xunit;

namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
public class ExcludeBindingMetadataProviderTest
{
[Theory]
[InlineData(true)]
[InlineData(false)]
public void IsBindingAllowed_LeftAlone_WhenTypeDoesntMatch(bool initialValue)
{
// Arrange
var provider = new ExcludeBindingMetadataProvider(typeof(string));

var key = ModelMetadataIdentity.ForProperty(
typeof(int),
nameof(Person.Age),
typeof(Person));

var context = new BindingMetadataProviderContext(key, new ModelAttributes(new object[0], new object[0]));

context.BindingMetadata.IsBindingAllowed = initialValue;

// Act
provider.CreateBindingMetadata(context);

// Assert
Assert.Equal(initialValue, context.BindingMetadata.IsBindingAllowed);
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void IsBindingAllowed_IsFalse_WhenTypeMatches(bool initialValue)
{
// Arrange
var provider = new ExcludeBindingMetadataProvider(typeof(int));

var key = ModelMetadataIdentity.ForProperty(
typeof(int),
nameof(Person.Age),
typeof(Person));

var context = new BindingMetadataProviderContext(key, new ModelAttributes(new object[0], new object[0]));

context.BindingMetadata.IsBindingAllowed = initialValue;

// Act
provider.CreateBindingMetadata(context);

// Assert
Assert.False(context.BindingMetadata.IsBindingAllowed);
}

private class Person
{
public int Age { get; set; }
}
}
}
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 Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Moq;
using Xunit;
Expand All @@ -14,7 +16,7 @@ public class ModelBinderFactoryTest
[Fact]
public void CreateBinder_Throws_WhenBinderNotCreated()
{
// Arrange
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = new TestOptionsManager<MvcOptions>();
var factory = new ModelBinderFactory(metadataProvider, options);
Expand All @@ -36,7 +38,7 @@ public void CreateBinder_Throws_WhenBinderNotCreated()
[Fact]
public void CreateBinder_CreatesNoOpBinder_WhenPropertyDoesntHaveABinder()
{
// Arrange
// Arrange
var metadataProvider = new TestModelMetadataProvider();

// There isn't a provider that can handle WidgetId.
Expand Down Expand Up @@ -66,10 +68,47 @@ public void CreateBinder_CreatesNoOpBinder_WhenPropertyDoesntHaveABinder()
Assert.NotNull(result);
}

[Fact]
public void CreateBinder_CreatesNoOpBinder_WhenPropertyBindingIsNotAllowed()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
metadataProvider
.ForProperty<Widget>(nameof(Widget.Id))
.BindingDetails(m => m.IsBindingAllowed = false);

var modelBinder = new ByteArrayModelBinder();

var options = new TestOptionsManager<MvcOptions>();
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(c =>
{
if (c.Metadata.ModelType == typeof(WidgetId))
{
return modelBinder;
}

return null;
}));

var factory = new ModelBinderFactory(metadataProvider, options);

var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForProperty(typeof(Widget), nameof(Widget.Id)),
};

// Act
var result = factory.CreateBinder(context);

// Assert
Assert.NotNull(result);
Assert.IsType<NoOpBinder>(result);
}

[Fact]
public void CreateBinder_NestedProperties()
{
// Arrange
// Arrange
var metadataProvider = new TestModelMetadataProvider();

var options = new TestOptionsManager<MvcOptions>();
Expand Down Expand Up @@ -105,7 +144,7 @@ public void CreateBinder_NestedProperties()
[Fact]
public void CreateBinder_BreaksCycles()
{
// Arrange
// Arrange
var metadataProvider = new TestModelMetadataProvider();

var callCount = 0;
Expand Down Expand Up @@ -142,7 +181,7 @@ public void CreateBinder_BreaksCycles()
[Fact]
public void CreateBinder_DoesNotCache_WhenTokenIsNull()
{
// Arrange
// Arrange
var metadataProvider = new TestModelMetadataProvider();

var options = new TestOptionsManager<MvcOptions>();
Expand Down Expand Up @@ -170,7 +209,7 @@ public void CreateBinder_DoesNotCache_WhenTokenIsNull()
[Fact]
public void CreateBinder_Caches_WhenTokenIsNotNull()
{
// Arrange
// Arrange
var metadataProvider = new TestModelMetadataProvider();

var options = new TestOptionsManager<MvcOptions>();
Expand Down
Loading

0 comments on commit 2c639f8

Please sign in to comment.