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> /// </summary>
public class DefaultBindingMetadataProvider : IBindingMetadataProvider 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) public void CreateBindingMetadata(BindingMetadataProviderContext context)
{ {
if (context == null) 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 // PropertyFilterProvider
var propertyFilterProviders = context.Attributes.OfType<IPropertyFilterProvider>().ToArray(); var propertyFilterProviders = context.Attributes.OfType<IPropertyFilterProvider>().ToArray();
if (propertyFilterProviders.Length == 0) if (propertyFilterProviders.Length == 0)

View File

@ -4,12 +4,10 @@
using System; using System;
using System.Threading; using System.Threading;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.ModelBinding; using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.Internal namespace Microsoft.AspNetCore.Mvc.Internal
@ -33,16 +31,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal
public void Configure(MvcOptions options) 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 // Set up ModelBinding
options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider()); options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider());
options.ModelBinderProviders.Add(new ServicesModelBinderProvider()); options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
@ -79,7 +67,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
// by altering the collection of providers. // by altering the collection of providers.
options.ModelMetadataDetailsProviders.Add(new ExcludeBindingMetadataProvider(typeof(Type))); options.ModelMetadataDetailsProviders.Add(new ExcludeBindingMetadataProvider(typeof(Type)));
options.ModelMetadataDetailsProviders.Add(new DefaultBindingMetadataProvider(messageProvider)); options.ModelMetadataDetailsProviders.Add(new DefaultBindingMetadataProvider());
options.ModelMetadataDetailsProviders.Add(new DefaultValidationMetadataProvider()); options.ModelMetadataDetailsProviders.Add(new DefaultValidationMetadataProvider());
// Set up validators // Set up validators

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -117,6 +118,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (formatters == null || formatters.Count == 0) if (formatters == null || formatters.Count == 0)
{ {
formatters = OptionsFormatters; 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; var objectType = result.DeclaredType;

View File

@ -3,6 +3,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Core;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
@ -48,6 +49,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
if (context.BindingInfo.BindingSource != null && if (context.BindingInfo.BindingSource != null &&
context.BindingInfo.BindingSource.CanAcceptDataFrom(BindingSource.Body)) 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); return new BodyModelBinder(_formatters, _readerFactory);
} }

View File

@ -1,52 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Core;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
public class EmptyModelMetadataProvider : DefaultModelMetadataProvider public class EmptyModelMetadataProvider : DefaultModelMetadataProvider
{ {
public EmptyModelMetadataProvider() public EmptyModelMetadataProvider()
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[] : base(
{ new DefaultCompositeMetadataDetailsProvider(new List<IMetadataDetailsProvider>()),
new MessageOnlyBindingProvider() new OptionsAccessor())
}))
{ {
} }
private class MessageOnlyBindingProvider : IBindingMetadataProvider private class OptionsAccessor : IOptions<MvcOptions>
{ {
private readonly ModelBindingMessageProvider _messageProvider = CreateMessageProvider(); public MvcOptions Value { get; } = new MvcOptions();
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,
};
}
} }
} }
} }

View File

@ -17,6 +17,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private readonly ICompositeMetadataDetailsProvider _detailsProvider; private readonly ICompositeMetadataDetailsProvider _detailsProvider;
private readonly DefaultMetadataDetails _details; 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 ReadOnlyDictionary<object, object> _additionalValues;
private ModelMetadata _elementMetadata; private ModelMetadata _elementMetadata;
private bool? _isBindingRequired; private bool? _isBindingRequired;
@ -36,6 +40,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
IModelMetadataProvider provider, IModelMetadataProvider provider,
ICompositeMetadataDetailsProvider detailsProvider, ICompositeMetadataDetailsProvider detailsProvider,
DefaultMetadataDetails details) 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) : base(details.Key)
{ {
if (provider == null) if (provider == null)
@ -53,9 +73,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
throw new ArgumentNullException(nameof(details)); throw new ArgumentNullException(nameof(details));
} }
if (modelBindingMessageProvider == null)
{
throw new ArgumentNullException(nameof(modelBindingMessageProvider));
}
_provider = provider; _provider = provider;
_detailsProvider = detailsProvider; _detailsProvider = detailsProvider;
_details = details; _details = details;
_modelBindingMessageProvider = modelBindingMessageProvider;
} }
/// <summary> /// <summary>
@ -82,6 +108,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
if (_details.BindingMetadata == null) if (_details.BindingMetadata == null)
{ {
var context = new BindingMetadataProviderContext(Identity, _details.ModelAttributes); 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); _detailsProvider.CreateBindingMetadata(context);
_details.BindingMetadata = context.BindingMetadata; _details.BindingMetadata = context.BindingMetadata;
} }

View File

@ -6,6 +6,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using Microsoft.Extensions.Internal; using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{ {
@ -23,11 +24,35 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary> /// </summary>
/// <param name="detailsProvider">The <see cref="ICompositeMetadataDetailsProvider"/>.</param> /// <param name="detailsProvider">The <see cref="ICompositeMetadataDetailsProvider"/>.</param>
public DefaultModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider) 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; DetailsProvider = detailsProvider;
ModelBindingMessageProvider = modelBindingMessageProvider;
_cacheEntryFactory = CreateCacheEntry; _cacheEntryFactory = CreateCacheEntry;
_metadataCacheEntryForObjectType = GetMetadataCacheEntryForObjectType(); _metadataCacheEntryForObjectType = GetMetadataCacheEntryForObjectType();
} }
@ -36,6 +61,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary> /// </summary>
protected ICompositeMetadataDetailsProvider DetailsProvider { get; } 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 /> /// <inheritdoc />
public virtual IEnumerable<ModelMetadata> GetMetadataForProperties(Type modelType) public virtual IEnumerable<ModelMetadata> GetMetadataForProperties(Type modelType)
{ {
@ -78,6 +109,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return cacheEntry.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) private ModelMetadataCacheEntry GetCacheEntry(Type modelType)
{ {
ModelMetadataCacheEntry cacheEntry; ModelMetadataCacheEntry cacheEntry;
@ -123,7 +164,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </remarks> /// </remarks>
protected virtual ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry) protected virtual ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry)
{ {
return new DefaultModelMetadata(this, DetailsProvider, entry); return new DefaultModelMetadata(this, DetailsProvider, entry, ModelBindingMessageProvider);
} }
/// <summary> /// <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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using Microsoft.AspNetCore.Mvc.Core;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{ {
@ -23,6 +24,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
/// </summary> /// </summary>
public ModelBindingMessageProvider() 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> /// <summary>

View File

@ -46,6 +46,14 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(context)); 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; IModelBinder binder;
if (TryGetCachedBinder(context.Metadata, context.CacheToken, out 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); 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -343,4 +343,13 @@
<data name="CouldNotCreateIModelBinder" xml:space="preserve"> <data name="CouldNotCreateIModelBinder" xml:space="preserve">
<value>Could not create a model binder for model object of type '{0}'.</value> <value>Could not create a model binder for model object of type '{0}'.</value>
</data> </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> </root>

View File

@ -116,6 +116,15 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
return cacheEntry; 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); fileInfo = _fileProvider.GetFileInfo(normalizedPath);
if (!fileInfo.Exists) if (!fileInfo.Exists)
{ {

View File

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

View File

@ -494,6 +494,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor
return GetString("RazorPage_NestingAttributeWritingScopesNotSupported"); 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -209,4 +209,7 @@
<data name="RazorPage_NestingAttributeWritingScopesNotSupported" xml:space="preserve"> <data name="RazorPage_NestingAttributeWritingScopesNotSupported" xml:space="preserve">
<value>Nesting of TagHelper attribute writing scopes is not supported.</value> <value>Nesting of TagHelper attribute writing scopes is not supported.</value>
</data> </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> </root>

View File

@ -890,6 +890,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures
return string.Format(CultureInfo.CurrentCulture, GetString("CreateModelExpression_NullModelMetadata"), p0, p1); 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) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -283,4 +283,7 @@
<data name="CreateModelExpression_NullModelMetadata" xml:space="preserve"> <data name="CreateModelExpression_NullModelMetadata" xml:space="preserve">
<value>The {0} was unable to provide metadata for expression '{1}'.</value> <value>The {0} was unable to provide metadata for expression '{1}'.</value>
</data> </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> </root>

View File

@ -37,6 +37,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewName)); 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. // Do not allocate in the common cases: ViewEngines contains one entry or initial attempt is successful.
IEnumerable<string> searchedLocations = null; IEnumerable<string> searchedLocations = null;
List<string> searchedList = null; List<string> searchedList = null;
@ -77,6 +85,14 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
throw new ArgumentException(Resources.ArgumentCannotBeNullOrEmpty, nameof(viewPath)); 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. // Do not allocate in the common cases: ViewEngines contains one entry or initial attempt is successful.
IEnumerable<string> searchedLocations = null; IEnumerable<string> searchedLocations = null;
List<string> searchedList = 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding 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 expected = "Hmm, the supplied value is not valid for Length.";
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
var messageProvider = new ModelBindingMessageProvider var bindingMetadataProvider = new DefaultBindingMetadataProvider();
{
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 compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider }); 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)); var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));
// Act // Act
@ -939,20 +932,13 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
var dictionary = new ModelStateDictionary(); var dictionary = new ModelStateDictionary();
dictionary.SetModelValue("key", new string[] { "some value" }, "some value"); dictionary.SetModelValue("key", new string[] { "some value" }, "some value");
var messageProvider = new ModelBindingMessageProvider var bindingMetadataProvider = new DefaultBindingMetadataProvider();
{
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 compositeProvider = new DefaultCompositeMetadataDetailsProvider(new[] { bindingMetadataProvider }); 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)); var metadata = provider.GetMetadataForProperty(typeof(string), nameof(string.Length));
// Act // Act
@ -1303,5 +1289,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
// Assert // Assert
Assert.Equal("value1", property.RawValue); 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]); 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] [Theory]
[InlineData(new[] { "application/*" }, "application/*")] [InlineData(new[] { "application/*" }, "application/*")]
[InlineData(new[] { "application/xml", "application/*", "application/json" }, "application/*")] [InlineData(new[] { "application/xml", "application/*", "application/json" }, "application/*")]

View File

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

View File

@ -1,7 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // 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.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Xunit; using Xunit;
@ -24,7 +26,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
[Theory] [Theory]
[MemberData(nameof(NonBodyBindingSources))] [MemberData(nameof(NonBodyBindingSources))]
public void Create_WhenBindingSourceIsNotFromBody_ReturnsNull(BindingSource source) public void GetBinder_WhenBindingSourceIsNotFromBody_ReturnsNull(BindingSource source)
{ {
// Arrange // Arrange
var provider = CreateProvider(); var provider = CreateProvider();
@ -39,12 +41,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.Null(result); Assert.Null(result);
} }
[Fact] public void GetBinder_WhenNoInputFormatters_Throws()
public void Create_WhenBindingSourceIsFromBody_ReturnsBinder()
{ {
// Arrange // 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 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)); var context = new TestModelBinderProviderContext(typeof(Person));
context.BindingInfo.BindingSource = BindingSource.Body; context.BindingInfo.BindingSource = BindingSource.Body;
@ -55,9 +70,11 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
Assert.IsType<BodyModelBinder>(result); 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 private class Person
@ -66,5 +83,18 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Binders
public int Age { get; set; } 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;
using System.Linq; using System.Linq;
using System.Reflection;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
@ -173,7 +172,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
private static DefaultModelMetadataProvider CreateProvider() private static DefaultModelMetadataProvider CreateProvider()
{ {
return new DefaultModelMetadataProvider(new EmptyCompositeMetadataDetailsProvider()); return new DefaultModelMetadataProvider(
new EmptyCompositeMetadataDetailsProvider(),
new TestOptionsManager<MvcOptions>());
} }
[Model("OnType")] [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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Internal; using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders; using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using Microsoft.AspNetCore.Mvc.ModelBinding.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata; using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Moq; using Moq;
using Xunit; using Xunit;
@ -14,24 +12,41 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
public class ModelBinderFactoryTest 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] [Fact]
public void CreateBinder_Throws_WhenBinderNotCreated() public void CreateBinder_Throws_WhenBinderNotCreated()
{ {
// Arrange // Arrange
var metadataProvider = new TestModelMetadataProvider(); var metadataProvider = new TestModelMetadataProvider();
var options = new TestOptionsManager<MvcOptions>(); 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() var context = new ModelBinderFactoryContext()
{ {
Metadata = metadataProvider.GetMetadataForType(typeof(string)), Metadata = metadataProvider.GetMetadataForType(typeof(string)),
}; };
// Act // Act & Assert
var exception = Assert.Throws<InvalidOperationException>(() => factory.CreateBinder(context)); var exception = Assert.Throws<InvalidOperationException>(() => factory.CreateBinder(context));
// Assert
Assert.Equal( Assert.Equal(
$"Could not create a model binder for model object of type '{typeof(string).FullName}'.", $"Could not create a model binder for model object of type '{typeof(string).FullName}'.",
exception.Message); exception.Message);

View File

@ -1046,31 +1046,17 @@ namespace Microsoft.AspNetCore.Mvc.DataAnnotations.Internal
private readonly object[] _attributes; private readonly object[] _attributes;
public AttributeInjectModelMetadataProvider(object[] attributes) public AttributeInjectModelMetadataProvider(object[] attributes)
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[] : base(
{ new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
new DefaultBindingMetadataProvider(CreateMessageProvider()), {
new DataAnnotationsMetadataProvider(), new DefaultBindingMetadataProvider(),
})) new DataAnnotationsMetadataProvider(),
}),
new TestOptionsManager<MvcOptions>())
{ {
_attributes = attributes; _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) protected override DefaultMetadataDetails CreateTypeDetails(ModelMetadataIdentity key)
{ {
var entry = base.CreateTypeDetails(key); var entry = base.CreateTypeDetails(key);

View File

@ -7,6 +7,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc.Razor.Compilation; using Microsoft.AspNetCore.Mvc.Razor.Compilation;
using Microsoft.Extensions.FileProviders;
using Moq; using Moq;
using Xunit; using Xunit;
@ -445,6 +446,23 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal
Assert.Same(result1.PageFactory, result2.PageFactory); 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] [Fact]
public void GetOrAdd_CachesCompilationExceptions() public void GetOrAdd_CachesCompilationExceptions()
{ {

View File

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

View File

@ -18,21 +18,21 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
{ {
var detailsProviders = new IMetadataDetailsProvider[] var detailsProviders = new IMetadataDetailsProvider[]
{ {
new DefaultBindingMetadataProvider(CreateMessageProvider()), new DefaultBindingMetadataProvider(),
new DefaultValidationMetadataProvider(), new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(), new DataAnnotationsMetadataProvider(),
new DataMemberRequiredBindingMetadataProvider(), new DataMemberRequiredBindingMetadataProvider(),
}; };
var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders); var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders);
return new DefaultModelMetadataProvider(compositeDetailsProvider); return new DefaultModelMetadataProvider(compositeDetailsProvider, new TestOptionsManager<MvcOptions>());
} }
public static IModelMetadataProvider CreateDefaultProvider(IList<IMetadataDetailsProvider> providers) public static IModelMetadataProvider CreateDefaultProvider(IList<IMetadataDetailsProvider> providers)
{ {
var detailsProviders = new List<IMetadataDetailsProvider>() var detailsProviders = new List<IMetadataDetailsProvider>()
{ {
new DefaultBindingMetadataProvider(CreateMessageProvider()), new DefaultBindingMetadataProvider(),
new DefaultValidationMetadataProvider(), new DefaultValidationMetadataProvider(),
new DataAnnotationsMetadataProvider(), new DataAnnotationsMetadataProvider(),
new DataMemberRequiredBindingMetadataProvider(), new DataMemberRequiredBindingMetadataProvider(),
@ -41,7 +41,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
detailsProviders.AddRange(providers); detailsProviders.AddRange(providers);
var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders); var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders);
return new DefaultModelMetadataProvider(compositeDetailsProvider); return new DefaultModelMetadataProvider(compositeDetailsProvider, new TestOptionsManager<MvcOptions>());
} }
public static IModelMetadataProvider CreateProvider(IList<IMetadataDetailsProvider> providers) public static IModelMetadataProvider CreateProvider(IList<IMetadataDetailsProvider> providers)
@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders); var compositeDetailsProvider = new DefaultCompositeMetadataDetailsProvider(detailsProviders);
return new DefaultModelMetadataProvider(compositeDetailsProvider); return new DefaultModelMetadataProvider(compositeDetailsProvider, new TestOptionsManager<MvcOptions>());
} }
private readonly TestModelMetadataDetailsProvider _detailsProvider; private readonly TestModelMetadataDetailsProvider _detailsProvider;
@ -64,13 +64,15 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
} }
private TestModelMetadataProvider(TestModelMetadataDetailsProvider detailsProvider) private TestModelMetadataProvider(TestModelMetadataDetailsProvider detailsProvider)
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[] : base(
{ new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
new DefaultBindingMetadataProvider(CreateMessageProvider()), {
new DefaultValidationMetadataProvider(), new DefaultBindingMetadataProvider(),
new DataAnnotationsMetadataProvider(), new DefaultValidationMetadataProvider(),
detailsProvider new DataAnnotationsMetadataProvider(),
})) detailsProvider
}),
new TestOptionsManager<MvcOptions>())
{ {
_detailsProvider = detailsProvider; _detailsProvider = detailsProvider;
} }
@ -106,20 +108,6 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return ForProperty(typeof(TContainer), propertyName); 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 : private class TestModelMetadataDetailsProvider :
IBindingMetadataProvider, IBindingMetadataProvider,
IDisplayMetadataProvider, IDisplayMetadataProvider,

View File

@ -32,20 +32,20 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
} }
[Fact] [Fact]
public void FindView_IsMainPage_ReturnsNotFoundResult_WhenNoViewEnginesAreRegistered() public void FindView_IsMainPage_Throws_WhenNoViewEnginesAreRegistered()
{ {
// Arrange // 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 viewName = "test-view";
var actionContext = GetActionContext(); var actionContext = GetActionContext();
var optionsAccessor = new TestOptionsManager<MvcViewOptions>(); var optionsAccessor = new TestOptionsManager<MvcViewOptions>();
var compositeViewEngine = new CompositeViewEngine(optionsAccessor); var compositeViewEngine = new CompositeViewEngine(optionsAccessor);
// Act // Act & Assert
var result = compositeViewEngine.FindView(actionContext, viewName, isMainPage: true); var exception = Assert.Throws<InvalidOperationException>(
() => compositeViewEngine.FindView(actionContext, viewName, isMainPage: true));
// Assert Assert.Equal(expected, exception.Message);
Assert.False(result.Success);
Assert.Empty(result.SearchedLocations);
} }
@ -165,16 +165,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
public void GetView_ReturnsNotFoundResult_WhenNoViewEnginesAreRegistered(bool isMainPage) public void GetView_ReturnsNotFoundResult_WhenNoViewEnginesAreRegistered(bool isMainPage)
{ {
// Arrange // 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 viewName = "test-view.cshtml";
var optionsAccessor = new TestOptionsManager<MvcViewOptions>(); var optionsAccessor = new TestOptionsManager<MvcViewOptions>();
var compositeViewEngine = new CompositeViewEngine(optionsAccessor); var compositeViewEngine = new CompositeViewEngine(optionsAccessor);
// Act // Act & Assert
var result = compositeViewEngine.GetView("~/Index.html", viewName, isMainPage); var exception = Assert.Throws<InvalidOperationException>(
() => compositeViewEngine.GetView("~/Index.html", viewName, isMainPage));
// Assert Assert.Equal(expected, exception.Message);
Assert.False(result.Success);
Assert.Empty(result.SearchedLocations);
} }
@ -305,16 +305,16 @@ namespace Microsoft.AspNetCore.Mvc.ViewEngines
public void FindView_ReturnsNotFoundResult_WhenNoViewEnginesAreRegistered() public void FindView_ReturnsNotFoundResult_WhenNoViewEnginesAreRegistered()
{ {
// Arrange // 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 viewName = "my-partial-view";
var optionsAccessor = new TestOptionsManager<MvcViewOptions>(); var optionsAccessor = new TestOptionsManager<MvcViewOptions>();
var compositeViewEngine = new CompositeViewEngine(optionsAccessor); var compositeViewEngine = new CompositeViewEngine(optionsAccessor);
// Act // Act & AssertS
var result = compositeViewEngine.FindView(GetActionContext(), viewName, isMainPage: false); var exception = Assert.Throws<InvalidOperationException>(
() => compositeViewEngine.FindView(GetActionContext(), viewName, isMainPage: false));
// Assert Assert.Equal(expected, exception.Message);
Assert.False(result.Success);
Assert.Empty(result.SearchedLocations);
} }
[Fact] [Fact]