Merge branch 'merge/release/2.2-to-master'

# Conflicts:
#	build/dependencies.props
#	korebuild-lock.txt
This commit is contained in:
Kiran Challa 2018-07-10 14:04:43 -07:00
commit 5a7a3bfcb3
33 changed files with 1022 additions and 110 deletions

View File

@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
_mvcOptions = mvcOptions;
}
public IList<ApiResponseType> GetApiResponseTypes(ControllerActionDescriptor action)
public ICollection<ApiResponseType> GetApiResponseTypes(ControllerActionDescriptor action)
{
// We only provide response info if we can figure out a type that is a user-data type.
// Void /Task object/IActionResult will result in no data.
@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
var runtimeReturnType = GetRuntimeReturnType(declaredReturnType);
var responseMetadataAttributes = GetResponseMetadataAttributes(action);
if (responseMetadataAttributes.Count == 0 &&
if (responseMetadataAttributes.Count == 0 &&
action.Properties.TryGetValue(typeof(ApiConventionResult), out var result))
{
// Action does not have any conventions. Use conventions on it if present.
@ -67,14 +67,11 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
.ToArray();
}
private IList<ApiResponseType> GetApiResponseTypes(
private ICollection<ApiResponseType> GetApiResponseTypes(
IReadOnlyList<IApiResponseMetadataProvider> responseMetadataAttributes,
Type type)
{
var results = new List<ApiResponseType>();
// Build list of all possible return types (and status codes) for an action.
var objectTypes = new Dictionary<int, Type>();
var results = new Dictionary<int, ApiResponseType>();
// Get the content type that the action explicitly set to support.
// Walk through all 'filter' attributes in order, and allow each one to see or override
@ -86,7 +83,17 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
metadataAttribute.SetContentTypes(contentTypes);
if (metadataAttribute.Type == typeof(void) &&
ApiResponseType apiResponseType;
if (metadataAttribute is IApiDefaultResponseMetadataProvider)
{
apiResponseType = new ApiResponseType
{
IsDefaultResponse = true,
Type = metadataAttribute.Type,
};
}
else if (metadataAttribute.Type == typeof(void) &&
type != null &&
(metadataAttribute.StatusCode == StatusCodes.Status200OK || metadataAttribute.StatusCode == StatusCodes.Status201Created))
{
@ -94,20 +101,38 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
// In this event, use the action's return type for 200 or 201 status codes. This lets you decorate an action with a
// [ProducesResponseType(201)] instead of [ProducesResponseType(201, typeof(Person)] when typeof(Person) can be inferred
// from the return type.
objectTypes[metadataAttribute.StatusCode] = type;
apiResponseType = new ApiResponseType
{
StatusCode = metadataAttribute.StatusCode,
Type = type,
};
}
else if (metadataAttribute.Type != null)
{
objectTypes[metadataAttribute.StatusCode] = metadataAttribute.Type;
apiResponseType = new ApiResponseType
{
StatusCode = metadataAttribute.StatusCode,
Type = metadataAttribute.Type,
};
}
else
{
continue;
}
results[apiResponseType.StatusCode] = apiResponseType;
}
}
// Set the default status only when no status has already been set explicitly
if (objectTypes.Count == 0 && type != null)
if (results.Count == 0 && type != null)
{
objectTypes[StatusCodes.Status200OK] = type;
results[StatusCodes.Status200OK] = new ApiResponseType
{
StatusCode = StatusCodes.Status200OK,
Type = type,
};
}
if (contentTypes.Count == 0)
@ -117,25 +142,15 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
var responseTypeMetadataProviders = _mvcOptions.OutputFormatters.OfType<IApiResponseTypeMetadataProvider>();
foreach (var objectType in objectTypes)
foreach (var apiResponse in results.Values)
{
if (objectType.Value == null || objectType.Value == typeof(void))
var responseType = apiResponse.Type;
if (responseType == null || responseType == typeof(void))
{
results.Add(new ApiResponseType()
{
StatusCode = objectType.Key,
Type = objectType.Value
});
continue;
}
var apiResponseType = new ApiResponseType()
{
Type = objectType.Value,
StatusCode = objectType.Key,
ModelMetadata = _modelMetadataProvider.GetMetadataForType(objectType.Value)
};
apiResponse.ModelMetadata = _modelMetadataProvider.GetMetadataForType(responseType);
foreach (var contentType in contentTypes)
{
@ -143,7 +158,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
var formatterSupportedContentTypes = responseTypeMetadataProvider.GetSupportedContentTypes(
contentType,
objectType.Value);
responseType);
if (formatterSupportedContentTypes == null)
{
@ -152,7 +167,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
foreach (var formatterSupportedContentType in formatterSupportedContentTypes)
{
apiResponseType.ApiResponseFormats.Add(new ApiResponseFormat()
apiResponse.ApiResponseFormats.Add(new ApiResponseFormat
{
Formatter = (IOutputFormatter)responseTypeMetadataProvider,
MediaType = formatterSupportedContentType,
@ -160,11 +175,9 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
}
}
}
results.Add(apiResponseType);
}
return results;
return results.Values;
}
private Type GetDeclaredReturnType(ControllerActionDescriptor action)

View File

@ -74,7 +74,7 @@ namespace Microsoft.AspNetCore.Mvc
var errorMessage = Resources.FormatApiConvention_UnsupportedAttributesOnConvention(
methodDisplayName,
Environment.NewLine + string.Join(Environment.NewLine, unsupportedAttributes) + Environment.NewLine,
$"{nameof(ProducesResponseTypeAttribute)}, {nameof(ApiConventionNameMatchAttribute)}");
$"{nameof(ProducesResponseTypeAttribute)}, {nameof(ProducesDefaultResponseTypeAttribute)}, {nameof(ApiConventionNameMatchAttribute)}");
throw new ArgumentException(errorMessage, nameof(conventionType));
}
@ -83,6 +83,7 @@ namespace Microsoft.AspNetCore.Mvc
private static bool IsAllowedAttribute(object attribute)
{
return attribute is ProducesResponseTypeAttribute ||
attribute is ProducesDefaultResponseTypeAttribute ||
attribute is ApiConventionNameMatchAttribute;
}
}

View File

@ -0,0 +1,12 @@
// 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.
namespace Microsoft.AspNetCore.Mvc.ApiExplorer
{
/// <summary>
/// Provides a return type for all HTTP status codes that are not covered by other <see cref="IApiResponseMetadataProvider"/> instances.
/// </summary>
public interface IApiDefaultResponseMetadataProvider : IApiResponseMetadataProvider
{
}
}

View File

@ -8,25 +8,53 @@ namespace Microsoft.AspNetCore.Mvc
{
public static class DefaultApiConventions
{
#region GET
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Get(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
object id) { }
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesDefaultResponseType]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Find(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
object id)
{ }
#endregion
#region POST
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Post(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
object model) { }
[ProducesResponseType(StatusCodes.Status201Created)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Create(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
object model)
{ }
#endregion
#region PUT
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Put(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
@ -37,13 +65,47 @@ namespace Microsoft.AspNetCore.Mvc
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
object model) { }
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Edit(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
object id,
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
object model)
{ }
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Update(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
object id,
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Any)]
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
object model)
{ }
#endregion
#region DELETE
[ProducesResponseType(StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
[ProducesResponseType(StatusCodes.Status400BadRequest)]
[ProducesDefaultResponseType]
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Prefix)]
public static void Delete(
[ApiConventionNameMatch(ApiConventionNameMatchBehavior.Suffix)]
[ApiConventionTypeMatch(ApiConventionTypeMatchBehavior.Any)]
object id) { }
#endregion
}
}

View File

@ -88,7 +88,7 @@ namespace Microsoft.AspNetCore.Mvc.Internal
values[i] = value as string ?? Convert.ToString(value);
}
}
if (cache.OrdinalEntries.TryGetValue(values, out var matchingRouteValues) ||
cache.OrdinalIgnoreCaseEntries.TryGetValue(values, out matchingRouteValues))
{
@ -441,7 +441,14 @@ namespace Microsoft.AspNetCore.Mvc.Internal
var hash = new HashCodeCombiner();
for (var i = 0; i < obj.Length; i++)
{
hash.Add(obj[i], _valueComparer);
var o = obj[i];
// Route values define null and "" to be equivalent.
if (string.IsNullOrEmpty(o))
{
o = null;
}
hash.Add(o, _valueComparer);
}
return hash.CombinedHash;

View File

@ -0,0 +1,49 @@
// 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.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace Microsoft.AspNetCore.Mvc
{
/// <summary>
/// A filter that specifies the <see cref="System.Type"/> for all HTTP status codes that are not covered by <see cref="ProducesResponseTypeAttribute"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public sealed class ProducesDefaultResponseTypeAttribute : Attribute, IApiDefaultResponseMetadataProvider
{
/// <summary>
/// Initializes an instance of <see cref="ProducesResponseTypeAttribute"/>.
/// </summary>
public ProducesDefaultResponseTypeAttribute()
: this(typeof(void))
{
}
/// <summary>
/// Initializes an instance of <see cref="ProducesResponseTypeAttribute"/>.
/// </summary>
/// <param name="type">The <see cref="Type"/> of object that is going to be written in the response.</param>
public ProducesDefaultResponseTypeAttribute(Type type)
{
Type = type ?? throw new ArgumentNullException(nameof(type));
}
/// <summary>
/// Gets or sets the type of the value returned by an action.
/// </summary>
public Type Type { get; }
/// <summary>
/// Gets or sets the HTTP status code of the response.
/// </summary>
public int StatusCode { get; }
/// <inheritdoc />
void IApiResponseMetadataProvider.SetContentTypes(MediaTypeCollection contentTypes)
{
// Users are supposed to use the 'Produces' attribute to set the content types that an action can support.
}
}
}

View File

@ -1214,6 +1214,33 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
public virtual UnauthorizedResult Unauthorized()
=> new UnauthorizedResult();
/// <summary>
/// Creates a <see cref="PartialViewResult"/> by specifying the name of a partial to render.
/// </summary>
/// <param name="viewName">The partial name.</param>
/// <returns>The created <see cref="PartialViewResult"/> object for the response.</returns>
public virtual PartialViewResult Partial(string viewName)
{
return Partial(viewName, model: null);
}
/// <summary>
/// Creates a <see cref="PartialViewResult"/> by specifying the name of a partial to render and the model object.
/// </summary>
/// <param name="viewName">The partial name.</param>
/// <param name="model">The model to be passed into the partial.</param>
/// <returns>The created <see cref="PartialViewResult"/> object for the response.</returns>
public virtual PartialViewResult Partial(string viewName, object model)
{
ViewContext.ViewData.Model = model;
return new PartialViewResult
{
ViewName = viewName,
ViewData = ViewContext.ViewData
};
}
#region ViewComponentResult
/// <summary>
/// Creates a <see cref="ViewComponentResult"/> by specifying the name of a view component to render.

View File

@ -1614,6 +1614,33 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
public virtual UnauthorizedResult Unauthorized()
=> new UnauthorizedResult();
/// <summary>
/// Creates a <see cref="PartialViewResult"/> by specifying the name of a partial to render.
/// </summary>
/// <param name="viewName">The partial name.</param>
/// <returns>The created <see cref="PartialViewResult"/> object for the response.</returns>
public virtual PartialViewResult Partial(string viewName)
{
return Partial(viewName, model: null);
}
/// <summary>
/// Creates a <see cref="PartialViewResult"/> by specifying the name of a partial to render and the model object.
/// </summary>
/// <param name="viewName">The partial name.</param>
/// <param name="model">The model to be passed into the partial.</param>
/// <returns>The created <see cref="PartialViewResult"/> object for the response.</returns>
public virtual PartialViewResult Partial(string viewName, object model)
{
ViewData.Model = model;
return new PartialViewResult
{
ViewName = viewName,
ViewData = ViewData
};
}
#region ViewComponentResult
/// <summary>
/// Creates a <see cref="ViewComponentResult"/> by specifying the name of a view component to render.

View File

@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
{
private const string ForAttributeName = "for";
private const string ModelAttributeName = "model";
private const string FallbackAttributeName = "fallback-name";
private const string OptionalAttributeName = "optional";
private object _model;
private bool _hasModel;
@ -82,6 +83,12 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
[HtmlAttributeName(OptionalAttributeName)]
public bool Optional { get; set; }
/// <summary>
/// View to lookup if the view specified by <see cref="Name"/> cannot be located.
/// </summary>
[HtmlAttributeName(FallbackAttributeName)]
public string FallbackName { get; set; }
/// <summary>
/// A <see cref="ViewDataDictionary"/> to pass into the partial view.
/// </summary>
@ -104,21 +111,46 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
throw new ArgumentNullException(nameof(context));
}
var viewEngineResult = FindView();
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;
var result = FindView(Name);
var viewSearchedLocations = result.SearchedLocations;
var fallBackViewSearchedLocations = Enumerable.Empty<string>();
if (!result.Success && !string.IsNullOrEmpty(FallbackName))
{
result = FindView(FallbackName);
fallBackViewSearchedLocations = result.SearchedLocations;
}
if (!result.Success)
{
if (Optional)
{
// Could not find the view or fallback view, but the partial is marked as optional.
return;
}
var locations = Environment.NewLine + string.Join(Environment.NewLine, viewSearchedLocations);
var errorMessage = Resources.FormatViewEngine_PartialViewNotFound(Name, locations);
if (!string.IsNullOrEmpty(FallbackName))
{
locations = Environment.NewLine + string.Join(Environment.NewLine, result.SearchedLocations);
errorMessage += Environment.NewLine + Resources.FormatViewEngine_FallbackViewNotFound(FallbackName, locations);
}
throw new InvalidOperationException(errorMessage);
}
var model = ResolveModel();
var viewBuffer = new ViewBuffer(_viewBufferScope, result.ViewName, ViewBuffer.PartialViewPageSize);
using (var writer = new ViewBufferTextWriter(viewBuffer, Encoding.UTF8))
{
await RenderPartialViewAsync(writer, model, result.View);
output.Content.SetHtmlContent(viewBuffer);
}
}
// Internal for testing
@ -152,26 +184,19 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
return ViewContext.ViewData.Model;
}
private ViewEngineResult FindView()
private ViewEngineResult FindView(string partialName)
{
var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, Name, isMainPage: false);
var viewEngineResult = _viewEngine.GetView(ViewContext.ExecutingFilePath, partialName, isMainPage: false);
var getViewLocations = viewEngineResult.SearchedLocations;
if (!viewEngineResult.Success)
{
viewEngineResult = _viewEngine.FindView(ViewContext, Name, isMainPage: false);
viewEngineResult = _viewEngine.FindView(ViewContext, partialName, isMainPage: false);
}
if (!viewEngineResult.Success && !Optional)
if (!viewEngineResult.Success)
{
var searchedLocations = Enumerable.Concat(getViewLocations, viewEngineResult.SearchedLocations);
var locations = string.Empty;
if (searchedLocations.Any())
{
locations += Environment.NewLine + string.Join(Environment.NewLine, searchedLocations);
}
throw new InvalidOperationException(
Resources.FormatViewEngine_PartialViewNotFound(Name, locations));
return ViewEngineResult.NotFound(partialName, searchedLocations);
}
return viewEngineResult;

View File

@ -206,6 +206,20 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
internal static string FormatPartialTagHelper_InvalidModelAttributes(object p0, object p1, object p2)
=> string.Format(CultureInfo.CurrentCulture, GetString("PartialTagHelper_InvalidModelAttributes"), p0, p1, p2);
/// <summary>
/// The fallback partial view '{0}' was not found. The following locations were searched:{1}
/// </summary>
internal static string ViewEngine_FallbackViewNotFound
{
get => GetString("ViewEngine_FallbackViewNotFound");
}
/// <summary>
/// The fallback partial view '{0}' was not found. The following locations were searched:{1}
/// </summary>
internal static string FormatViewEngine_FallbackViewNotFound(object p0, object p1)
=> string.Format(CultureInfo.CurrentCulture, GetString("ViewEngine_FallbackViewNotFound"), p0, p1);
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);

View File

@ -159,4 +159,7 @@
<data name="PartialTagHelper_InvalidModelAttributes" xml:space="preserve">
<value>Cannot use '{0}' with both '{1}' and '{2}' attributes.</value>
</data>
<data name="ViewEngine_FallbackViewNotFound" xml:space="preserve">
<value>The fallback partial view '{0}' was not found. The following locations were searched:{1}</value>
</data>
</root>

View File

@ -72,6 +72,60 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
public Task<ActionResult<BaseModel>> Get(int id) => null;
}
[Fact]
public void GetApiResponseTypes_CombinesFilters()
{
// Arrange
var filterDescriptors = new[]
{
new FilterDescriptor(new ProducesResponseTypeAttribute(400), FilterScope.Global),
new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(object), 201), FilterScope.Controller),
new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(ProblemDetails), 400), FilterScope.Controller),
new FilterDescriptor(new ProducesResponseTypeAttribute(typeof(BaseModel), 201), FilterScope.Action),
new FilterDescriptor(new ProducesResponseTypeAttribute(404), FilterScope.Action),
};
var actionDescriptor = new ControllerActionDescriptor
{
FilterDescriptors = filterDescriptors,
MethodInfo = typeof(GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresentController).GetMethod(nameof(GetApiResponseTypes_ReturnsResponseTypesFromActionIfPresentController.Get)),
};
var provider = GetProvider();
// Act
var result = provider.GetApiResponseTypes(actionDescriptor);
// Assert
Assert.Collection(
result.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.Equal(201, responseType.StatusCode);
Assert.Equal(typeof(BaseModel), responseType.Type);
Assert.False(responseType.IsDefaultResponse);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(400, responseType.StatusCode);
Assert.Equal(typeof(ProblemDetails), responseType.Type);
Assert.False(responseType.IsDefaultResponse);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(404, responseType.StatusCode);
Assert.Equal(typeof(void), responseType.Type);
Assert.False(responseType.IsDefaultResponse);
Assert.Empty(responseType.ApiResponseFormats);
});
}
[Fact]
public void GetApiResponseTypes_ReturnsResponseTypesFromApiConventionItem()
{
@ -159,6 +213,54 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
public Task<ActionResult<BaseModel>> PostModel(int id, BaseModel model) => null;
}
[Fact]
public void GetApiResponseTypes_ReturnsDefaultProblemResponse()
{
// Arrange
var actionDescriptor = GetControllerActionDescriptor(
typeof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController),
nameof(GetApiResponseTypes_ReturnsResponseTypesFromDefaultConventionsController.DeleteBase));
actionDescriptor.Properties[typeof(ApiConventionResult)] = new ApiConventionResult(new IApiResponseMetadataProvider[]
{
new ProducesResponseTypeAttribute(201),
new ProducesResponseTypeAttribute(404),
new ProducesDefaultResponseTypeAttribute(typeof(SerializableError)),
});
var provider = GetProvider();
// Act
var result = provider.GetApiResponseTypes(actionDescriptor);
// Assert
Assert.Collection(
result.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.True(responseType.IsDefaultResponse);
Assert.Equal(typeof(SerializableError), responseType.Type);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(201, responseType.StatusCode);
Assert.Equal(typeof(BaseModel), responseType.Type);
Assert.False(responseType.IsDefaultResponse);
Assert.Collection(
responseType.ApiResponseFormats,
format => Assert.Equal("application/json", format.MediaType));
},
responseType =>
{
Assert.Equal(404, responseType.StatusCode);
Assert.Equal(typeof(void), responseType.Type);
Assert.False(responseType.IsDefaultResponse);
Assert.Empty(responseType.ApiResponseFormats);
});
}
private static ApiResponseTypeProvider GetProvider()
{
var mvcOptions = new MvcOptions

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 System.Linq;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Testing;
@ -15,11 +16,10 @@ namespace Microsoft.AspNetCore.Mvc
public void Constructor_ThrowsIfConventionMethodIsAnnotatedWithProducesAttribute()
{
// Arrange
var expected = $"Method {typeof(ConventionWithProducesAttribute).FullName + ".Get"} is decorated with the following attributes that are not allowed on an API convention method:" +
Environment.NewLine +
typeof(ProducesAttribute).FullName +
Environment.NewLine +
$"The following attributes are allowed on API convention methods: {nameof(ProducesResponseTypeAttribute)}, {nameof(ApiConventionNameMatchAttribute)}";
var methodName = typeof(ConventionWithProducesAttribute).FullName + '.' + nameof(ConventionWithProducesAttribute.Get);
var attribute = typeof(ProducesAttribute);
var expected = GetErrorMessage(methodName, attribute);
// Act & Assert
ExceptionAssert.ThrowsArgument(
@ -38,11 +38,9 @@ namespace Microsoft.AspNetCore.Mvc
public void Constructor_ThrowsIfConventionMethodHasRouteAttribute()
{
// Arrange
var expected = $"Method {typeof(ConventionWithRouteAttribute).FullName + ".Get"} is decorated with the following attributes that are not allowed on an API convention method:" +
Environment.NewLine +
typeof(HttpGetAttribute).FullName +
Environment.NewLine +
$"The following attributes are allowed on API convention methods: {nameof(ProducesResponseTypeAttribute)}, {nameof(ApiConventionNameMatchAttribute)}";
var methodName = typeof(ConventionWithRouteAttribute).FullName + '.' + nameof(ConventionWithRouteAttribute.Get);
var attribute = typeof(HttpGetAttribute);
var expected = GetErrorMessage(methodName, attribute);
// Act & Assert
ExceptionAssert.ThrowsArgument(
@ -61,11 +59,9 @@ namespace Microsoft.AspNetCore.Mvc
public void Constructor_ThrowsIfMultipleUnsupportedAttributesArePresentOnConvention()
{
// Arrange
var expected = $"Method {typeof(ConventionWitUnsupportedAttributes).FullName + ".Get"} is decorated with the following attributes that are not allowed on an API convention method:" +
Environment.NewLine +
string.Join(Environment.NewLine, typeof(ProducesAttribute).FullName, typeof(ServiceFilterAttribute).FullName, typeof(AuthorizeAttribute).FullName) +
Environment.NewLine +
$"The following attributes are allowed on API convention methods: {nameof(ProducesResponseTypeAttribute)}, {nameof(ApiConventionNameMatchAttribute)}";
var methodName = typeof(ConventionWitUnsupportedAttributes).FullName + '.' + nameof(ConventionWitUnsupportedAttributes.Get);
var attributes = new[] { typeof(ProducesAttribute), typeof(ServiceFilterAttribute), typeof(AuthorizeAttribute) };
var expected = GetErrorMessage(methodName, attributes);
// Act & Assert
ExceptionAssert.ThrowsArgument(
@ -82,5 +78,14 @@ namespace Microsoft.AspNetCore.Mvc
[Authorize]
public static void Get() { }
}
private static string GetErrorMessage(string methodName, params Type[] attributes)
{
return $"Method {methodName} is decorated with the following attributes that are not allowed on an API convention method:" +
Environment.NewLine +
string.Join(Environment.NewLine, attributes.Select(a => a.FullName)) +
Environment.NewLine +
$"The following attributes are allowed on API convention methods: {nameof(ProducesResponseTypeAttribute)}, {nameof(ProducesDefaultResponseTypeAttribute)}, {nameof(ApiConventionNameMatchAttribute)}";
}
}
}

View File

@ -111,6 +111,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
Assert.True(result);
Assert.Collection(
conventionResult.ResponseMetadataProviders.OrderBy(o => o.StatusCode),
r => Assert.IsAssignableFrom<IApiDefaultResponseMetadataProvider>(r),
r => Assert.Equal(200, r.StatusCode),
r => Assert.Equal(404, r.StatusCode));
}
@ -130,6 +131,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
Assert.True(result);
Assert.Collection(
conventionResult.ResponseMetadataProviders.OrderBy(o => o.StatusCode),
r => Assert.IsAssignableFrom<IApiDefaultResponseMetadataProvider>(r),
r => Assert.Equal(201, r.StatusCode),
r => Assert.Equal(400, r.StatusCode));
}
@ -152,6 +154,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
Assert.True(result);
Assert.Collection(
conventionResult.ResponseMetadataProviders.OrderBy(o => o.StatusCode),
r => Assert.IsAssignableFrom<IApiDefaultResponseMetadataProvider>(r),
r => Assert.Equal(204, r.StatusCode),
r => Assert.Equal(400, r.StatusCode),
r => Assert.Equal(404, r.StatusCode));
@ -175,6 +178,7 @@ namespace Microsoft.AspNetCore.Mvc.ApiExplorer
Assert.True(result);
Assert.Collection(
conventionResult.ResponseMetadataProviders.OrderBy(o => o.StatusCode),
r => Assert.IsAssignableFrom<IApiDefaultResponseMetadataProvider>(r),
r => Assert.Equal(200, r.StatusCode),
r => Assert.Equal(400, r.StatusCode),
r => Assert.Equal(404, r.StatusCode));

View File

@ -277,6 +277,176 @@ namespace Microsoft.AspNetCore.Mvc.Infrastructure
Assert.Equal(expected, candidates);
}
[Fact]
public void SelectCandidates_Match_CaseSensitiveMatch_MatchesOnEmptyString()
{
var actions = new ActionDescriptor[]
{
new ActionDescriptor()
{
DisplayName = "A1",
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "area", null },
{ "controller", "Home" },
{ "action", "Index" }
},
}
};
var selector = CreateSelector(actions);
var routeContext = CreateRouteContext("GET");
// Example: In conventional route, one could set non-inline defaults
// new { area = "", controller = "Home", action = "Index" }
routeContext.RouteData.Values.Add("area", "");
routeContext.RouteData.Values.Add("controller", "Home");
routeContext.RouteData.Values.Add("action", "Index");
// Act
var candidates = selector.SelectCandidates(routeContext);
// Assert
var action = Assert.Single(candidates);
Assert.Same(actions[0], action);
}
[Fact]
public void SelectCandidates_Match_CaseInsensitiveMatch_MatchesOnEmptyString()
{
var actions = new ActionDescriptor[]
{
new ActionDescriptor()
{
DisplayName = "A1",
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "area", null },
{ "controller", "Home" },
{ "action", "Index" }
},
}
};
var selector = CreateSelector(actions);
var routeContext = CreateRouteContext("GET");
// Example: In conventional route, one could set non-inline defaults
// new { area = "", controller = "Home", action = "Index" }
routeContext.RouteData.Values.Add("area", "");
routeContext.RouteData.Values.Add("controller", "HoMe");
routeContext.RouteData.Values.Add("action", "InDeX");
// Act
var candidates = selector.SelectCandidates(routeContext);
// Assert
var action = Assert.Single(candidates);
Assert.Same(actions[0], action);
}
[Fact]
public void SelectCandidates_Match_MatchesOnNull()
{
var actions = new ActionDescriptor[]
{
new ActionDescriptor()
{
DisplayName = "A1",
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "area", null },
{ "controller", "Home" },
{ "action", "Index" }
},
}
};
var selector = CreateSelector(actions);
var routeContext = CreateRouteContext("GET");
// Example: In conventional route, one could set non-inline defaults
// new { area = (string)null, controller = "Foo", action = "Index" }
routeContext.RouteData.Values.Add("area", null);
routeContext.RouteData.Values.Add("controller", "Home");
routeContext.RouteData.Values.Add("action", "Index");
// Act
var candidates = selector.SelectCandidates(routeContext);
// Assert
var action = Assert.Single(candidates);
Assert.Same(actions[0], action);
}
[Fact]
public void SelectCandidates_Match_ActionDescriptorWithEmptyRouteValues_MatchesOnEmptyString()
{
var actions = new ActionDescriptor[]
{
new ActionDescriptor()
{
DisplayName = "A1",
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "foo", "" },
{ "controller", "Home" },
{ "action", "Index" }
},
}
};
var selector = CreateSelector(actions);
var routeContext = CreateRouteContext("GET");
// Example: In conventional route, one could set non-inline defaults
// new { area = (string)null, controller = "Home", action = "Index" }
routeContext.RouteData.Values.Add("foo", "");
routeContext.RouteData.Values.Add("controller", "Home");
routeContext.RouteData.Values.Add("action", "Index");
// Act
var candidates = selector.SelectCandidates(routeContext);
// Assert
var action = Assert.Single(candidates);
Assert.Same(actions[0], action);
}
[Fact]
public void SelectCandidates_Match_ActionDescriptorWithEmptyRouteValues_MatchesOnNull()
{
var actions = new ActionDescriptor[]
{
new ActionDescriptor()
{
DisplayName = "A1",
RouteValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
{ "foo", "" },
{ "controller", "Home" },
{ "action", "Index" }
},
}
};
var selector = CreateSelector(actions);
var routeContext = CreateRouteContext("GET");
// Example: In conventional route, one could set non-inline defaults
// new { area = (string)null, controller = "Home", action = "Index" }
routeContext.RouteData.Values.Add("foo", null);
routeContext.RouteData.Values.Add("controller", "Home");
routeContext.RouteData.Values.Add("action", "Index");
// Act
var candidates = selector.SelectCandidates(routeContext);
// Assert
var action = Assert.Single(candidates);
Assert.Same(actions[0], action);
}
[Fact]
public void SelectBestCandidate_AmbiguousActions_LogIsCorrect()
{

View File

@ -1,10 +1,8 @@
// 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.Linq;
using System.Net.Http;
using AngleSharp.Dom.Html;
using AngleSharp.Parser.Html;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
@ -19,27 +17,7 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
var parser = new HtmlParser();
var htmlDocument = parser.Parse(htmlContent);
return RetrieveAntiforgeryToken(htmlDocument);
}
public static string RetrieveAntiforgeryToken(IHtmlDocument htmlDocument)
{
var hiddenInputs = htmlDocument.QuerySelectorAll("form input[type=hidden]");
foreach (var input in hiddenInputs)
{
if (!input.HasAttribute("name"))
{
continue;
}
var name = input.GetAttribute("name");
if (name == "__RequestVerificationToken" || name == "HtmlEncode[[__RequestVerificationToken]]")
{
return input.GetAttribute("value");
}
}
throw new Exception($"Antiforgery token could not be located in {htmlDocument.Source.Text}.");
return htmlDocument.RetrieveAntiforgeryToken();
}
public static CookieMetadata RetrieveAntiforgeryCookie(HttpResponseMessage response)

View File

@ -711,7 +711,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert
var description = Assert.Single(result);
Assert.Equal(2, description.SupportedResponseTypes.Count);
Assert.Collection(
description.SupportedResponseTypes.OrderBy(responseType => responseType.StatusCode),
@ -749,7 +748,6 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
// Assert
var description = Assert.Single(result);
Assert.Equal(2, description.SupportedResponseTypes.Count);
Assert.Collection(
description.SupportedResponseTypes.OrderBy(responseType => responseType.StatusCode),
@ -1171,6 +1169,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Collection(
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.True(responseType.IsDefaultResponse);
},
responseType =>
{
Assert.Equal(typeof(Product).FullName, responseType.ResponseType);
Assert.Equal(200, responseType.StatusCode);
@ -1255,6 +1257,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Collection(
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.True(responseType.IsDefaultResponse);
},
responseType =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(201, responseType.StatusCode);
@ -1283,6 +1289,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Collection(
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.True(responseType.IsDefaultResponse);
},
responseType =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(204, responseType.StatusCode);
@ -1316,6 +1326,10 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Collection(
description.SupportedResponseTypes.OrderBy(r => r.StatusCode),
responseType =>
{
Assert.True(responseType.IsDefaultResponse);
},
responseType =>
{
Assert.Equal(typeof(void).FullName, responseType.ResponseType);
Assert.Equal(200, responseType.StatusCode);

View File

@ -566,7 +566,7 @@ Products: Music Systems, Televisions (3)";
var document = await Client.GetHtmlDocumentAsync(url);
// Assert
var banner = QuerySelector(document, ".banner");
var banner = document.RequiredQuerySelector(".banner");
Assert.Equal("Some status message", banner.TextContent);
}
@ -581,19 +581,36 @@ Products: Music Systems, Televisions (3)";
var document = await Client.GetHtmlDocumentAsync(url);
// Assert
var banner = QuerySelector(document, ".banner");
var banner = document.RequiredQuerySelector(".banner");
Assert.Empty(banner.TextContent);
}
private static IElement QuerySelector(IHtmlDocument document, string selector)
[Fact]
public async Task PartialTagHelper_AllowsUsingFallback()
{
var element = document.QuerySelector(selector);
if (element == null)
{
throw new ArgumentException($"Document does not contain element that matches the selector {selector}: " + Environment.NewLine + document.DocumentElement.OuterHtml);
}
// Arrange
var url = "/Customer/PartialWithFallback";
return element;
// Act
var document = await Client.GetHtmlDocumentAsync(url);
// Assert
var content = document.RequiredQuerySelector("#content");
Assert.Equal("Hello from fallback", content.TextContent);
}
[Fact]
public async Task PartialTagHelper_AllowsUsingOptional()
{
// Arrange
var url = "/Customer/PartialWithOptional";
// Act
var document = await Client.GetHtmlDocumentAsync(url);
// Assert
var content = document.RequiredQuerySelector("#content");
Assert.Empty(content.TextContent);
}
private static HttpRequestMessage RequestWithLocale(string url, string locale)

View File

@ -0,0 +1,43 @@
// 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 AngleSharp.Dom;
using AngleSharp.Dom.Html;
namespace Microsoft.AspNetCore.Mvc.FunctionalTests
{
public static class IHtmlDocumentExtensions
{
public static IElement RequiredQuerySelector(this IHtmlDocument document, string selector)
{
var element = document.QuerySelector(selector);
if (element == null)
{
throw new ArgumentException($"Document does not contain element that matches the selector {selector}: " + Environment.NewLine + document.DocumentElement.OuterHtml);
}
return element;
}
public static string RetrieveAntiforgeryToken(this IHtmlDocument htmlDocument)
{
var hiddenInputs = htmlDocument.QuerySelectorAll("form input[type=hidden]");
foreach (var input in hiddenInputs)
{
if (!input.HasAttribute("name"))
{
continue;
}
var name = input.GetAttribute("name");
if (name == "__RequestVerificationToken" || name == "HtmlEncode[[__RequestVerificationToken]]")
{
return input.GetAttribute("value");
}
}
throw new Exception($"Antiforgery token could not be located in {htmlDocument.Source.Text}.");
}
}
}

View File

@ -11,7 +11,6 @@ using System.Net.Http.Headers;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Authorization;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Testing;
using Newtonsoft.Json.Linq;
@ -144,6 +143,26 @@ namespace Microsoft.AspNetCore.Mvc.FunctionalTests
Assert.Equal("CustomActionResult", content);
}
[Fact]
public async Task Page_Handler_ReturnPartialWithoutModel()
{
// Act
var document = await Client.GetHtmlDocumentAsync("RenderPartialWithoutModel");
var element = document.RequiredQuerySelector("#content");
Assert.Equal("Welcome, Guest", element.TextContent);
}
[Fact]
public async Task Page_Handler_ReturnPartialWithModel()
{
// Act
var document = await Client.GetHtmlDocumentAsync("RenderPartialWithModel");
var element = document.RequiredQuerySelector("#content");
Assert.Equal("Welcome, Admin", element.TextContent);
}
[Fact]
public async Task Page_Handler_AsyncReturnTypeImplementsIActionResult()
{

View File

@ -1894,6 +1894,52 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
testPageModel.Verify();
}
[Fact]
public void PartialView_WithName()
{
// Arrange
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
var pageModel = new TestPageModel
{
PageContext = new PageContext
{
ViewData = viewData
}
};
// Act
var result = pageModel.Partial("LoginStatus");
// Assert
Assert.NotNull(result);
Assert.Equal("LoginStatus", result.ViewName);
Assert.Same(viewData, result.ViewData);
}
[Fact]
public void PartialView_WithNameAndModel()
{
// Arrange
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
var pageModel = new TestPageModel
{
PageContext = new PageContext
{
ViewData = viewData
}
};
var model = new { Username = "Admin" };
// Act
var result = pageModel.Partial("LoginStatus", model);
// Assert
Assert.NotNull(result);
Assert.Equal("LoginStatus", result.ViewName);
Assert.Equal(model, result.Model);
Assert.Same(viewData, result.ViewData);
}
[Fact]
public void ViewComponent_WithName()
{

View File

@ -1697,6 +1697,52 @@ namespace Microsoft.AspNetCore.Mvc.RazorPages
Assert.Equal(statusCode, result.StatusCode);
}
[Fact]
public void PartialView_WithName()
{
// Arrange
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
var pageModel = new TestPage
{
ViewContext = new ViewContext
{
ViewData = viewData
}
};
// Act
var result = pageModel.Partial("LoginStatus");
// Assert
Assert.NotNull(result);
Assert.Equal("LoginStatus", result.ViewName);
Assert.Same(viewData, result.ViewData);
}
[Fact]
public void PartialView_WithNameAndModel()
{
// Arrange
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary());
var pageModel = new TestPage
{
ViewContext = new ViewContext
{
ViewData = viewData
}
};
var model = new { Username = "Admin" };
// Act
var result = pageModel.Partial("LoginStatus", model);
// Assert
Assert.NotNull(result);
Assert.Equal("LoginStatus", result.ViewName);
Assert.Equal(model, result.Model);
Assert.Same(viewData, result.ViewData);
}
[Fact]
public void ViewComponent_WithName()
{

View File

@ -648,6 +648,198 @@ namespace Microsoft.AspNetCore.Mvc.TagHelpers
Assert.Empty(content);
}
[Fact]
public async Task ProcessAsync_RendersMainPartial_If_FallbackIsSet_AndMainPartialIsFound()
{
// Arrange
var expected = "Hello from partial!";
var bufferScope = new TestViewBufferScope();
var partialName = "_Partial";
var fallbackName = "_Fallback";
var model = new object();
var viewContext = GetViewContext();
var view = new Mock<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
v.Writer.Write(expected);
})
.Returns(Task.CompletedTask);
var fallbackView = new Mock<IView>();
fallbackView.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
v.Writer.Write("Hello from fallback partial!");
})
.Returns(Task.CompletedTask);
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), partialName, false))
.Returns(ViewEngineResult.Found(partialName, view.Object));
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), fallbackName, false))
.Returns(ViewEngineResult.Found(fallbackName, fallbackView.Object));
var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
{
Name = partialName,
ViewContext = viewContext,
FallbackName = fallbackName
};
var tagHelperContext = GetTagHelperContext();
var output = GetTagHelperOutput();
// Act
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder());
Assert.Equal(expected, content);
}
[Fact]
public async Task ProcessAsync_IfHasFallback_Throws_When_MainPartialAndFallback_AreNotFound()
{
// Arrange
var bufferScope = new TestViewBufferScope();
var partialName = "_Partial";
var fallbackName = "_Fallback";
var expected = string.Join(
Environment.NewLine,
$"The partial view '{partialName}' was not found. The following locations were searched:",
"PartialNotFound1",
"PartialNotFound2",
"PartialNotFound3",
"PartialNotFound4",
$"The fallback partial view '{fallbackName}' was not found. The following locations were searched:",
"FallbackNotFound1",
"FallbackNotFound2",
"FallbackNotFound3",
"FallbackNotFound4");
var viewData = new ViewDataDictionary(new TestModelMetadataProvider(), new ModelStateDictionary());
var viewContext = GetViewContext();
var view = Mock.Of<IView>();
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), partialName, false))
.Returns(ViewEngineResult.NotFound(partialName, new[] { "PartialNotFound1", "PartialNotFound2" }));
viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
.Returns(ViewEngineResult.NotFound(partialName, new[] { $"PartialNotFound3", $"PartialNotFound4" }));
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), fallbackName, false))
.Returns(ViewEngineResult.NotFound(partialName, new[] { "FallbackNotFound1", "FallbackNotFound2" }));
viewEngine.Setup(v => v.FindView(viewContext, fallbackName, false))
.Returns(ViewEngineResult.NotFound(partialName, new[] { $"FallbackNotFound3", $"FallbackNotFound4" }));
var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
{
Name = partialName,
ViewContext = viewContext,
ViewData = viewData,
FallbackName = fallbackName
};
var tagHelperContext = GetTagHelperContext();
var output = GetTagHelperOutput();
// Act & Assert
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
() => tagHelper.ProcessAsync(tagHelperContext, output));
Assert.Equal(expected, exception.Message);
}
[Fact]
public async Task ProcessAsync_RendersFallbackView_If_MainIsNotFound_AndGetViewReturnsView()
{
// Arrange
var expected = "Hello from fallback!";
var bufferScope = new TestViewBufferScope();
var partialName = "_Partial";
var fallbackName = "_Fallback";
var model = new object();
var viewContext = GetViewContext();
var view = new Mock<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
v.Writer.Write(expected);
})
.Returns(Task.CompletedTask);
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), partialName, false))
.Returns(ViewEngineResult.NotFound(partialName, Array.Empty<string>()));
viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
.Returns(ViewEngineResult.NotFound(partialName, Array.Empty<string>()));
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), fallbackName, false))
.Returns(ViewEngineResult.Found(fallbackName, view.Object));
var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
{
Name = partialName,
ViewContext = viewContext,
FallbackName = fallbackName
};
var tagHelperContext = GetTagHelperContext();
var output = GetTagHelperOutput();
// Act
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder());
Assert.Equal(expected, content);
}
[Fact]
public async Task ProcessAsync_RendersFallbackView_If_MainIsNotFound_AndFindViewReturnsView()
{
// Arrange
var expected = "Hello from fallback!";
var bufferScope = new TestViewBufferScope();
var partialName = "_Partial";
var fallbackName = "_Fallback";
var model = new object();
var viewContext = GetViewContext();
var view = new Mock<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
v.Writer.Write(expected);
})
.Returns(Task.CompletedTask);
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), partialName, false))
.Returns(ViewEngineResult.NotFound(partialName, Array.Empty<string>()));
viewEngine.Setup(v => v.FindView(viewContext, partialName, false))
.Returns(ViewEngineResult.NotFound(partialName, Array.Empty<string>()));
viewEngine.Setup(v => v.GetView(It.IsAny<string>(), fallbackName, false))
.Returns(ViewEngineResult.NotFound(fallbackName, Array.Empty<string>()));
viewEngine.Setup(v => v.FindView(viewContext, fallbackName, false))
.Returns(ViewEngineResult.Found(fallbackName, view.Object));
var tagHelper = new PartialTagHelper(viewEngine.Object, bufferScope)
{
Name = partialName,
ViewContext = viewContext,
FallbackName = fallbackName
};
var tagHelperContext = GetTagHelperContext();
var output = GetTagHelperOutput();
// Act
await tagHelper.ProcessAsync(tagHelperContext, output);
// Assert
var content = HtmlContentUtilities.HtmlContentToString(output.Content, new HtmlTestEncoder());
Assert.Equal(expected, content);
}
private static ViewContext GetViewContext()
{
return new ViewContext(

View File

@ -0,0 +1,3 @@
@page
<partial name="_DoesNotExist" fallback-name="./_Fallback" />

View File

@ -0,0 +1,3 @@
@page
<div id="content"><partial name="_DoesNotExist" optional="true" /></div>

View File

@ -0,0 +1 @@
<span id="content">Hello from fallback</span>

View File

@ -0,0 +1 @@
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

View File

@ -0,0 +1,15 @@
// 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;
using Microsoft.AspNetCore.Mvc.RazorPages;
namespace RazorPagesWebSite
{
public class RenderPartialWithModel : PageModel
{
public IActionResult OnGet() => Partial("_PartialWithModel", this);
public string Username => "Admin";
}
}

View File

@ -0,0 +1,4 @@
@page
@model RazorPagesWebSite.RenderPartialWithModel
<p>The partial will be loaded here ...</p>

View File

@ -0,0 +1,5 @@
@page
@functions {
public IActionResult OnGet() => Partial("_PartialWithoutModel");
}

View File

@ -0,0 +1,3 @@
@model RazorPagesWebSite.RenderPartialWithModel
<span id="content">Welcome, @Model.Username</span>

View File

@ -0,0 +1 @@
<span id="content">Welcome, Guest</span>

View File

@ -1,4 +1,4 @@
<Project>
<Project>
<PropertyGroup>
<VersionPrefix>3.0.0</VersionPrefix>
<VersionSuffix>alpha1</VersionSuffix>