|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Abstractions;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Infrastructure;
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.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Moq;
namespace Microsoft.AspNetCore.Mvc.Controllers;
public class ControllerBinderDelegateProviderTest
{
private static readonly MvcOptions _options = new MvcOptions();
private static readonly IOptions<MvcOptions> _optionsAccessor = Options.Create(_options);
[Fact]
public async Task CreateBinderDelegate_Delegate_DoesNotAddActionArgumentsOrCallBinderOrValidator_IfBindingIsNotAllowed_OnParameter()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ControllerParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
BindingInfo = new BindingInfo(),
ParameterInfo = ParameterInfos.BindNeverParameterInfo
});
var controllerContext = GetControllerContext(actionDescriptor);
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Verifiable();
var mockValidator = new Mock<IModelValidator>(MockBehavior.Strict);
mockValidator.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()));
var factory = GetModelBinderFactory(binder.Object);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var controller = new TestController();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory,
GetModelValidatorProvider(mockValidator.Object));
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
TestModelMetadataProvider.CreateDefaultProvider(),
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Empty(arguments);
binder
.Verify(o => o.BindModelAsync(
It.IsAny<DefaultModelBindingContext>()),
Times.Never());
mockValidator
.Verify(o => o.Validate(
It.IsAny<ModelValidationContext>()),
Times.Never());
}
[Fact]
public async Task CreateBinderDelegate_Delegate_DoesNotAddActionArgumentsOrCallBinderOrValidator_IfBindingIsNotAllowed_OnProperty()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = nameof(TestController.RequiredButBindNeverProperty),
ParameterType = typeof(object)
});
var controllerContext = GetControllerContext(actionDescriptor);
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Verifiable();
var mockValidator = new Mock<IModelValidator>(MockBehavior.Strict);
mockValidator.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()));
var validatorProvider = GetModelValidatorProvider(mockValidator.Object);
var factory = GetModelBinderFactory(binder.Object);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var controller = new TestController();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory,
GetModelValidatorProvider(mockValidator.Object));
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
TestModelMetadataProvider.CreateDefaultProvider(),
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Empty(arguments);
binder
.Verify(o => o.BindModelAsync(
It.IsAny<DefaultModelBindingContext>()),
Times.Never());
mockValidator
.Verify(o => o.Validate(
It.IsAny<ModelValidationContext>()),
Times.Never());
}
[Fact]
public async Task CreateBinderDelegate_Delegate_DoesNotAddActionArguments_IfBinderReturnsNull()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
BindingInfo = new BindingInfo(),
});
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Returns(Task.CompletedTask);
var factory = GetModelBinderFactory(binder.Object);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Empty(arguments);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_DoesNotAddActionArguments_IfBinderDoesNotSetModel()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
BindingInfo = new BindingInfo(),
});
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Returns(Task.CompletedTask);
var factory = GetModelBinderFactory(binder.Object);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Empty(arguments);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_AddsActionArguments_IfBinderReturnsNotNull()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(string),
BindingInfo = new BindingInfo(),
});
var value = "Hello world";
var metadataProvider = new EmptyModelMetadataProvider();
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Callback((ModelBindingContext context) =>
{
context.ModelMetadata = metadataProvider.GetMetadataForType(typeof(string));
context.Result = ModelBindingResult.Success(value);
})
.Returns(Task.CompletedTask);
var factory = GetModelBinderFactory(binder.Object);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Single(arguments);
Assert.Equal(value, arguments["foo"]);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_GetsMetadataFromParameter()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ControllerParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
ParameterInfo = ParameterInfos.NoAttributesParameterInfo
});
var controllerContext = GetControllerContext(actionDescriptor);
var mockBinder = new Mock<IModelBinder>();
var factory = GetModelBinderFactory(mockBinder.Object);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var modelMetadata = new Mock<FakeModelMetadata>();
modelMetadata.Setup(m => m.IsBindingAllowed).Returns(true);
var mockMetadataProvider = new Mock<DefaultModelMetadataProvider>(
Mock.Of<ICompositeMetadataDetailsProvider>());
mockMetadataProvider
.Setup(p => p.GetMetadataForParameter(ParameterInfos.NoAttributesParameterInfo))
.Returns(modelMetadata.Object);
var parameterBinder = GetParameterBinder(
mockMetadataProvider.Object,
factory);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
mockMetadataProvider.Object,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
mockBinder
.Verify(o => o.BindModelAsync(
It.Is<ModelBindingContext>(context => context.ModelMetadata == modelMetadata.Object)),
Times.Once());
}
[Fact]
public async Task CreateBinderDelegate_Delegate_GetsMetadataFromType_IsMetadataProviderIsNotDefaultMetadataProvider()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ControllerParameterDescriptor
{
Name = "foo",
ParameterType = typeof(Person)
});
var controllerContext = GetControllerContext(actionDescriptor);
var mockBinder = new Mock<IModelBinder>();
var factory = GetModelBinderFactory(mockBinder.Object);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var modelMetadata = new Mock<FakeModelMetadata>();
modelMetadata.Setup(m => m.IsBindingAllowed).Returns(true);
var mockMetadataProvider = new Mock<IModelMetadataProvider>();
mockMetadataProvider
.Setup(p => p.GetMetadataForType(typeof(Person)))
.Returns(modelMetadata.Object);
var parameterBinder = GetParameterBinder(
mockMetadataProvider.Object,
factory);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
mockMetadataProvider.Object,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
mockBinder
.Verify(o => o.BindModelAsync(
It.Is<ModelBindingContext>(context => context.ModelMetadata == modelMetadata.Object)),
Times.Once());
}
[Fact]
public async Task CreateBinderDelegate_Delegate_CallsValidator_IfModelBinderSucceeds()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ControllerParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
ParameterInfo = ParameterInfos.CustomValidationParameterInfo
});
var controllerContext = GetControllerContext(actionDescriptor);
var factory = GetModelBinderFactory("Hello");
var mockValidator = new Mock<IModelValidator>();
mockValidator
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Returns(new[] { new ModelValidationResult("memberName", "some message") });
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory,
GetModelValidatorProvider(mockValidator.Object));
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
mockValidator.Verify(o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Once());
Assert.False(controllerContext.ModelState.IsValid);
Assert.Equal(
"some message",
controllerContext.ModelState["memberName"].Errors.Single().ErrorMessage);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_DoesNotCallValidator_IfModelBinderFails()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(
new ParameterDescriptor
{
Name = "foo",
ParameterType = typeof(object),
BindingInfo = new BindingInfo(),
});
var controllerContext = GetControllerContext(actionDescriptor);
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Returns(Task.CompletedTask);
var mockValidator = new Mock<IModelValidator>(MockBehavior.Strict);
mockValidator.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()));
var factory = GetModelBinderFactory(binder.Object);
var controller = new TestController();
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory,
GetModelValidatorProvider(mockValidator.Object));
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
mockValidator
.Verify(o => o.Validate(
It.IsAny<ModelValidationContext>()),
Times.Never());
}
[Fact]
public async Task CreateBinderDelegate_Delegate_CallsValidator_ForControllerProperties_IfModelBinderSucceeds()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = nameof(TestController.ValidatedProperty),
ParameterType = typeof(string),
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var mockValidator = new Mock<IModelValidator>(MockBehavior.Strict);
mockValidator
.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()))
.Returns(new[] { new ModelValidationResult("memberName", "some message") });
var factory = GetModelBinderFactory("Hello");
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = new ParameterBinder(
modelMetadataProvider,
factory,
GetObjectValidator(modelMetadataProvider, GetModelValidatorProvider(mockValidator.Object)),
_optionsAccessor,
NullLoggerFactory.Instance);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
mockValidator.Verify(o => o.Validate(It.IsAny<ModelValidationContext>()), Times.Once());
Assert.False(controllerContext.ModelState.IsValid);
Assert.Equal(
"some message",
controllerContext.ModelState["memberName"].Errors.Single().ErrorMessage);
}
[Fact]
public async Task DoesNotValidate_ForControllerProperties_IfObjectValidatorDoesNotInheritFromBase()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = nameof(TestController.ValidatedProperty),
ParameterType = typeof(string),
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var factory = GetModelBinderFactory("Hello");
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var mockValidator = new Mock<IObjectModelValidator>(MockBehavior.Strict);
mockValidator
.Setup(o => o.Validate(
It.IsAny<ActionContext>(),
It.IsAny<ValidationStateDictionary>(),
It.IsAny<string>(),
It.IsAny<object>()));
var parameterBinder = new ParameterBinder(
modelMetadataProvider,
factory,
mockValidator.Object,
_optionsAccessor,
NullLoggerFactory.Instance);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.True(controllerContext.ModelState.IsValid);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_DoesNotCallValidator_ForControllerProperties_IfModelBinderFails()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = nameof(TestController.StringProperty),
ParameterType = typeof(string),
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Returns(Task.CompletedTask);
var mockValidator = new Mock<IModelValidator>(MockBehavior.Strict);
mockValidator.Setup(o => o.Validate(It.IsAny<ModelValidationContext>()));
var factory = GetModelBinderFactory(binder.Object);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = new ParameterBinder(
modelMetadataProvider,
factory,
GetObjectValidator(modelMetadataProvider, GetModelValidatorProvider(mockValidator.Object)),
_optionsAccessor,
NullLoggerFactory.Instance);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
mockValidator
.Verify(o => o.Validate(
It.IsAny<ModelValidationContext>()),
Times.Never());
}
[Fact]
public async Task CreateBinderDelegate_Delegate_SetsControllerProperties_ForReferenceTypes()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = nameof(TestController.StringProperty),
BindingInfo = new BindingInfo(),
ParameterType = typeof(string)
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var factory = GetModelBinderFactory("Hello");
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Equal("Hello", controller.StringProperty);
Assert.Equal(new List<string> { "goodbye" }, controller.CollectionProperty);
Assert.Null(controller.UntouchedProperty);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_AddsToCollectionControllerProperties()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = nameof(TestController.CollectionProperty),
BindingInfo = new BindingInfo(),
ParameterType = typeof(ICollection<string>),
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var expected = new List<string> { "Hello", "World", "!!" };
var factory = GetModelBinderFactory(expected);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Equal(expected, controller.CollectionProperty);
Assert.Null(controller.StringProperty);
Assert.Null(controller.UntouchedProperty);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_DoesNotSetNullValues_ForNonNullableProperties()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = nameof(TestController.NonNullableProperty),
BindingInfo = new BindingInfo() { BindingSource = BindingSource.Custom },
ParameterType = typeof(int)
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new StubModelBinder(ModelBindingResult.Success(model: null));
var factory = GetModelBinderFactory(binder);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
// Some non default value.
controller.NonNullableProperty = -1;
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Equal(-1, controller.NonNullableProperty);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_SetsNullValues_ForNullableProperties()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = "NullableProperty",
BindingInfo = new BindingInfo() { BindingSource = BindingSource.Custom },
ParameterType = typeof(int?)
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new StubModelBinder(ModelBindingResult.Success(model: null));
var factory = GetModelBinderFactory(binder);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
// Some non default value.
controller.NullableProperty = -1;
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Null(controller.NullableProperty);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_SupportsRequestPredicate_ForPropertiesAndParameters_NotBound()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(new ParameterDescriptor
{
Name = "test-parameter",
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Custom,
// Simulates [BindProperty] on a parameter
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute()).RequestPredicate,
},
ParameterType = typeof(string)
});
actionDescriptor.BoundProperties.Add(new ParameterDescriptor
{
Name = nameof(TestController.NullableProperty),
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Custom,
// Simulates [BindProperty] on a property
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute()).RequestPredicate,
},
ParameterType = typeof(string)
});
var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.HttpContext.Request.Method = "GET";
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new StubModelBinder(ModelBindingResult.Success(model: null));
var factory = GetModelBinderFactory(binder);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
// Some non default value.
controller.NullableProperty = -1;
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Equal(-1, controller.NullableProperty);
Assert.DoesNotContain("test-parameter", arguments.Keys);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_SupportsRequestPredicate_ForPropertiesAndParameters_Bound()
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.Parameters.Add(new ParameterDescriptor
{
Name = "test-parameter",
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Custom,
// Simulates [BindProperty] on a parameter
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute()).RequestPredicate,
},
ParameterType = typeof(string)
});
actionDescriptor.BoundProperties.Add(new ParameterDescriptor
{
Name = nameof(TestController.NullableProperty),
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Custom,
// Simulates [BindProperty] on a property
RequestPredicate = ((IRequestPredicateProvider)new BindPropertyAttribute()).RequestPredicate,
},
ParameterType = typeof(string)
});
var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.HttpContext.Request.Method = "POST";
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new StubModelBinder(ModelBindingResult.Success(model: null));
var factory = GetModelBinderFactory(binder);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
// Some non default value.
controller.NullableProperty = -1;
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Null(controller.NullableProperty);
Assert.Contains("test-parameter", arguments.Keys);
Assert.Null(arguments["test-parameter"]);
}
// property name, property type, property accessor, input value, expected value
public static TheoryData<string, Type, Func<object, object>, object, object> SkippedPropertyData
{
get
{
return new TheoryData<string, Type, Func<object, object>, object, object>
{
{
nameof(TestController.ArrayProperty),
typeof(string[]),
controller => ((TestController)controller).ArrayProperty,
new string[] { "hello", "world" },
new string[] { "goodbye" }
},
{
nameof(TestController.CollectionProperty),
typeof(ICollection<string>),
controller => ((TestController)controller).CollectionProperty,
null,
new List<string> { "goodbye" }
},
{
nameof(TestController.NonCollectionProperty),
typeof(Person),
controller => ((TestController)controller).NonCollectionProperty,
new Person { Name = "Fred" },
new Person { Name = "Ginger" }
},
{
nameof(TestController.NullCollectionProperty),
typeof(ICollection<string>),
controller => ((TestController)controller).NullCollectionProperty,
new List<string> { "hello", "world" },
null
},
};
}
}
[Theory]
[MemberData(nameof(SkippedPropertyData))]
public async Task CreateBinderDelegate_Delegate_SkipsReadOnlyControllerProperties(
string propertyName,
Type propertyType,
Func<object, object> propertyAccessor,
object inputValue,
object expectedValue)
{
// Arrange
var actionDescriptor = GetActionDescriptor();
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = propertyName,
BindingInfo = new BindingInfo(),
ParameterType = propertyType,
});
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var factory = GetModelBinderFactory(inputValue);
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Equal(expectedValue, propertyAccessor(controller));
Assert.Null(controller.StringProperty);
Assert.Null(controller.UntouchedProperty);
}
[Fact]
public async Task CreateBinderDelegate_Delegate_SetsMultipleControllerProperties()
{
// Arrange
var boundPropertyTypes = new Dictionary<string, Type>
{
{ nameof(TestController.ArrayProperty), typeof(string[]) }, // Skipped
{ nameof(TestController.CollectionProperty), typeof(List<string>) },
{ nameof(TestController.NonCollectionProperty), typeof(Person) }, // Skipped
{ nameof(TestController.NullCollectionProperty), typeof(List<string>) }, // Skipped
{ nameof(TestController.StringProperty), typeof(string) },
};
var inputPropertyValues = new Dictionary<string, object>
{
{ nameof(TestController.ArrayProperty), new string[] { "hello", "world" } },
{ nameof(TestController.CollectionProperty), new List<string> { "hello", "world" } },
{ nameof(TestController.NonCollectionProperty), new Person { Name = "Fred" } },
{ nameof(TestController.NullCollectionProperty), new List<string> { "hello", "world" } },
{ nameof(TestController.StringProperty), "Hello" },
};
var actionDescriptor = GetActionDescriptor();
foreach (var keyValuePair in boundPropertyTypes)
{
actionDescriptor.BoundProperties.Add(
new ParameterDescriptor
{
Name = keyValuePair.Key,
BindingInfo = new BindingInfo(),
ParameterType = keyValuePair.Value,
});
}
var controllerContext = GetControllerContext(actionDescriptor);
var controller = new TestController();
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var binder = new StubModelBinder(bindingContext =>
{
// BindingContext.ModelName will be string.Empty here. This is a 'fallback to empty prefix'
// because the value providers have no data.
if (inputPropertyValues.TryGetValue(bindingContext.FieldName, out var model))
{
bindingContext.Result = ModelBindingResult.Success(model);
}
else
{
bindingContext.Result = ModelBindingResult.Failed();
}
});
var factory = GetModelBinderFactory(binder);
controllerContext.ValueProviderFactories.Add(new SimpleValueProviderFactory());
var modelMetadataProvider = TestModelMetadataProvider.CreateDefaultProvider();
var parameterBinder = GetParameterBinder(
modelMetadataProvider,
factory);
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
modelMetadataProvider,
actionDescriptor,
_options);
await binderDelegate(controllerContext, controller, arguments);
// Assert
Assert.Equal(new string[] { "goodbye" }, controller.ArrayProperty); // Skipped
Assert.Equal(new List<string> { "hello", "world" }, controller.CollectionProperty);
Assert.Equal(new Person { Name = "Ginger" }, controller.NonCollectionProperty); // Skipped
Assert.Null(controller.NullCollectionProperty); // Skipped
Assert.Null(controller.UntouchedProperty); // Not bound
Assert.Equal("Hello", controller.StringProperty);
}
private class TransferInfo
{
[Range(25, 50)]
public int AccountId { get; set; }
public double Amount { get; set; }
}
public static TheoryData<List<ParameterDescriptor>> MultipleActionParametersAndValidationData
{
get
{
return new TheoryData<List<ParameterDescriptor>>
{
// Irrespective of the order in which the parameters are defined on the action,
// the validation on the TransferInfo's AccountId should occur.
// Here 'accountId' parameter is bound by the prefix 'accountId' while the 'transferInfo'
// property is bound using the empty prefix and the 'TransferInfo' property names.
new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "accountId",
ParameterType = typeof(int)
},
new ParameterDescriptor()
{
Name = "transferInfo",
ParameterType = typeof(TransferInfo),
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Body
}
}
},
new List<ParameterDescriptor>()
{
new ParameterDescriptor()
{
Name = "transferInfo",
ParameterType = typeof(TransferInfo),
BindingInfo = new BindingInfo()
{
BindingSource = BindingSource.Body
}
},
new ParameterDescriptor()
{
Name = "accountId",
ParameterType = typeof(int)
}
}
};
}
}
[Theory]
[MemberData(nameof(MultipleActionParametersAndValidationData))]
public async Task MultipleActionParameter_ValidModelState(List<ParameterDescriptor> parameters)
{
// Since validation attribute is only present on the FromBody model's property(TransferInfo's AccountId),
// validation should not trigger for the parameter which is bound from Uri.
// Arrange
var actionDescriptor = new ControllerActionDescriptor()
{
BoundProperties = new List<ParameterDescriptor>(),
Parameters = parameters
};
var modelMetadataProvider = new EmptyModelMetadataProvider();
var modelBinderProvider = new BodyModelBinderProvider(new[] { Mock.Of<IInputFormatter>() }, Mock.Of<IHttpRequestStreamReaderFactory>());
var factory = TestModelBinderFactory.CreateDefault(modelBinderProvider);
var modelValidatorProvider = new Mock<IModelValidatorProvider>(MockBehavior.Strict).Object;
var parameterBinder = new Mock<ParameterBinder>(
new EmptyModelMetadataProvider(),
factory,
GetObjectValidator(modelMetadataProvider, modelValidatorProvider),
_optionsAccessor,
NullLoggerFactory.Instance);
parameterBinder.Setup(p => p.BindModelAsync(
It.IsAny<ActionContext>(),
It.IsAny<IModelBinder>(),
It.IsAny<IValueProvider>(),
It.IsAny<ParameterDescriptor>(),
It.IsAny<ModelMetadata>(),
null,
null))
.Returns((ActionContext context, IModelBinder modelBinder, IValueProvider valueProvider, ParameterDescriptor descriptor, ModelMetadata metadata, object v, object c) =>
{
ModelBindingResult result;
if (descriptor.Name == "accountId")
{
result = ModelBindingResult.Success(10);
}
else if (descriptor.Name == "transferInfo")
{
result = ModelBindingResult.Success(new TransferInfo
{
AccountId = 40,
Amount = 250.0
});
}
else
{
result = ModelBindingResult.Failed();
}
return new ValueTask<ModelBindingResult>(result);
});
var controllerContext = GetControllerContext(actionDescriptor);
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var modelState = controllerContext.ModelState;
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder.Object,
factory,
TestModelMetadataProvider.CreateDefaultProvider(),
actionDescriptor,
_options);
await binderDelegate(controllerContext, new TestController(), arguments);
// Assert
Assert.True(modelState.IsValid);
Assert.True(arguments.TryGetValue("accountId", out var value));
var accountId = Assert.IsType<int>(value);
Assert.Equal(10, accountId);
Assert.True(arguments.TryGetValue("transferInfo", out value));
var transferInfo = Assert.IsType<TransferInfo>(value);
Assert.NotNull(transferInfo);
Assert.Equal(40, transferInfo.AccountId);
Assert.Equal(250.0, transferInfo.Amount);
}
[Fact]
public async Task BinderDelegateRecordsErrorWhenValueProviderThrowsValueProviderException()
{
// Arrange
var actionDescriptor = new ControllerActionDescriptor()
{
BoundProperties = new List<ParameterDescriptor>(),
Parameters = new[] { new ParameterDescriptor { Name = "name", ParameterType = typeof(string) } },
};
var modelMetadataProvider = new EmptyModelMetadataProvider();
var modelBinderProvider = Mock.Of<IModelBinderProvider>();
var factory = TestModelBinderFactory.CreateDefault(modelBinderProvider);
var modelValidatorProvider = Mock.Of<IModelValidatorProvider>();
var parameterBinder = new ParameterBinder(
new EmptyModelMetadataProvider(),
factory,
GetObjectValidator(modelMetadataProvider, modelValidatorProvider),
_optionsAccessor,
NullLoggerFactory.Instance);
var valueProviderFactory = new Mock<IValueProviderFactory>();
valueProviderFactory.Setup(f => f.CreateValueProviderAsync(It.IsAny<ValueProviderFactoryContext>()))
.Throws(new ValueProviderException("Some error"));
var controllerContext = GetControllerContext(actionDescriptor);
controllerContext.ValueProviderFactories.Add(valueProviderFactory.Object);
var arguments = new Dictionary<string, object>(StringComparer.Ordinal);
var modelState = controllerContext.ModelState;
// Act
var binderDelegate = ControllerBinderDelegateProvider.CreateBinderDelegate(
parameterBinder,
factory,
TestModelMetadataProvider.CreateDefaultProvider(),
actionDescriptor,
_options);
await binderDelegate(controllerContext, new TestController(), arguments);
// Assert
var entry = Assert.Single(modelState);
Assert.Empty(entry.Key);
var error = Assert.Single(entry.Value.Errors);
Assert.Equal("Some error", error.ErrorMessage);
}
private static ControllerContext GetControllerContext(ControllerActionDescriptor descriptor = null)
{
var services = new ServiceCollection();
services.AddSingleton<ILoggerFactory>(NullLoggerFactory.Instance);
var context = new ControllerContext()
{
ActionDescriptor = descriptor ?? GetActionDescriptor(),
HttpContext = new DefaultHttpContext()
{
RequestServices = services.BuildServiceProvider()
},
RouteData = new RouteData(),
};
context.ValueProviderFactories.Add(new SimpleValueProviderFactory());
return context;
}
private static ControllerActionDescriptor GetActionDescriptor()
{
Func<object, int> method = foo => 1;
return new ControllerActionDescriptor
{
MethodInfo = method.GetMethodInfo(),
ControllerTypeInfo = typeof(TestController).GetTypeInfo(),
BoundProperties = new List<ParameterDescriptor>(),
Parameters = new List<ParameterDescriptor>()
};
}
private static IModelValidatorProvider GetModelValidatorProvider(IModelValidator validator = null)
{
if (validator == null)
{
validator = Mock.Of<IModelValidator>();
}
var validatorProvider = new Mock<IModelValidatorProvider>();
validatorProvider
.Setup(p => p.CreateValidators(It.IsAny<ModelValidatorProviderContext>()))
.Callback<ModelValidatorProviderContext>(context =>
{
foreach (var result in context.Results)
{
result.Validator = validator;
result.IsReusable = true;
}
});
return validatorProvider.Object;
}
private static ModelBinderFactory GetModelBinderFactory(object model = null)
{
var binder = new Mock<IModelBinder>();
binder
.Setup(b => b.BindModelAsync(It.IsAny<DefaultModelBindingContext>()))
.Returns<DefaultModelBindingContext>(mbc =>
{
mbc.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
});
return GetModelBinderFactory(binder.Object);
}
private static ModelBinderFactory GetModelBinderFactory(IModelBinder binder)
{
var provider = new Mock<IModelBinderProvider>();
provider
.Setup(p => p.GetBinder(It.IsAny<ModelBinderProviderContext>()))
.Returns(binder);
return TestModelBinderFactory.Create(provider.Object);
}
private static ParameterBinder GetParameterBinder(
IModelMetadataProvider modelMetadataProvider = null,
IModelBinderFactory factory = null,
IModelValidatorProvider modelValidatorProvider = null)
{
if (factory == null)
{
factory = TestModelBinderFactory.CreateDefault();
}
if (modelValidatorProvider == null)
{
modelValidatorProvider = Mock.Of<IModelValidatorProvider>();
}
var metadataProvider = modelMetadataProvider ?? TestModelMetadataProvider.CreateDefaultProvider();
var objectModelValidator = GetObjectValidator(modelMetadataProvider, modelValidatorProvider);
return new ParameterBinder(
metadataProvider,
factory,
objectModelValidator,
_optionsAccessor,
NullLoggerFactory.Instance);
}
private static DefaultObjectValidator GetObjectValidator(
IModelMetadataProvider modelMetadataProvider,
IModelValidatorProvider validatorProvider)
{
return new DefaultObjectValidator(
modelMetadataProvider,
new[] { validatorProvider },
_options);
}
// No need for bind-related attributes on properties in this controller class. Properties are added directly
// to the BoundProperties collection, bypassing usual requirements.
private class TestController
{
public string UntouchedProperty { get; set; }
public string[] ArrayProperty { get; } = new string[] { "goodbye" };
public ICollection<string> CollectionProperty { get; } = new List<string> { "goodbye" };
public Person NonCollectionProperty { get; } = new Person { Name = "Ginger" };
public ICollection<string> NullCollectionProperty { get; private set; }
public string StringProperty { get; set; }
public int NonNullableProperty { get; set; }
public int? NullableProperty { get; set; }
[CustomValidation("Test message")] public string ValidatedProperty { get; set; }
// Despite being "required", the BindNever means this property won't be involved
// in binding, so no validation will be performed
[Required, BindNever] public string RequiredButBindNeverProperty { get; set; }
}
private class Person : IEquatable<Person>, IEquatable<object>
{
public string Name { get; set; }
public bool Equals(Person other)
{
return other != null && string.Equals(Name, other.Name, StringComparison.Ordinal);
}
bool IEquatable<object>.Equals(object obj)
{
return Equals(obj as Person);
}
}
private class CustomBindingSourceAttribute : Attribute, IBindingSourceMetadata
{
public BindingSource BindingSource { get { return BindingSource.Custom; } }
}
private class ValueProviderMetadataAttribute : Attribute, IBindingSourceMetadata
{
public BindingSource BindingSource { get { return BindingSource.Query; } }
}
private class CustomValidationAttribute : Attribute, IModelValidator
{
public string Message { get; }
public CustomValidationAttribute(string message)
{
Message = message;
}
public IEnumerable<ModelValidationResult> Validate(ModelValidationContext context)
{
yield return new ModelValidationResult(context.ModelMetadata.BinderModelName, Message);
}
}
private class ParameterInfos
{
public void Method(
object param1,
[BindNever] object param2,
[CustomValidation("some message")] string param3)
{
}
public static ParameterInfo NoAttributesParameterInfo
= typeof(ParameterInfos)
.GetMethod(nameof(ParameterInfos.Method))
.GetParameters()[0];
public static ParameterInfo BindNeverParameterInfo
= typeof(ParameterInfos)
.GetMethod(nameof(ParameterInfos.Method))
.GetParameters()[1];
public static ParameterInfo CustomValidationParameterInfo
= typeof(ParameterInfos)
.GetMethod(nameof(ParameterInfos.Method))
.GetParameters()[2];
}
public abstract class FakeModelMetadata : ModelMetadata
{
public FakeModelMetadata()
: base(ModelMetadataIdentity.ForType(typeof(string)))
{
}
}
private class TestObjectModelValidator : IObjectModelValidator
{
public void Validate(
ActionContext actionContext,
ValidationStateDictionary validationState,
string prefix,
object model)
{
throw new NotImplementedException();
}
}
}
|