Fail more gracefully when option collections cleared

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;
@ -117,6 +118,16 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (formatters == null || formatters.Count == 0)
{
formatters = OptionsFormatters;
// Complain about MvcOptions.OutputFormatters only if the result has an empty Formatters.
Debug.Assert(formatters != null, "MvcOptions.OutputFormatters cannot be null.");
if (formatters.Count == 0)
{
throw new InvalidOperationException(Resources.FormatOutputFormattersAreRequired(
typeof(MvcOptions).FullName,
nameof(MvcOptions.OutputFormatters),
typeof(IOutputFormatter).FullName));
}
}
var objectType = result.DeclaredType;
@ -136,7 +147,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
{
// No formatter supports this.
Logger.NoFormatter(formatterContext);
context.HttpContext.Response.StatusCode = StatusCodes.Status406NotAcceptable;
return TaskCache.CompletedTask;
}
@ -189,7 +200,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
if (acceptableMediaTypes.Count == 0)
{
// 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();
selectFormatterWithoutRegardingAcceptHeader = true;
@ -399,7 +410,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
return null;
}
/// <summary>
/// Selects the <see cref="IOutputFormatter"/> to write the response based on the content type values
/// present in <paramref name="sortedAcceptableContentTypes"/> and <paramref name="possibleOutputContentTypes"/>.
@ -423,19 +434,19 @@ namespace Microsoft.AspNetCore.Mvc.Internal
IList<MediaTypeSegmentWithQuality> sortedAcceptableContentTypes,
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);
for (var j = 0; j < possibleOutputContentTypes.Count; j++)
for (var j = 0; j < possibleOutputContentTypes.Count; 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];
formatterContext.ContentType = new StringSegment(possibleOutputContentTypes[j]);
if (formatter.CanWriteResult(formatterContext))
if (formatter.CanWriteResult(formatterContext))
{
return formatter;
}
@ -443,7 +454,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
}
}
}
return null;
}

View File

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

View File

@ -1,52 +1,25 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Mvc.Core;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ModelBinding
{
public class EmptyModelMetadataProvider : DefaultModelMetadataProvider
{
public EmptyModelMetadataProvider()
: base(new DefaultCompositeMetadataDetailsProvider(new IMetadataDetailsProvider[]
{
new MessageOnlyBindingProvider()
}))
: base(
new DefaultCompositeMetadataDetailsProvider(new List<IMetadataDetailsProvider>()),
new OptionsAccessor())
{
}
private class MessageOnlyBindingProvider : IBindingMetadataProvider
private class OptionsAccessor : IOptions<MvcOptions>
{
private readonly ModelBindingMessageProvider _messageProvider = CreateMessageProvider();
public void CreateBindingMetadata(BindingMetadataProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
// Don't bother with ModelBindingMessageProvider copy constructor. No other provider can change the
// delegates.
context.BindingMetadata.ModelBindingMessageProvider = _messageProvider;
}
private static ModelBindingMessageProvider CreateMessageProvider()
{
return new ModelBindingMessageProvider
{
MissingBindRequiredValueAccessor = Resources.FormatModelBinding_MissingBindRequiredMember,
MissingKeyOrValueAccessor = Resources.FormatKeyValuePair_BothKeyAndValueMustBePresent,
ValueMustNotBeNullAccessor = Resources.FormatModelBinding_NullValueNotValid,
AttemptedValueIsInvalidAccessor = Resources.FormatModelState_AttemptedValueIsInvalid,
UnknownValueIsInvalidAccessor = Resources.FormatModelState_UnknownValueIsInvalid,
ValueIsInvalidAccessor = Resources.FormatHtmlGeneration_ValueIsInvalid,
ValueMustBeANumberAccessor = Resources.FormatHtmlGeneration_ValueMustBeNumber,
};
}
public MvcOptions Value { get; } = new MvcOptions();
}
}
}

View File

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

View File

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

View File

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

View File

@ -46,12 +46,20 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(context));
}
if (_providers.Length == 0)
{
throw new InvalidOperationException(Resources.FormatModelBinderProvidersAreRequired(
typeof(MvcOptions).FullName,
nameof(MvcOptions.ModelBinderProviders),
typeof(IModelBinderProvider).FullName));
}
IModelBinder binder;
if (TryGetCachedBinder(context.Metadata, context.CacheToken, out binder))
{
return binder;
}
// 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
// 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);
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to bind from the body.
/// </summary>
internal static string InputFormattersAreRequired
{
get { return GetString("InputFormattersAreRequired"); }
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to bind from the body.
/// </summary>
internal static string FormatInputFormattersAreRequired(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("InputFormattersAreRequired"), p0, p1, p2);
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to model bind.
/// </summary>
internal static string ModelBinderProvidersAreRequired
{
get { return GetString("ModelBinderProvidersAreRequired"); }
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to model bind.
/// </summary>
internal static string FormatModelBinderProvidersAreRequired(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderProvidersAreRequired"), p0, p1, p2);
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to format a response.
/// </summary>
internal static string OutputFormattersAreRequired
{
get { return GetString("OutputFormattersAreRequired"); }
}
/// <summary>
/// '{0}.{1}' must not be empty. At least one '{2}' is required to format a response.
/// </summary>
internal static string FormatOutputFormattersAreRequired(object p0, object p1, object p2)
{
return string.Format(CultureInfo.CurrentCulture, GetString("OutputFormattersAreRequired"), p0, p1, p2);
}
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</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>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
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
: and then encoded with base64 encoding.
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
: and then encoded with base64 encoding.
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
: and then encoded with base64 encoding.
-->
@ -343,4 +343,13 @@
<data name="CouldNotCreateIModelBinder" xml:space="preserve">
<value>Could not create a model binder for model object of type '{0}'.</value>
</data>
<data name="InputFormattersAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to bind from the body.</value>
</data>
<data name="ModelBinderProvidersAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to model bind.</value>
</data>
<data name="OutputFormattersAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to format a response.</value>
</data>
</root>

View File

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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</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>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
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
: and then encoded with base64 encoding.
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
: and then encoded with base64 encoding.
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
: and then encoded with base64 encoding.
-->
@ -209,4 +209,7 @@
<data name="RazorPage_NestingAttributeWritingScopesNotSupported" xml:space="preserve">
<value>Nesting of TagHelper attribute writing scopes is not supported.</value>
</data>
<data name="FileProvidersAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.</value>
</data>
</root>

View File

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

View File

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</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>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
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
: and then encoded with base64 encoding.
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
: and then encoded with base64 encoding.
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
: and then encoded with base64 encoding.
-->
@ -283,4 +283,7 @@
<data name="CreateModelExpression_NullModelMetadata" xml:space="preserve">
<value>The {0} was unable to provide metadata for expression '{1}'.</value>
</data>
<data name="ViewEnginesAreRequired" xml:space="preserve">
<value>'{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering.</value>
</data>
</root>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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