diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs
index 782b84b2cd..af97d9a043 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/Metadata/ModelMetadataIdentity.cs
@@ -66,17 +66,37 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
};
}
+ ///
+ /// Creates a for the provided parameter.
+ ///
+ /// The .
+ /// A .
public static ModelMetadataIdentity ForParameter(ParameterInfo parameter)
+ => ForParameter(parameter, parameter?.ParameterType);
+
+ ///
+ /// Creates a for the provided parameter with the specified
+ /// model type.
+ ///
+ /// The .
+ /// The model type.
+ /// A .
+ public static ModelMetadataIdentity ForParameter(ParameterInfo parameter, Type modelType)
{
if (parameter == null)
{
throw new ArgumentNullException(nameof(parameter));
}
+ if (modelType == null)
+ {
+ throw new ArgumentNullException(nameof(modelType));
+ }
+
return new ModelMetadataIdentity()
{
Name = parameter.Name,
- ModelType = parameter.ParameterType,
+ ModelType = modelType,
ParameterInfo = parameter,
};
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs
index 1b4f01cd6d..147ccc45f5 100644
--- a/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Abstractions/ModelBinding/ModelMetadataProvider.cs
@@ -32,5 +32,27 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
/// The .
/// A instance describing the .
public abstract ModelMetadata GetMetadataForParameter(ParameterInfo parameter);
+
+ ///
+ /// Supplies metadata describing a parameter.
+ ///
+ /// The
+ /// The actual model type.
+ /// A instance describing the .
+ public virtual ModelMetadata GetMetadataForParameter(ParameterInfo parameter, Type modelType)
+ {
+ throw new NotSupportedException();
+ }
+
+ ///
+ /// Supplies metadata describing a property.
+ ///
+ /// The .
+ /// The actual model type.
+ /// A instance describing the .
+ public virtual ModelMetadata GetMetadataForProperty(PropertyInfo propertyInfo, Type modelType)
+ {
+ throw new NotSupportedException();
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs
index 5626f1ccd2..c51f809da2 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ControllerBase.cs
@@ -1704,6 +1704,14 @@ namespace Microsoft.AspNetCore.Mvc
public virtual UnauthorizedResult Unauthorized()
=> new UnauthorizedResult();
+ ///
+ /// Creates an that produces a response.
+ ///
+ /// The created for the response.
+ [NonAction]
+ public virtual UnauthorizedObjectResult Unauthorized(object value)
+ => new UnauthorizedObjectResult(value);
+
///
/// Creates an that produces a response.
///
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs
index 7c41dda166..709dffcf5c 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerBoundPropertyDescriptor.cs
@@ -3,16 +3,17 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Controllers
{
///
/// A descriptor for model bound properties of a controller.
///
- public class ControllerBoundPropertyDescriptor : ParameterDescriptor
+ public class ControllerBoundPropertyDescriptor : ParameterDescriptor, IPropertyInfoParameterDescriptor
{
///
- /// Gets or sets the for this property.
+ /// Gets or sets the for this property.
///
public PropertyInfo PropertyInfo { get; set; }
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs
index e48ef9dd00..593a40d0fc 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Controllers/ControllerParameterDescriptor.cs
@@ -3,16 +3,17 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.Controllers
{
///
/// A descriptor for method parameters of an action method.
///
- public class ControllerParameterDescriptor : ParameterDescriptor
+ public class ControllerParameterDescriptor : ParameterDescriptor, IParameterInfoParameterDescriptor
{
///
- /// Gets or sets the .
+ /// Gets or sets the .
///
public ParameterInfo ParameterInfo { get; set; }
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IParameterInfoParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IParameterInfoParameterDescriptor.cs
new file mode 100644
index 0000000000..93e6a09b28
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IParameterInfoParameterDescriptor.cs
@@ -0,0 +1,19 @@
+// 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.Reflection;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+
+namespace Microsoft.AspNetCore.Mvc.Infrastructure
+{
+ ///
+ /// A for action parameters.
+ ///
+ public interface IParameterInfoParameterDescriptor
+ {
+ ///
+ /// Gets the .
+ ///
+ ParameterInfo ParameterInfo { get; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IPropertyInfoParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IPropertyInfoParameterDescriptor.cs
new file mode 100644
index 0000000000..5a9a8682d1
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/Infrastructure/IPropertyInfoParameterDescriptor.cs
@@ -0,0 +1,19 @@
+// 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.Reflection;
+using Microsoft.AspNetCore.Mvc.Abstractions;
+
+namespace Microsoft.AspNetCore.Mvc.Infrastructure
+{
+ ///
+ /// A for bound properties.
+ ///
+ public interface IPropertyInfoParameterDescriptor
+ {
+ ///
+ /// Gets the .
+ ///
+ PropertyInfo PropertyInfo { get; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs
index 5b79143002..b9e374701e 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/DefaultModelMetadataProvider.cs
@@ -4,6 +4,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Reflection;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Options;
@@ -97,14 +98,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return cacheEntry.Details.Properties;
}
+ ///
public override ModelMetadata GetMetadataForParameter(ParameterInfo parameter)
+ => GetMetadataForParameter(parameter, parameter?.ParameterType);
+
+ ///
+ public override ModelMetadata GetMetadataForParameter(ParameterInfo parameter, Type modelType)
{
if (parameter == null)
{
throw new ArgumentNullException(nameof(parameter));
}
- var cacheEntry = GetCacheEntry(parameter);
+ if (modelType == null)
+ {
+ throw new ArgumentNullException(nameof(modelType));
+ }
+
+ var cacheEntry = GetCacheEntry(parameter, modelType);
return cacheEntry.Metadata;
}
@@ -122,6 +133,24 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return cacheEntry.Metadata;
}
+ ///
+ public override ModelMetadata GetMetadataForProperty(PropertyInfo propertyInfo, Type modelType)
+ {
+ if (propertyInfo == null)
+ {
+ throw new ArgumentNullException(nameof(propertyInfo));
+ }
+
+ if (modelType == null)
+ {
+ throw new ArgumentNullException(nameof(modelType));
+ }
+
+ var cacheEntry = GetCacheEntry(propertyInfo, modelType);
+
+ return cacheEntry.Metadata;
+ }
+
private static DefaultModelBindingMessageProvider GetMessageProvider(IOptions optionsAccessor)
{
if (optionsAccessor == null)
@@ -151,10 +180,17 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return cacheEntry;
}
- private ModelMetadataCacheEntry GetCacheEntry(ParameterInfo parameter)
+ private ModelMetadataCacheEntry GetCacheEntry(ParameterInfo parameter, Type modelType)
{
return _typeCache.GetOrAdd(
- ModelMetadataIdentity.ForParameter(parameter),
+ ModelMetadataIdentity.ForParameter(parameter, modelType),
+ _cacheEntryFactory);
+ }
+
+ private ModelMetadataCacheEntry GetCacheEntry(PropertyInfo property, Type modelType)
+ {
+ return _typeCache.GetOrAdd(
+ ModelMetadataIdentity.ForProperty(modelType, property.Name, property.DeclaringType),
_cacheEntryFactory);
}
@@ -165,6 +201,10 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
details = CreateParameterDetails(key);
}
+ else if (key.MetadataKind == ModelMetadataKind.Property)
+ {
+ details = CreateSinglePropertyDetails(key);
+ }
else
{
details = CreateTypeDetails(key);
@@ -174,6 +214,22 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
return new ModelMetadataCacheEntry(metadata, details);
}
+ private DefaultMetadataDetails CreateSinglePropertyDetails(ModelMetadataIdentity propertyKey)
+ {
+ var propertyHelpers = PropertyHelper.GetVisibleProperties(propertyKey.ContainerType);
+ for (var i = 0; i < propertyHelpers.Length; i++)
+ {
+ var propertyHelper = propertyHelpers[i];
+ if (propertyHelper.Name == propertyKey.Name)
+ {
+ return CreateSinglePropertyDetails(propertyKey, propertyHelper);
+ }
+ }
+
+ Debug.Fail($"Unable to find property '{propertyKey.Name}' on type '{propertyKey.ContainerType}.");
+ return null;
+ }
+
private ModelMetadataCacheEntry GetMetadataCacheEntryForObjectType()
{
var key = ModelMetadataIdentity.ForType(typeof(object));
@@ -217,35 +273,48 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
for (var i = 0; i < propertyHelpers.Length; i++)
{
var propertyHelper = propertyHelpers[i];
+
var propertyKey = ModelMetadataIdentity.ForProperty(
propertyHelper.Property.PropertyType,
propertyHelper.Name,
key.ModelType);
- var attributes = ModelAttributes.GetAttributesForProperty(
- key.ModelType,
- propertyHelper.Property);
-
- var propertyEntry = new DefaultMetadataDetails(propertyKey, attributes);
- if (propertyHelper.Property.CanRead && propertyHelper.Property.GetMethod?.IsPublic == true)
- {
- var getter = PropertyHelper.MakeNullSafeFastPropertyGetter(propertyHelper.Property);
- propertyEntry.PropertyGetter = getter;
- }
-
- if (propertyHelper.Property.CanWrite &&
- propertyHelper.Property.SetMethod?.IsPublic == true &&
- !key.ModelType.GetTypeInfo().IsValueType)
- {
- propertyEntry.PropertySetter = propertyHelper.ValueSetter;
- }
-
+ var propertyEntry = CreateSinglePropertyDetails(propertyKey, propertyHelper);
propertyEntries.Add(propertyEntry);
}
return propertyEntries.ToArray();
}
+ private DefaultMetadataDetails CreateSinglePropertyDetails(
+ ModelMetadataIdentity propertyKey,
+ PropertyHelper propertyHelper)
+ {
+ Debug.Assert(propertyKey.MetadataKind == ModelMetadataKind.Property);
+ var containerType = propertyKey.ContainerType;
+
+ var attributes = ModelAttributes.GetAttributesForProperty(
+ containerType,
+ propertyHelper.Property,
+ propertyKey.ModelType);
+
+ var propertyEntry = new DefaultMetadataDetails(propertyKey, attributes);
+ if (propertyHelper.Property.CanRead && propertyHelper.Property.GetMethod?.IsPublic == true)
+ {
+ var getter = PropertyHelper.MakeNullSafeFastPropertyGetter(propertyHelper.Property);
+ propertyEntry.PropertyGetter = getter;
+ }
+
+ if (propertyHelper.Property.CanWrite &&
+ propertyHelper.Property.SetMethod?.IsPublic == true &&
+ !containerType.IsValueType)
+ {
+ propertyEntry.PropertySetter = propertyHelper.ValueSetter;
+ }
+
+ return propertyEntry;
+ }
+
///
/// Creates the entry for a model .
///
@@ -269,7 +338,7 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding.Metadata
{
return new DefaultMetadataDetails(
key,
- ModelAttributes.GetAttributesForParameter(key.ParameterInfo));
+ ModelAttributes.GetAttributesForParameter(key.ParameterInfo, key.ModelType));
}
private class TypeCache : ConcurrentDictionary
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs
index e5b59933ae..0f9a5bf103 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/Metadata/ModelAttributes.cs
@@ -135,9 +135,25 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
///
public static ModelAttributes GetAttributesForProperty(Type type, PropertyInfo property)
{
- if (type == null)
+ return GetAttributesForProperty(type, property, property.PropertyType);
+ }
+
+ ///
+ /// Gets the attributes for the given with the specified .
+ ///
+ /// The in which caller found .
+ ///
+ /// A for which attributes need to be resolved.
+ ///
+ /// The model type
+ ///
+ /// A instance with the attributes of the property and its .
+ ///
+ public static ModelAttributes GetAttributesForProperty(Type containerType, PropertyInfo property, Type modelType)
+ {
+ if (containerType == null)
{
- throw new ArgumentNullException(nameof(type));
+ throw new ArgumentNullException(nameof(containerType));
}
if (property == null)
@@ -146,9 +162,9 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
}
var propertyAttributes = property.GetCustomAttributes();
- var typeAttributes = property.PropertyType.GetTypeInfo().GetCustomAttributes();
+ var typeAttributes = modelType.GetCustomAttributes();
- var metadataType = GetMetadataType(type);
+ var metadataType = GetMetadataType(containerType);
if (metadataType != null)
{
var metadataProperty = metadataType.GetRuntimeProperty(property.Name);
@@ -174,12 +190,12 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
throw new ArgumentNullException(nameof(type));
}
- var attributes = type.GetTypeInfo().GetCustomAttributes();
+ var attributes = type.GetCustomAttributes();
var metadataType = GetMetadataType(type);
if (metadataType != null)
{
- attributes = attributes.Concat(metadataType.GetTypeInfo().GetCustomAttributes());
+ attributes = attributes.Concat(metadataType.GetCustomAttributes());
}
return new ModelAttributes(attributes, propertyAttributes: null, parameterAttributes: null);
@@ -205,9 +221,40 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
return new ModelAttributes(typeAttributes, propertyAttributes: null, parameterAttributes);
}
+ ///
+ /// Gets the attributes for the given with the specified .
+ ///
+ ///
+ /// The for which attributes need to be resolved.
+ ///
+ /// The model type.
+ ///
+ /// A instance with the attributes of the parameter and its .
+ ///
+ public static ModelAttributes GetAttributesForParameter(ParameterInfo parameterInfo, Type modelType)
+ {
+ if (parameterInfo == null)
+ {
+ throw new ArgumentNullException(nameof(parameterInfo));
+ }
+
+ if (modelType == null)
+ {
+ throw new ArgumentNullException(nameof(modelType));
+ }
+
+ // Prior versions called IModelMetadataProvider.GetMetadataForType(...) and therefore
+ // GetAttributesForType(...) for parameters. Maintain that set of attributes (including those from an
+ // ModelMetadataTypeAttribute reference) for back-compatibility.
+ var typeAttributes = GetAttributesForType(modelType).TypeAttributes;
+ var parameterAttributes = parameterInfo.GetCustomAttributes();
+
+ return new ModelAttributes(typeAttributes, propertyAttributes: null, parameterAttributes);
+ }
+
private static Type GetMetadataType(Type type)
{
- return type.GetTypeInfo().GetCustomAttribute()?.MetadataType;
+ return type.GetCustomAttribute()?.MetadataType;
}
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs
index 973058609b..51ed9c3034 100644
--- a/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs
+++ b/src/Microsoft.AspNetCore.Mvc.Core/ModelBinding/ParameterBinder.cs
@@ -4,6 +4,7 @@
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.Internal;
using Microsoft.AspNetCore.Mvc.ModelBinding.Validation;
using Microsoft.Extensions.Logging;
@@ -283,6 +284,8 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
ModelBindingContext modelBindingContext,
ModelBindingResult modelBindingResult)
{
+ RecalculateModelMetadata(parameter, modelBindingResult, ref metadata);
+
if (!modelBindingResult.IsModelSet && metadata.IsBindingRequired)
{
// Enforce BindingBehavior.Required (e.g., [BindRequired])
@@ -330,5 +333,40 @@ namespace Microsoft.AspNetCore.Mvc.ModelBinding
metadata);
}
}
+
+ private void RecalculateModelMetadata(
+ ParameterDescriptor parameter,
+ ModelBindingResult modelBindingResult,
+ ref ModelMetadata metadata)
+ {
+ // Attempt to recalculate ModelMetadata for top level parameters and properties using the actual
+ // model type. This ensures validation uses a combination of top-level validation metadata
+ // as well as metadata on the actual, rather than declared, model type.
+
+ if (!modelBindingResult.IsModelSet ||
+ modelBindingResult.Model == null ||
+ !(_modelMetadataProvider is ModelMetadataProvider modelMetadataProvider))
+ {
+ return;
+ }
+
+ var modelType = modelBindingResult.Model.GetType();
+ if (parameter is IParameterInfoParameterDescriptor parameterInfoParameter)
+ {
+ var parameterInfo = parameterInfoParameter.ParameterInfo;
+ if (modelType != parameterInfo.ParameterType)
+ {
+ metadata = modelMetadataProvider.GetMetadataForParameter(parameterInfo, modelType);
+ }
+ }
+ else if (parameter is IPropertyInfoParameterDescriptor propertyInfoParameter)
+ {
+ var propertyInfo = propertyInfoParameter.PropertyInfo;
+ if (modelType != propertyInfo.PropertyType)
+ {
+ metadata = modelMetadataProvider.GetMetadataForProperty(propertyInfo, modelType);
+ }
+ }
+ }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedObjectResult.cs b/src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedObjectResult.cs
new file mode 100644
index 0000000000..ebab2df60f
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Mvc.Core/UnauthorizedObjectResult.cs
@@ -0,0 +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 Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
+
+namespace Microsoft.AspNetCore.Mvc
+{
+ ///
+ /// An that when executed will produce a Unauthorized (401) response.
+ ///
+ [DefaultStatusCode(DefaultStatusCode)]
+ public class UnauthorizedObjectResult : ObjectResult
+ {
+ private const int DefaultStatusCode = StatusCodes.Status401Unauthorized;
+
+ ///
+ /// Creates a new instance.
+ ///
+ public UnauthorizedObjectResult(object value) : base(value)
+ {
+ StatusCode = DefaultStatusCode;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs
index dcf212e077..099b86192c 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/HandlerParameterDescriptor.cs
@@ -3,11 +3,15 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
- public class HandlerParameterDescriptor : ParameterDescriptor
+ public class HandlerParameterDescriptor : ParameterDescriptor, IParameterInfoParameterDescriptor
{
+ ///
+ /// Gets or sets the .
+ ///
public ParameterInfo ParameterInfo { get; set; }
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs
index 56cbdc40f7..cb02222300 100644
--- a/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs
+++ b/src/Microsoft.AspNetCore.Mvc.RazorPages/Infrastructure/PageBoundPropertyDescriptor.cs
@@ -3,11 +3,17 @@
using System.Reflection;
using Microsoft.AspNetCore.Mvc.Abstractions;
+using Microsoft.AspNetCore.Mvc.Infrastructure;
namespace Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure
{
- public class PageBoundPropertyDescriptor : ParameterDescriptor
+ public class PageBoundPropertyDescriptor : ParameterDescriptor, IPropertyInfoParameterDescriptor
{
+ ///
+ /// Gets or sets the for this property.
+ ///
public PropertyInfo Property { get; set; }
+
+ PropertyInfo IPropertyInfoParameterDescriptor.PropertyInfo => Property;
}
}
diff --git a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs
index 1a463966cf..fef6b7c0e8 100644
--- a/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs
+++ b/src/Microsoft.AspNetCore.Mvc.TagHelpers/PartialTagHelper.cs
@@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
private const string ForAttributeName = "for";
private const string ModelAttributeName = "model";
+ private const string OptionalAttributeName = "optional";
private object _model;
private bool _hasModel;
private bool _hasFor;
@@ -74,6 +75,13 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
}
}
+ ///
+ /// When optional, executing the tag helper will no-op if the view cannot be located.
+ /// Otherwise will throw stating the view could not be found.
+ ///
+ [HtmlAttributeName(OptionalAttributeName)]
+ public bool Optional { get; set; }
+
///
/// A to pass into the partial view.
///
@@ -96,16 +104,21 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
throw new ArgumentNullException(nameof(context));
}
- var model = ResolveModel();
- var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize);
- using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8))
- {
- await RenderPartialViewAsync(writer, model);
+ var viewEngineResult = FindView();
- // Reset the TagName. We don't want `partial` to render.
- output.TagName = null;
- output.Content.SetHtmlContent(viewBuffer);
+ if (viewEngineResult.Success)
+ {
+ var model = ResolveModel();
+ var viewBuffer = new ViewBuffer(_viewBufferScope, Name, ViewBuffer.PartialViewPageSize);
+ using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8))
+ {
+ await RenderPartialViewAsync(writer, model, viewEngineResult.View);
+ output.Content.SetHtmlContent(viewBuffer);
+ }
}
+
+ // Reset the TagName. We don't want `partial` to render.
+ output.TagName = null;
}
// Internal for testing
@@ -139,7 +152,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
return ViewContext.ViewData.Model;
}
- private async Task RenderPartialViewAsync(TextWriter writer, object model)
+ private ViewEngineResult FindView()
{
var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, Name, isMainPage: false);
var getViewLocations = viewEngineResult.SearchedLocations;
@@ -148,7 +161,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
viewEngineResult = _viewEngine.FindView(ViewContext, Name, isMainPage: false);
}
- if (!viewEngineResult.Success)
+ if (!viewEngineResult.Success && !Optional)
{
var searchedLocations = Enumerable.Concat(getViewLocations, viewEngineResult.SearchedLocations);
var locations = string.Empty;
@@ -161,7 +174,11 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Resources.FormatViewEngine_PartialViewNotFound(Name, locations));
}
- var view = viewEngineResult.View;
+ return viewEngineResult;
+ }
+
+ private async Task RenderPartialViewAsync(TextWriter writer, object model, IView view)
+ {
// Determine which ViewData we should use to construct a new ViewData
var baseViewData = ViewData ?? ViewContext.ViewData;
var newViewData = new ViewDataDictionary