Fail more gracefully when option collections cleared

- #4690
- move `ModelBindingMessageProvider` init from `DefaultBindingMetadataProvider` to `DefaultModelMetadata`
 - in addition to avoiding error cases, this removes some boilerplate
- add specific errors to `BodyModelBinderProvider`, `CompilerCache`, `CompositeViewEngine`, `ModelBinderFactory`,
  and `ObjectResultExecutor`
 - `DefaultRazorViewEngineFileProviderAccessor.FileProvider` now a `NullFileProvider` in empty case
This commit is contained in:
Doug Bunting 2016-06-29 12:58:01 -07:00
parent a852352223
commit 42cea41737
29 changed files with 515 additions and 306 deletions

View File

@ -15,19 +15,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
/// </summary>
public class DefaultBindingMetadataProvider : IBindingMetadataProvider
{
private readonly ModelBindingMessageProvider _messageProvider;
public DefaultBindingMetadataProvider(ModelBindingMessageProvider messageProvider)
{
if (messageProvider == null)
{
throw new ArgumentNullException(nameof(messageProvider));
}
_messageProvider = messageProvider;
}
/// <inheritdoc />
public void CreateBindingMetadata(BindingMetadataProviderContext context)
{
if (context == null)
@ -65,10 +52,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
// ModelBindingMessageProvider
// Provide a unique instance based on one passed to the constructor.
context.BindingMetadata.ModelBindingMessageProvider = new ModelBindingMessageProvider(_messageProvider);
// PropertyFilterProvider
var propertyFilterProviders = context.Attributes.OfType<IPropertyFilterProvider>().ToArray();
if (propertyFilterProviders.Length == 0)

View File

@ -4,12 +4,10 @@
using System;
using System.Threading;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
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;
namespace Microsoft.AspNetCore.Mvc.Internal
@ -33,16 +31,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public void Configure(MvcOptions options)
{
// Set up default error messages
var messageProvider = options.ModelBindingMessageProvider;
messageProvider.MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember;
messageProvider.MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent;
messageProvider.ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid;
messageProvider.AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid;
messageProvider.UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid;
messageProvider.ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid;
messageProvider.ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber;
// Set up ModelBinding
options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider());
options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
@ -79,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// by altering the collection of providers.
options.ModelMetadataDetailsProviders.Add(new ExcludeBindingMetadataProvider(typeof(Type)));
options.ModelMetadataDetailsProviders.Add(new DefaultBindingMetadataProvider(messageProvider));
options.ModelMetadataDetailsProviders.Add(new DefaultBindingMetadataProvider());
options.ModelMetadataDetailsProviders.Add(new DefaultValidationMetadataProvider());
// Set up validators

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
@ -117,6 +118,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (formatters == null || formatters.Count == 0)
{
formatters = OptionsFormatters;
// Complain about MvcOptions.OutputFormatters only if the result has an empty Formatters.
Debug.Assert(formatters != null, "MvcOptions.OutputFormatters cannot be null.");
if (formatters.Count == 0)
{
throw new InvalidOperationException(Resources.FormatOutputFormattersAreRequired(
typeof(MvcOptions).FullName,
nameof(MvcOptions.OutputFormatters),
typeof(IOutputFormatter).FullName));
}
}
var objectType = result.DeclaredType;

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal;
@ -48,6 +49,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
if (context.BindingInfo.BindingSource != null &&
context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body))
{
if (_formatters.Count == 0)
{
throw new InvalidOperationException(Resources.FormatInputFormattersAreRequired(
typeof(MvcOptions).FullName,
nameof(MvcOptions.InputFormatters),
typeof(IInputFormatter).FullName));
}
return new BodyModelBinder(_formatters, _readerFactory);
}

View File

@ -1,52 +1,25 @@
// 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 Microsoft.AspNetCore.Mvc.Core;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
public class EmptyModelMetadataProvider : DefaultModelMetadataProvider
{
public EmptyModelMetadataProvider()
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
{
new MessageOnlyBindingProvider()
}))
: base(
new DefaultCompositeMetadataDetailsProvider(new List<IMetadataDetailsProvider>()),
new OptionsAccessor())
{
}
private class MessageOnlyBindingProvider : IBindingMetadataProvider
private class OptionsAccessor : IOptions<MvcOptions>
{
private readonly ModelBindingMessageProvider _messageProvider = CreateMessageProvider();
public void CreateBindingMetadata(BindingMetadataProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Don't bother with ModelBindingMessageProvider copy constructor. No other provider can change the
// delegates.
context.BindingMetadata.ModelBindingMessageProvider = _messageProvider;
}
private static ModelBindingMessageProvider CreateMessageProvider()
{
return new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember,
MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent,
ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid,
AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid,
UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid,
ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid,
ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber,
};
}
public MvcOptions Value { get; } = new MvcOptions();
}
}
}

View File

@ -17,6 +17,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private readonly ICompositeMetadataDetailsProvider _detailsProvider;
private readonly DefaultMetadataDetails _details;
// Default message provider for all DefaultModelMetadata instances; cloned before exposing to
// IBindingMetadataProvider instances to ensure customizations are not accidentally shared.
private readonly ModelBindingMessageProvider _modelBindingMessageProvider;
private ReadOnlyDictionary<object, object> _additionalValues;
private ModelMetadata _elementMetadata;
private bool? _isBindingRequired;
@ -36,6 +40,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
IModelMetadataProvider provider,
ICompositeMetadataDetailsProvider detailsProvider,
DefaultMetadataDetails details)
: this(provider, detailsProvider, details, new ModelBindingMessageProvider())
{
}
/// <summary>
/// Creates a new <see cref="DefaultModelMetadata"/>.
/// </summary>
/// <param name="provider">The <see cref="IModelMetadataProvider"/>.</param>
/// <param name="detailsProvider">The <see cref="ICompositeMetadataDetailsProvider"/>.</param>
/// <param name="details">The <see cref="DefaultMetadataDetails"/>.</param>
/// <param name="modelBindingMessageProvider">The <see cref="Metadata.ModelBindingMessageProvider"/>.</param>
public DefaultModelMetadata(
IModelMetadataProvider provider,
ICompositeMetadataDetailsProvider detailsProvider,
DefaultMetadataDetails details,
ModelBindingMessageProvider modelBindingMessageProvider)
: base(details.Key)
{
if (provider == null)
@ -53,9 +73,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
throw new ArgumentNullException(nameof(details));
}
if (modelBindingMessageProvider == null)
{
throw new ArgumentNullException(nameof(modelBindingMessageProvider));
}
_provider = provider;
_detailsProvider = detailsProvider;
_details = details;
_modelBindingMessageProvider = modelBindingMessageProvider;
}
/// <summary>
@ -82,6 +108,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
if (_details.BindingMetadata == null)
{
var context = new BindingMetadataProviderContext(Identity, _details.ModelAttributes);
// Provide a unique ModelBindingMessageProvider instance so providers' customizations are per-type.
context.BindingMetadata.ModelBindingMessageProvider =
new ModelBindingMessageProvider(_modelBindingMessageProvider);
_detailsProvider.CreateBindingMetadata(context);
_details.BindingMetadata = context.BindingMetadata;
}

View File

@ -6,6 +6,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Reflection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
@ -23,11 +24,35 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary>
/// <param name="detailsProvider">The <see cref="ICompositeMetadataDetailsProvider"/>.</param>
public DefaultModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider)
: this(detailsProvider, new ModelBindingMessageProvider())
{
}
/// <summary>
/// Creates a new <see cref="DefaultModelMetadataProvider"/>.
/// </summary>
/// <param name="detailsProvider">The <see cref="ICompositeMetadataDetailsProvider"/>.</param>
/// <param name="optionsAccessor">The accessor for <see cref="MvcOptions"/>.</param>
public DefaultModelMetadataProvider(
ICompositeMetadataDetailsProvider detailsProvider,
IOptions<MvcOptions> optionsAccessor)
: this(detailsProvider, GetMessageProvider(optionsAccessor))
{
}
private DefaultModelMetadataProvider(
ICompositeMetadataDetailsProvider detailsProvider,
ModelBindingMessageProvider modelBindingMessageProvider)
{
if (detailsProvider == null)
{
throw new ArgumentNullException(nameof(detailsProvider));
}
DetailsProvider = detailsProvider;
ModelBindingMessageProvider = modelBindingMessageProvider;
_cacheEntryFactory = CreateCacheEntry;
_metadataCacheEntryForObjectType = GetMetadataCacheEntryForObjectType();
}
@ -36,6 +61,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary>
protected ICompositeMetadataDetailsProvider DetailsProvider { get; }
/// <summary>
/// Gets the <see cref="Metadata.ModelBindingMessageProvider"/>.
/// </summary>
/// <value>Same as <see cref="MvcOptions.ModelBindingMessageProvider"/> in all production scenarios.</value>
protected ModelBindingMessageProvider ModelBindingMessageProvider { get; }
/// <inheritdoc />
public virtual IEnumerable<ModelMetadata> GetMetadataForProperties(Type modelType)
{
@ -78,6 +109,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return cacheEntry.Metadata;
}
private static ModelBindingMessageProvider GetMessageProvider(IOptions<MvcOptions> optionsAccessor)
{
if (optionsAccessor == null)
{
throw new ArgumentNullException(nameof(optionsAccessor));
}
return optionsAccessor.Value.ModelBindingMessageProvider;
}
private ModelMetadataCacheEntry GetCacheEntry(Type modelType)
{
ModelMetadataCacheEntry cacheEntry;
@ -123,7 +164,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </remarks>
protected virtual ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry)
{
return new DefaultModelMetadata(this, DetailsProvider, entry);
return new DefaultModelMetadata(this, DetailsProvider, entry, ModelBindingMessageProvider);
}
/// <summary>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.Core;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
@ -23,6 +24,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary>
public ModelBindingMessageProvider()
{
MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember;
MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent;
ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid;
AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid;
UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid;
ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid;
ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber;
}
/// <summary>

View File

@ -46,6 +46,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(context));
}
if (_providers.Length == 0)
{
throw new InvalidOperationException(Resources.FormatModelBinderProvidersAreRequired(
typeof(MvcOptions).FullName,
nameof(MvcOptions.ModelBinderProviders),
typeof(IModelBinderProvider).FullName));
}
IModelBinder binder;
if (TryGetCachedBinder(context.Metadata, context.CacheToken, out binder))
{

View File

@ -1162,6 +1162,54 @@ namespace Microsoft.AspNetCore.Mvc.Core
return string.Format(CultureInfo.CurrentCulture, GetString("CouldNotCreateIModelBinder"), p0);
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to bind from the body.
/// </summary>
internal static string InputFormattersAreRequired
{
get { return GetString("InputFormattersAreRequired"); }
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to bind from the body.
/// </summary>
internal static string FormatInputFormattersAreRequired(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("InputFormattersAreRequired"), p0, p1, p2);
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to model bind.
/// </summary>
internal static string ModelBinderProvidersAreRequired
{
get { return GetString("ModelBinderProvidersAreRequired"); }
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to model bind.
/// </summary>
internal static string FormatModelBinderProvidersAreRequired(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderProvidersAreRequired"), p0, p1, p2);
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to format a response.
/// </summary>
internal static string OutputFormattersAreRequired
{
get { return GetString("OutputFormattersAreRequired"); }
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to format a response.
/// </summary>
internal static string FormatOutputFormattersAreRequired(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("OutputFormattersAreRequired"), p0, p1, p2);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -343,4 +343,13 @@
<data name="CouldNotCreateIModelBinder" xml:space="preserve">
<value>Could not create a model binder for model object of type '{0}'.</value>
</data>
<data name="InputFormattersAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to bind from the body.</value>
</data>
<data name="ModelBinderProvidersAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to model bind.</value>
</data>
<data name="OutputFormattersAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to format a response.</value>
</data>
</root>

View File

@ -116,6 +116,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
return cacheEntry;
}
if (_fileProvider is NullFileProvider)
{
var message = Resources.FormatFileProvidersAreRequired(
typeof(RazorViewEngineOptions).FullName,
nameof(RazorViewEngineOptions.FileProviders),
typeof(IFileProvider).FullName);
throw new InvalidOperationException(message);
}
fileInfo = _fileProvider.GetFileInfo(normalizedPath);
if (!fileInfo.Exists)
{

View File

@ -18,7 +18,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
public DefaultRazorViewEngineFileProviderAccessor(IOptions<RazorViewEngineOptions> optionsAccessor)
{
var fileProviders = optionsAccessor.Value.FileProviders;
if (fileProviders.Count == 1)
if (fileProviders.Count == 0)
{
FileProvider = new NullFileProvider();
}
else if (fileProviders.Count == 1)
{
FileProvider = fileProviders[0];
}

View File

@ -494,6 +494,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return GetString("RazorPage_NestingAttributeWritingScopesNotSupported");
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.
/// </summary>
internal static string FileProvidersAreRequired
{
get { return GetString("FileProvidersAreRequired"); }
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.
/// </summary>
internal static string FormatFileProvidersAreRequired(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("FileProvidersAreRequired"), p0, p1, p2);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -209,4 +209,7 @@
<data name="RazorPage_NestingAttributeWritingScopesNotSupported" xml:space="preserve">
<value>Nesting of TagHelper attribute writing scopes is not supported.</value>
</data>
<data name="FileProvidersAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.</value>
</data>
</root>

View File

@ -890,6 +890,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return string.Format(CultureInfo.CurrentCulture, GetString("CreateModelExpression_NullModelMetadata"), p0, p1);
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.
/// </summary>
internal static string ViewEnginesAreRequired
{
get { return GetString("ViewEnginesAreRequired"); }
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.
/// </summary>
internal static string FormatViewEnginesAreRequired(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ViewEnginesAreRequired"), p0, p1, p2);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -283,4 +283,7 @@
<data name="CreateModelExpression_NullModelMetadata" xml:space="preserve">
<value>The {0} was unable to provide metadata for expression '{1}'.</value>
</data>
<data name="ViewEnginesAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.</value>
</data>
</root>

View File

@ -37,6 +37,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName));
}
if (ViewEngines.Count == 0)
{
throw new InvalidOperationException(Resources.FormatViewEnginesAreRequired(
typeof(MvcViewOptions).FullName,
nameof(MvcViewOptions.ViewEngines),
typeof(IViewEngine).FullName));
}
// Do not allocate in the common cases: ViewEngines contains one entry or initial attempt is successful.
IEnumerable<string> searchedLocations = null;
List<string> searchedList = null;
@ -77,6 +85,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewPath));
}
if (ViewEngines.Count == 0)
{
throw new InvalidOperationException(Resources.FormatViewEnginesAreRequired(
typeof(MvcViewOptions).FullName,
nameof(MvcViewOptions.ViewEngines),
typeof(IViewEngine).FullName));
}
// Do not allocate in the common cases: ViewEngines contains one entry or initial attempt is successful.
IEnumerable<string> searchedLocations = null;
List<string> searchedList = null;

View File

@ -2,9 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
@ -887,20 +887,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var expected = "Hmm, the supplied value is not valid for Length.";
var dictionary = new ModelStateDictionary();
var messageProvider = new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor = name => "Unexpected MissingBindRequiredValueAccessor use",
MissingKeyOrValueAccessor = () => "Unexpected MissingKeyOrValueAccessor use",
ValueMustNotBeNullAccessor = value => "Unexpected ValueMustNotBeNullAccessor use",
AttemptedValueIsInvalidAccessor =
(value, name) => "Unexpected InvalidValueWithKnownAttemptedValueAccessor use",
UnknownValueIsInvalidAccessor = name => $"Hmm, the supplied value is not valid for { name }.",
ValueIsInvalidAccessor = value => "Unexpected InvalidValueWithUnknownModelErrorAccessor use",
ValueMustBeANumberAccessor = name => "Unexpected ValueMustBeANumberAccessor use",
};
var bindingMetadataProvider = new DefaultBindingMetadataProvider(messageProvider);
var bindingMetadataProvider = new DefaultBindingMetadataProvider();
var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
var provider = new DefaultModelMetadataProvider(compositeProvider);
var optionsAccessor = new OptionsAccessor();
optionsAccessor.Value.ModelBindingMessageProvider.UnknownValueIsInvalidAccessor =
name => $"Hmm, the supplied value is not valid for { name }.";
var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor);
var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));
// Act
@ -939,20 +932,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var dictionary = new ModelStateDictionary();
dictionary.SetModelValue("key", new string[] { "some value" }, "some value");
var messageProvider = new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor = name => "Unexpected MissingBindRequiredValueAccessor use",
MissingKeyOrValueAccessor = () => "Unexpected MissingKeyOrValueAccessor use",
ValueMustNotBeNullAccessor = value => "Unexpected ValueMustNotBeNullAccessor use",
AttemptedValueIsInvalidAccessor =
(value, name) => $"Hmm, the value '{ value }' is not valid for { name }.",
UnknownValueIsInvalidAccessor = name => "Unexpected InvalidValueWithUnknownAttemptedValueAccessor use",
ValueIsInvalidAccessor = value => "Unexpected InvalidValueWithUnknownModelErrorAccessor use",
ValueMustBeANumberAccessor = name => "Unexpected ValueMustBeANumberAccessor use",
};
var bindingMetadataProvider = new DefaultBindingMetadataProvider(messageProvider);
var bindingMetadataProvider = new DefaultBindingMetadataProvider();
var compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider });
var provider = new DefaultModelMetadataProvider(compositeProvider);
var optionsAccessor = new OptionsAccessor();
optionsAccessor.Value.ModelBindingMessageProvider.AttemptedValueIsInvalidAccessor =
(value, name) => $"Hmm, the value '{ value }' is not valid for { name }.";
var provider = new DefaultModelMetadataProvider(compositeProvider, optionsAccessor);
var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));
// Act
@ -1303,5 +1289,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert
Assert.Equal("value1", property.RawValue);
}
private class OptionsAccessor : IOptions<MvcOptions>
{
public MvcOptions Value { get; } = new MvcOptions();
}
}
}

View File

@ -393,6 +393,25 @@ namespace Microsoft.AspNetCore.Mvc.Internal
actionContext.HttpContext.Response.Headers[HeaderNames.ContentType]);
}
[Fact]
public async Task ExecuteAsync_ThrowsWithNoFormatters()
{
// Arrange
var expected = $"'{typeof(MvcOptions).FullName}.{nameof(MvcOptions.OutputFormatters)}' must not be " +
$"empty. At least one '{typeof(IOutputFormatter).FullName}' is required to format a response.";
var executor = CreateExecutor();
var actionContext = new ActionContext
{
HttpContext = GetHttpContext(),
};
var result = new ObjectResult("some value");
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => executor.ExecuteAsync(actionContext, result));
Assert.Equal(expected, exception.Message);
}
[Theory]
[InlineData(new[] { "application/*" }, "application/*")]
[InlineData(new[] { "application/xml", "application/*", "application/json" }, "application/*")]

View File

@ -1,7 +1,6 @@
// 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 Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
@ -25,7 +24,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -49,7 +48,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -72,7 +71,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -96,7 +95,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -119,7 +118,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -143,7 +142,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForType(typeof(string)),
new ModelAttributes(attributes));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -165,7 +164,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -188,7 +187,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -211,7 +210,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -234,7 +233,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -257,7 +256,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -283,7 +282,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(int), "Length", typeof(string)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -301,7 +300,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindRequiredOnClass)),
new ModelAttributes(propertyAttributes: new object[0], typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -319,7 +318,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindNeverOnClass)),
new ModelAttributes(propertyAttributes: new object[0], typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -337,7 +336,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(InheritedBindNeverOnClass)),
new ModelAttributes(propertyAttributes: new object[0], typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -360,7 +359,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindNeverOnClass)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -383,7 +382,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindNeverOnClass)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -406,7 +405,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(InheritedBindNeverOnClass)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -429,7 +428,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindRequiredOnClass)),
new ModelAttributes(propertyAttributes, typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -448,7 +447,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
ModelMetadataIdentity.ForProperty(typeof(string), "Property", typeof(BindRequiredOverridesInheritedBindNever)),
new ModelAttributes(propertyAttributes: new object[0], typeAttributes: new object[0]));
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -477,7 +476,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
context.BindingMetadata.IsBindingAllowed = initialValue;
context.BindingMetadata.IsBindingRequired = initialValue;
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -508,7 +507,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
context.BindingMetadata.IsBindingAllowed = initialValue;
context.BindingMetadata.IsBindingRequired = initialValue;
var provider = new DefaultBindingMetadataProvider(CreateMessageProvider());
var provider = new DefaultBindingMetadataProvider();
// Act
provider.CreateBindingMetadata(context);
@ -518,20 +517,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
Assert.Equal(initialValue, context.BindingMetadata.IsBindingRequired);
}
private static ModelBindingMessageProvider CreateMessageProvider()
{
return new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember,
MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent,
ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid,
AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid,
UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid,
ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid,
ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber,
};
}
[BindNever]
private class BindNeverOnClass
{

View File

@ -1,7 +1,9 @@
// 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.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters;
using Xunit;
@ -24,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
[Theory]
[MemberData(nameof(NonBodyBindingSources))]
public void Create_WhenBindingSourceIsNotFromBody_ReturnsNull(BindingSource source)
public void GetBinder_WhenBindingSourceIsNotFromBody_ReturnsNull(BindingSource source)
{
// Arrange
var provider = CreateProvider();
@ -39,12 +41,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Null(result);
}
[Fact]
public void Create_WhenBindingSourceIsFromBody_ReturnsBinder()
public void GetBinder_WhenNoInputFormatters_Throws()
{
// Arrange
var expected = $"'{typeof(MvcOptions).FullName}.{nameof(MvcOptions.InputFormatters)}' must not be empty. " +
$"At least one '{typeof(IInputFormatter).FullName}' is required to bind from the body.";
var provider = CreateProvider();
var context = new TestModelBinderProviderContext(typeof(Person));
context.BindingInfo.BindingSource = BindingSource.Body;
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => provider.GetBinder(context));
Assert.Equal(expected, exception.Message);
}
[Fact]
public void GetBinder_WhenBindingSourceIsFromBody_ReturnsBinder()
{
// Arrange
var provider = CreateProvider(new TestInputFormatter());
var context = new TestModelBinderProviderContext(typeof(Person));
context.BindingInfo.BindingSource = BindingSource.Body;
@ -55,9 +70,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType<BodyModelBinder>(result);
}
private static BodyModelBinderProvider CreateProvider()
private static BodyModelBinderProvider CreateProvider(params IInputFormatter[] formatters)
{
return new BodyModelBinderProvider(new List<IInputFormatter>(), new TestHttpRequestStreamReaderFactory());
return new BodyModelBinderProvider(
new List<IInputFormatter>(formatters),
new TestHttpRequestStreamReaderFactory());
}
private class Person
@ -66,5 +83,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
public int Age { get; set; }
}
private class TestInputFormatter : IInputFormatter
{
public bool CanRead(InputFormatterContext context)
{
throw new NotImplementedException();
}
public Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
{
throw new NotImplementedException();
}
}
}
}

View File

@ -3,7 +3,6 @@
using System;
using System.Linq;
using System.Reflection;
using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@ -173,7 +172,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private static DefaultModelMetadataProvider CreateProvider()
{
return new DefaultModelMetadataProvider(new EmptyCompositeMetadataDetailsProvider());
return new DefaultModelMetadataProvider(
new EmptyCompositeMetadataDetailsProvider(),
new TestOptionsManager<MvcOptions>());
}
[Model("OnType")]

View File

@ -2,10 +2,8 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Moq;
using Xunit;
@ -14,24 +12,41 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
public class ModelBinderFactoryTest
{
// No providers => can't create a binder
[Fact]
public void CreateBinder_Throws_WhenNoProviders()
{
// Arrange
var expected = $"'{typeof(MvcOptions).FullName}.{nameof(MvcOptions.ModelBinderProviders)}' must not be " +
$"empty. At least one '{typeof(IModelBinderProvider).FullName}' is required to model bind.";
var metadataProvider = new TestModelMetadataProvider();
var options = new TestOptionsManager<MvcOptions>();
var factory = new ModelBinderFactory(metadataProvider, options);
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(string)),
};
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => factory.CreateBinder(context));
Assert.Equal(expected, exception.Message);
}
[Fact]
public void CreateBinder_Throws_WhenBinderNotCreated()
{
// Arrange
var metadataProvider = new TestModelMetadataProvider();
var options = new TestOptionsManager<MvcOptions>();
var factory = new ModelBinderFactory(metadataProvider, options);
options.Value.ModelBinderProviders.Add(new TestModelBinderProvider(_ => null));
var factory = new ModelBinderFactory(metadataProvider, options);
var context = new ModelBinderFactoryContext()
{
Metadata = metadataProvider.GetMetadataForType(typeof(string)),
};
// Act
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => factory.CreateBinder(context));
// Assert
Assert.Equal(
$"Could not create a model binder for model object of type '{typeof(string).FullName}'.",
exception.Message);

View File

@ -1046,31 +1046,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
private readonly object[] _attributes;
public AttributeInjectModelMetadataProvider(object[] attributes)
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
{
new DefaultBindingMetadataProvider(CreateMessageProvider()),
new DataAnnotationsMetadataProvider(),
}))
: base(
new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
{
new DefaultBindingMetadataProvider(),
new DataAnnotationsMetadataProvider(),
}),
new TestOptionsManager<MvcOptions>())
{
_attributes = attributes;
}
private static ModelBindingMessageProvider CreateMessageProvider()
{
return new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor =
name => $"A value for the '{ name }' property was not provided.",
MissingKeyOrValueAccessor = () => $"A value is required.",
ValueMustNotBeNullAccessor = value => $"The value '{ value }' is invalid.",
AttemptedValueIsInvalidAccessor =
(value, name) => $"The value '{ value }' is not valid for { name }.",
UnknownValueIsInvalidAccessor = name => $"The supplied value is invalid for { name }.",
ValueIsInvalidAccessor = value => $"The value '{ value }' is invalid.",
ValueMustBeANumberAccessor = name => $"The field { name } must be a number.",
};
}
protected override DefaultMetadataDetails CreateTypeDetails(ModelMetadataIdentity key)
{
var entry = base.CreateTypeDetails(key);

View File

@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.Extensions.FileProviders;
using Moq;
using Xunit;
@ -445,6 +446,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
Assert.Same(result1.PageFactory, result2.PageFactory);
}
[Fact]
public void GetOrAdd_ThrowsIfNullFileProvider()
{
// Arrange
var expected =
$"'{typeof(RazorViewEngineOptions).FullName}.{nameof(RazorViewEngineOptions.FileProviders)}' must " +
$"not be empty. At least one '{typeof(IFileProvider).FullName}' is required to locate a view for " +
"rendering.";
var fileProvider = new NullFileProvider();
var cache = new CompilerCache(fileProvider);
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(
() => cache.GetOrAdd(ViewPath, _ => { throw new InvalidTimeZoneException(); }));
Assert.Equal(expected, exception.Message);
}
[Fact]
public void GetOrAdd_CachesCompilationExceptions()
{

View File

@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
public class DefaultRazorViewEngineFileProviderAccessorTest
{
[Fact]
public void FileProvider_ReturnsInstanceIfExactlyOneFileProviderIsSpecified()
public void FileProvider_ReturnsInstance_IfExactlyOneFileProviderIsRegistered()
{
// Arrange
var fileProvider = new TestFileProvider();
@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
}
[Fact]
public void FileProvider_ReturnsCompositeFileProviderIfNoInstancesAreRegistered()
public void FileProvider_ReturnsNullFileProvider_IfNoInstancesAreRegistered()
{
// Arrange
var options = new RazorViewEngineOptions();
@ -41,11 +41,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
var actual = fileProviderAccessor.FileProvider;
// Assert
Assert.IsType<CompositeFileProvider>(actual);
Assert.IsType<NullFileProvider>(actual);
}
[Fact]
public void FileProvider_ReturnsCompositeFileProviderIfMoreThanOneInstanceIsRegistered()
public void FileProvider_ReturnsCompositeFileProvider_IfMoreThanOneInstanceIsRegistered()
{
// Arrange
var options = new RazorViewEngineOptions();

View File

@ -18,21 +18,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
var detailsProviders = new IMetadataDetailsProvider[]
{
new DefaultBindingMetadataProvider(CreateMessageProvider()),
new DefaultBindingMetadataProvider(),
new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(),
new DataMemberRequiredBindingMetadataProvider(),
};
var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders);
return new DefaultModelMetadataProvider(compositeDetailsProvider);
return new DefaultModelMetadataProvider(compositeDetailsProvider, new TestOptionsManager<MvcOptions>());
}
public static IModelMetadataProvider CreateDefaultProvider(IList<IMetadataDetailsProvider> providers)
{
var detailsProviders = new List<IMetadataDetailsProvider>()
{
new DefaultBindingMetadataProvider(CreateMessageProvider()),
new DefaultBindingMetadataProvider(),
new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(),
new DataMemberRequiredBindingMetadataProvider(),
@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
detailsProviders.AddRange(providers);
var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders);
return new DefaultModelMetadataProvider(compositeDetailsProvider);
return new DefaultModelMetadataProvider(compositeDetailsProvider, new TestOptionsManager<MvcOptions>());
}
public static IModelMetadataProvider CreateProvider(IList<IMetadataDetailsProvider> providers)
@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders);
return new DefaultModelMetadataProvider(compositeDetailsProvider);
return new DefaultModelMetadataProvider(compositeDetailsProvider, new TestOptionsManager<MvcOptions>());
}
private readonly TestModelMetadataDetailsProvider _detailsProvider;
@ -64,13 +64,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
private TestModelMetadataProvider(TestModelMetadataDetailsProvider detailsProvider)
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
{
new DefaultBindingMetadataProvider(CreateMessageProvider()),
new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(),
detailsProvider
}))
: base(
new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
{
new DefaultBindingMetadataProvider(),
new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(),
detailsProvider
}),
new TestOptionsManager<MvcOptions>())
{
_detailsProvider = detailsProvider;
}
@ -106,20 +108,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return ForProperty(typeof(TContainer), propertyName);
}
private static ModelBindingMessageProvider CreateMessageProvider()
{
return new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor = name => $"A value for the '{ name }' property was not provided.",
MissingKeyOrValueAccessor = () => $"A value is required.",
ValueMustNotBeNullAccessor = value => $"The value '{ value }' is invalid.",
AttemptedValueIsInvalidAccessor = (value, name) => $"The value '{ value }' is not valid for { name }.",
UnknownValueIsInvalidAccessor = name => $"The supplied value is invalid for { name }.",
ValueIsInvalidAccessor = value => $"The value '{ value }' is invalid.",
ValueMustBeANumberAccessor = name => $"The field { name } must be a number.",
};
}
private class TestModelMetadataDetailsProvider :
IBindingMetadataProvider,
IDisplayMetadataProvider,

View File

@ -32,20 +32,20 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
}
[Fact]
public void FindView_IsMainPage_ReturnsNotFoundResult_WhenNoViewEnginesAreRegistered()
public void FindView_IsMainPage_Throws_WhenNoViewEnginesAreRegistered()
{
// Arrange
var expected = $"'{typeof(MvcViewOptions).FullName}.{nameof(MvcViewOptions.ViewEngines)}' must not be " +
$"empty. At least one '{typeof(IViewEngine).FullName}' is required to locate a view for rendering.";
var viewName = "test-view";
var actionContext = GetActionContext();
var optionsAccessor = new TestOptionsManager<MvcViewOptions>();
var compositeViewEngine = new CompositeViewEngine(optionsAccessor);
// Act
var result = compositeViewEngine.FindView(actionContext, viewName, isMainPage: true);
// Assert
Assert.False(result.Success);
Assert.Empty(result.SearchedLocations);
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(
() => compositeViewEngine.FindView(actionContext, viewName, isMainPage: true));
Assert.Equal(expected, exception.Message);
}
@ -165,16 +165,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
public void GetView_ReturnsNotFoundResult_WhenNoViewEnginesAreRegistered(bool isMainPage)
{
// Arrange
var expected = $"'{typeof(MvcViewOptions).FullName}.{nameof(MvcViewOptions.ViewEngines)}' must not be " +
$"empty. At least one '{typeof(IViewEngine).FullName}' is required to locate a view for rendering.";
var viewName = "test-view.cshtml";
var optionsAccessor = new TestOptionsManager<MvcViewOptions>();
var compositeViewEngine = new CompositeViewEngine(optionsAccessor);
// Act
var result = compositeViewEngine.GetView("~/Index.html", viewName, isMainPage);
// Assert
Assert.False(result.Success);
Assert.Empty(result.SearchedLocations);
// Act & Assert
var exception = Assert.Throws<InvalidOperationException>(
() => compositeViewEngine.GetView("~/Index.html", viewName, isMainPage));
Assert.Equal(expected, exception.Message);
}
@ -305,16 +305,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
public void FindView_ReturnsNotFoundResult_WhenNoViewEnginesAreRegistered()
{
// Arrange
var expected = $"'{typeof(MvcViewOptions).FullName}.{nameof(MvcViewOptions.ViewEngines)}' must not be " +
$"empty. At least one '{typeof(IViewEngine).FullName}' is required to locate a view for rendering.";
var viewName = "my-partial-view";
var optionsAccessor = new TestOptionsManager<MvcViewOptions>();
var compositeViewEngine = new CompositeViewEngine(optionsAccessor);
// Act
var result = compositeViewEngine.FindView(GetActionContext(), viewName, isMainPage: false);
// Assert
Assert.False(result.Success);
Assert.Empty(result.SearchedLocations);
// Act & AssertS
var exception = Assert.Throws<InvalidOperationException>(
() => compositeViewEngine.FindView(GetActionContext(), viewName, isMainPage: false));
Assert.Equal(expected, exception.Message);
}
[Fact]