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;
@ -136,7 +147,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{ {
// No formatter supports this. // No formatter supports this.
Logger.NoFormatter(formatterContext); Logger.NoFormatter(formatterContext);
context.HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable; context.HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable;
return TaskCache.CompletedTask; return TaskCache.CompletedTask;
} }
@ -189,7 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (acceptableMediaTypes.Count == 0) if (acceptableMediaTypes.Count == 0)
{ {
// There is either no Accept header value, or it contained */* and we // There is either no Accept header value, or it contained */* and we
// are not currently respecting the 'browser accept header'. // are not currently respecting the 'browser accept header'.
Logger.NoAcceptForNegotiation(); Logger.NoAcceptForNegotiation();
selectFormatterWithoutRegardingAcceptHeader = true; selectFormatterWithoutRegardingAcceptHeader = true;
@ -399,7 +410,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return null; return null;
} }
/// <summary> /// <summary>
/// Selects the <see cref="IOutputFormatter"/> to write the response based on the content type values /// Selects the <see cref="IOutputFormatter"/> to write the response based on the content type values
/// present in <paramref name="sortedAcceptableContentTypes"/> and <paramref name="possibleOutputContentTypes"/>. /// present in <paramref name="sortedAcceptableContentTypes"/> and <paramref name="possibleOutputContentTypes"/>.
@ -423,19 +434,19 @@ namespace Microsoft.AspNetCore.Mvc.Internal
IList<MediaTypeSegmentWithQuality> sortedAcceptableContentTypes, IList<MediaTypeSegmentWithQuality> sortedAcceptableContentTypes,
MediaTypeCollection possibleOutputContentTypes) MediaTypeCollection possibleOutputContentTypes)
{ {
for (var i = 0; i < sortedAcceptableContentTypes.Count; i++) for (var i = 0; i < sortedAcceptableContentTypes.Count; i++)
{ {
var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType); var acceptableContentType = new MediaType(sortedAcceptableContentTypes[i].MediaType);
for (var j = 0; j < possibleOutputContentTypes.Count; j++) for (var j = 0; j < possibleOutputContentTypes.Count; j++)
{ {
var candidateContentType = new MediaType(possibleOutputContentTypes[j]); var candidateContentType = new MediaType(possibleOutputContentTypes[j]);
if (candidateContentType.IsSubsetOf(acceptableContentType)) if (candidateContentType.IsSubsetOf(acceptableContentType))
{ {
for (var k = 0; k < formatters.Count; k++) for (var k = 0; k < formatters.Count; k++)
{ {
var formatter = formatters[k]; var formatter = formatters[k];
formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]); formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]);
if (formatter.CanWriteResult(formatterContext)) if (formatter.CanWriteResult(formatterContext))
{ {
return formatter; return formatter;
} }
@ -443,7 +454,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
} }
} }
} }
return null; return null;
} }

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,12 +46,20 @@ 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))
{ {
return binder; return binder;
} }
// Perf: We're calling the Uncached version of the API here so we can: // Perf: We're calling the Uncached version of the API here so we can:
// 1. avoid allocating a context when the value is already cached // 1. avoid allocating a context when the value is already cached
// 2. avoid checking the cache twice when the value is not cached // 2. avoid checking the cache twice when the value is not cached

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

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@ -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

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@ -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

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<root> <root>
<!-- <!--
Microsoft ResX Schema Microsoft ResX Schema
Version 2.0 Version 2.0
The primary goals of this format is to allow a simple XML format The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes various data types are done through the TypeConverter classes
associated with the data types. associated with the data types.
Example: Example:
... ado.net/XML headers & schema ... ... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader> <resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader> <resheader name="version">2.0</resheader>
@ -26,36 +26,36 @@
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value> <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment> <comment>This is a comment</comment>
</data> </data>
There are any number of "resheader" rows that contain simple There are any number of "resheader" rows that contain simple
name/value pairs. name/value pairs.
Each data row contains a name, and value. The row also contains a Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture. text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the Classes that don't support this are serialized and stored with the
mimetype set. mimetype set.
The mimetype is used for serialized objects, and tells the The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly: extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below. read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64 mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64 mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64 mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter : using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding. : and then encoded with base64 encoding.
--> -->
@ -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]