diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultBindingMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultBindingMetadataProvider.cs index be82a8f14a..8ed75a30ef 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultBindingMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/DefaultBindingMetadataProvider.cs @@ -15,19 +15,6 @@ namespace Microsoft.AspNetCore.Mvc.Internal /// public class DefaultBindingMetadataProvider : IBindingMetadataProvider { - private readonly ModelBindingMessageProvider _messageProvider; - - public DefaultBindingMetadataProvider(ModelBindingMessageProvider messageProvider) - { - if (messageProvider == null) - { - throw new ArgumentNullException(nameof(messageProvider)); - } - - _messageProvider = messageProvider; - } - - /// 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().ToArray(); if (propertyFilterProviders.Length == 0) diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs index 0246e9a223..74fa4b9914 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/MvcCoreMvcOptionsSetup.cs @@ -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 diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectResultExecutor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectResultExecutor.cs index cb0306c18a..ff06f6d483 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectResultExecutor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Internal/ObjectResultExecutor.cs @@ -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; } - + /// /// Selects the to write the response based on the content type values /// present in and . @@ -423,19 +434,19 @@ namespace Microsoft.AspNetCore.Mvc.Internal IList 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; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs index 0658cc71d3..d3f16b1088 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Binders/BodyModelBinderProvider.cs @@ -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); } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/EmptyModelMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/EmptyModelMetadataProvider.cs index 454df38355..122d760af8 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/EmptyModelMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/EmptyModelMetadataProvider.cs @@ -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()), + new OptionsAccessor()) { } - private class MessageOnlyBindingProvider : IBindingMetadataProvider + private class OptionsAccessor : IOptions { - 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(); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs index 8dc54ddea4..b99d128441 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadata.cs @@ -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 _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()) + { + } + + /// + /// Creates a new . + /// + /// The . + /// The . + /// The . + /// The . + 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; } /// @@ -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; } diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs index 2c564af29a..9353a04a1f 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs @@ -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 /// /// The . public DefaultModelMetadataProvider(ICompositeMetadataDetailsProvider detailsProvider) + : this(detailsProvider, new ModelBindingMessageProvider()) { + } + + /// + /// Creates a new . + /// + /// The . + /// The accessor for . + public DefaultModelMetadataProvider( + ICompositeMetadataDetailsProvider detailsProvider, + IOptions 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 /// protected ICompositeMetadataDetailsProvider DetailsProvider { get; } + /// + /// Gets the . + /// + /// Same as in all production scenarios. + protected ModelBindingMessageProvider ModelBindingMessageProvider { get; } + /// public virtual IEnumerable GetMetadataForProperties(Type modelType) { @@ -78,6 +109,16 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata return cacheEntry.Metadata; } + private static ModelBindingMessageProvider GetMessageProvider(IOptions 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 /// protected virtual ModelMetadata CreateModelMetadata(DefaultMetadataDetails entry) { - return new DefaultModelMetadata(this, DetailsProvider, entry); + return new DefaultModelMetadata(this, DetailsProvider, entry, ModelBindingMessageProvider); } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelBindingMessageProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelBindingMessageProvider.cs index 4f1b67f65c..950e059241 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelBindingMessageProvider.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelBindingMessageProvider.cs @@ -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 /// 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; } /// diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs index fc72488a76..f2f74099a6 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ModelBinderFactory.cs @@ -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 diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs index bb38364f51..418c37e1bf 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Core/Properties/Resources.Designer.cs @@ -1162,6 +1162,54 @@ namespace Microsoft.AspNetCore.Mvc.Core return string.Format(CultureInfo.CurrentCulture, GetString("CouldNotCreateIModelBinder"), p0); } + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to bind from the body. + /// + internal static string InputFormattersAreRequired + { + get { return GetString("InputFormattersAreRequired"); } + } + + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to bind from the body. + /// + internal static string FormatInputFormattersAreRequired(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("InputFormattersAreRequired"), p0, p1, p2); + } + + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to model bind. + /// + internal static string ModelBinderProvidersAreRequired + { + get { return GetString("ModelBinderProvidersAreRequired"); } + } + + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to model bind. + /// + internal static string FormatModelBinderProvidersAreRequired(object p0, object p1, object p2) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ModelBinderProvidersAreRequired"), p0, p1, p2); + } + + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to format a response. + /// + internal static string OutputFormattersAreRequired + { + get { return GetString("OutputFormattersAreRequired"); } + } + + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to format a response. + /// + 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); diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx index d5ce00f4ef..732bba5387 100644 --- a/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Core/Resources.resx @@ -1,17 +1,17 @@  - @@ -343,4 +343,13 @@ Could not create a model binder for model object of type '{0}'. + + '{0}.{1}' must not be empty. At least one '{2}' is required to bind from the body. + + + '{0}.{1}' must not be empty. At least one '{2}' is required to model bind. + + + '{0}.{1}' must not be empty. At least one '{2}' is required to format a response. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs index 15b11c1846..575f1e5728 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/CompilerCache.cs @@ -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) { diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorViewEngineFileProviderAccessor.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorViewEngineFileProviderAccessor.cs index e1fc5f6bf7..5823a4b39c 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorViewEngineFileProviderAccessor.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Internal/DefaultRazorViewEngineFileProviderAccessor.cs @@ -18,7 +18,11 @@ namespace Microsoft.AspNetCore.Mvc.Razor.Internal public DefaultRazorViewEngineFileProviderAccessor(IOptions 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]; } diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs index fe96da1030..9552b4a630 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Properties/Resources.Designer.cs @@ -494,6 +494,22 @@ namespace Microsoft.AspNetCore.Mvc.Razor return GetString("RazorPage_NestingAttributeWritingScopesNotSupported"); } + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering. + /// + internal static string FileProvidersAreRequired + { + get { return GetString("FileProvidersAreRequired"); } + } + + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering. + /// + 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); diff --git a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx index 5d8203d3da..6e402095f5 100644 --- a/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.Razor/Resources.resx @@ -1,17 +1,17 @@  - @@ -209,4 +209,7 @@ Nesting of TagHelper attribute writing scopes is not supported. + + '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs index a3ae42d54f..a2db38f704 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Properties/Resources.Designer.cs @@ -890,6 +890,22 @@ namespace Microsoft.AspNetCore.Mvc.ViewFeatures return string.Format(CultureInfo.CurrentCulture, GetString("CreateModelExpression_NullModelMetadata"), p0, p1); } + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering. + /// + internal static string ViewEnginesAreRequired + { + get { return GetString("ViewEnginesAreRequired"); } + } + + /// + /// '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering. + /// + 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); diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx index 61c168de0c..55a2345b48 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/Resources.resx @@ -1,17 +1,17 @@  - @@ -283,4 +283,7 @@ The {0} was unable to provide metadata for expression '{1}'. + + '{0}.{1}' must not be empty. At least one '{2}' is required to locate a view for rendering. + \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/CompositeViewEngine.cs b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/CompositeViewEngine.cs index c492c88cea..d0478b2aeb 100644 --- a/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/CompositeViewEngine.cs +++ b/src/Microsoft.AspNetCore.Mvc.ViewFeatures/ViewEngines/CompositeViewEngine.cs @@ -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 searchedLocations = null; List 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 searchedLocations = null; List searchedList = null; diff --git a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs index 231f444240..3611cee6ed 100644 --- a/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Abstractions.Test/ModelBinding/ModelStateDictionaryTest.cs @@ -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 + { + public MvcOptions Value { get; } = new MvcOptions(); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs index 55912a2483..78e3fa1e38 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Infrastructure/ObjectResultExecutorTest.cs @@ -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( + () => executor.ExecuteAsync(actionContext, result)); + Assert.Equal(expected, exception.Message); + } + [Theory] [InlineData(new[] { "application/*" }, "application/*")] [InlineData(new[] { "application/xml", "application/*", "application/json" }, "application/*")] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultBindingMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultBindingMetadataProviderTest.cs index 85f8fca32c..9e553ea05d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultBindingMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/Internal/DefaultBindingMetadataProviderTest.cs @@ -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 { diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderProviderTest.cs index 7a15456b11..ff64a9bf4d 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Binders/BodyModelBinderProviderTest.cs @@ -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(() => 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(result); } - private static BodyModelBinderProvider CreateProvider() + private static BodyModelBinderProvider CreateProvider(params IInputFormatter[] formatters) { - return new BodyModelBinderProvider(new List(), new TestHttpRequestStreamReaderFactory()); + return new BodyModelBinderProvider( + new List(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 ReadAsync(InputFormatterContext context) + { + throw new NotImplementedException(); + } + } } } diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs index 66fda668b3..700be54129 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/Metadata/DefaultModelMetadataProviderTest.cs @@ -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()); } [Model("OnType")] diff --git a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBinderFactoryTest.cs b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBinderFactoryTest.cs index 4524b49fb5..8b95b45a52 100644 --- a/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBinderFactoryTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Core.Test/ModelBinding/ModelBinderFactoryTest.cs @@ -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(); + var factory = new ModelBinderFactory(metadataProvider, options); + var context = new ModelBinderFactoryContext() + { + Metadata = metadataProvider.GetMetadataForType(typeof(string)), + }; + + // Act & Assert + var exception = Assert.Throws(() => factory.CreateBinder(context)); + Assert.Equal(expected, exception.Message); + } + [Fact] public void CreateBinder_Throws_WhenBinderNotCreated() { // Arrange var metadataProvider = new TestModelMetadataProvider(); var options = new TestOptionsManager(); - 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(() => factory.CreateBinder(context)); - - // Assert Assert.Equal( $"Could not create a model binder for model object of type '{typeof(string).FullName}'.", exception.Message); diff --git a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs index f50bf402c7..ed343b3be0 100644 --- a/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.DataAnnotations.Test/Internal/ModelMetadataProviderTest.cs @@ -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()) { _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); diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs index e7e4b5ddf2..cf525a6443 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/CompilerCacheTest.cs @@ -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( + () => cache.GetOrAdd(ViewPath, _ => { throw new InvalidTimeZoneException(); })); + Assert.Equal(expected, exception.Message); + } + [Fact] public void GetOrAdd_CachesCompilationExceptions() { diff --git a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorViewEngineFileProviderAccessorTest.cs b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorViewEngineFileProviderAccessorTest.cs index 52ac2610c4..7f64ceb9f8 100644 --- a/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorViewEngineFileProviderAccessorTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.Razor.Test/Internal/DefaultRazorViewEngineFileProviderAccessorTest.cs @@ -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(actual); + Assert.IsType(actual); } [Fact] - public void FileProvider_ReturnsCompositeFileProviderIfMoreThanOneInstanceIsRegistered() + public void FileProvider_ReturnsCompositeFileProvider_IfMoreThanOneInstanceIsRegistered() { // Arrange var options = new RazorViewEngineOptions(); diff --git a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs index d40cd366a6..a6b3f89719 100644 --- a/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs +++ b/test/Microsoft.AspNetCore.Mvc.TestCommon/TestModelMetadataProvider.cs @@ -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()); } public static IModelMetadataProvider CreateDefaultProvider(IList providers) { var detailsProviders = new List() { - 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()); } public static IModelMetadataProvider CreateProvider(IList 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()); } 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()) { _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, diff --git a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewEngines/CompositeViewEngineTest.cs b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewEngines/CompositeViewEngineTest.cs index 036a8ed1d3..3696a61639 100644 --- a/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewEngines/CompositeViewEngineTest.cs +++ b/test/Microsoft.AspNetCore.Mvc.ViewFeatures.Test/ViewEngines/CompositeViewEngineTest.cs @@ -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(); 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( + () => 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(); 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( + () => 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(); 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( + () => compositeViewEngine.FindView(GetActionContext(), viewName, isMainPage: false)); + Assert.Equal(expected, exception.Message); } [Fact]