|
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#nullable enable
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ViewEngines;
using Microsoft.Extensions.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Mvc.ViewFeatures;
/// <summary>
/// Finds and executes an <see cref="IView"/> for a <see cref="PartialViewResult"/>.
/// </summary>
public partial class PartialViewResultExecutor : ViewExecutor, IActionResultExecutor<PartialViewResult>
{
private const string ActionNameKey = "action";
/// <summary>
/// Creates a new <see cref="PartialViewResultExecutor"/>.
/// </summary>
/// <param name="viewOptions">The <see cref="IOptions{MvcViewOptions}"/>.</param>
/// <param name="writerFactory">The <see cref="IHttpResponseStreamWriterFactory"/>.</param>
/// <param name="viewEngine">The <see cref="ICompositeViewEngine"/>.</param>
/// <param name="tempDataFactory">The <see cref="ITempDataDictionaryFactory"/>.</param>
/// <param name="diagnosticListener">The <see cref="DiagnosticListener"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
/// <param name="modelMetadataProvider">The <see cref="IModelMetadataProvider"/>.</param>
public PartialViewResultExecutor(
IOptions<MvcViewOptions> viewOptions,
IHttpResponseStreamWriterFactory writerFactory,
ICompositeViewEngine viewEngine,
ITempDataDictionaryFactory tempDataFactory,
DiagnosticListener diagnosticListener,
ILoggerFactory loggerFactory,
IModelMetadataProvider modelMetadataProvider)
: base(viewOptions, writerFactory, viewEngine, tempDataFactory, diagnosticListener, modelMetadataProvider)
{
ArgumentNullException.ThrowIfNull(loggerFactory);
Logger = loggerFactory.CreateLogger<PartialViewResultExecutor>();
}
/// <summary>
/// Gets the <see cref="ILogger"/>.
/// </summary>
protected ILogger Logger { get; }
/// <summary>
/// Attempts to find the <see cref="IView"/> associated with <paramref name="viewResult"/>.
/// </summary>
/// <param name="actionContext">The <see cref="ActionContext"/> associated with the current request.</param>
/// <param name="viewResult">The <see cref="PartialViewResult"/>.</param>
/// <returns>A <see cref="ViewEngineResult"/>.</returns>
public virtual ViewEngineResult FindView(ActionContext actionContext, PartialViewResult viewResult)
{
ArgumentNullException.ThrowIfNull(actionContext);
ArgumentNullException.ThrowIfNull(viewResult);
var viewEngine = viewResult.ViewEngine ?? ViewEngine;
var viewName = viewResult.ViewName ?? GetActionName(actionContext) ?? string.Empty;
var stopwatch = ValueStopwatch.StartNew();
var result = viewEngine.GetView(executingFilePath: null, viewPath: viewName, isMainPage: false);
var originalResult = result;
if (!result.Success)
{
result = viewEngine.FindView(actionContext, viewName, isMainPage: false);
}
Log.PartialViewResultExecuting(Logger, result.ViewName);
if (!result.Success)
{
if (originalResult.SearchedLocations.Any())
{
if (result.SearchedLocations.Any())
{
// Return a new ViewEngineResult listing all searched locations.
var locations = new List<string>(originalResult.SearchedLocations);
locations.AddRange(result.SearchedLocations);
result = ViewEngineResult.NotFound(viewName, locations);
}
else
{
// GetView() searched locations but FindView() did not. Use first ViewEngineResult.
result = originalResult;
}
}
}
if (result.Success)
{
DiagnosticListener.ViewFound(
actionContext,
isMainPage: false,
viewResult: viewResult,
viewName: viewName,
view: result.View);
Log.PartialViewFound(Logger, result.View, stopwatch.GetElapsedTime());
}
else
{
DiagnosticListener.ViewNotFound(
actionContext,
isMainPage: false,
viewResult: viewResult,
viewName: viewName,
searchedLocations: result.SearchedLocations);
Log.PartialViewNotFound(Logger, viewName, result.SearchedLocations);
}
return result;
}
/// <summary>
/// Executes the <see cref="IView"/> asynchronously.
/// </summary>
/// <param name="actionContext">The <see cref="ActionContext"/> associated with the current request.</param>
/// <param name="view">The <see cref="IView"/>.</param>
/// <param name="viewResult">The <see cref="PartialViewResult"/>.</param>
/// <returns>A <see cref="Task"/> which will complete when view execution is completed.</returns>
public virtual Task ExecuteAsync(ActionContext actionContext, IView view, PartialViewResult viewResult)
{
ArgumentNullException.ThrowIfNull(actionContext);
ArgumentNullException.ThrowIfNull(view);
ArgumentNullException.ThrowIfNull(viewResult);
return ExecuteAsync(
actionContext,
view,
viewResult.ViewData,
viewResult.TempData,
viewResult.ContentType,
viewResult.StatusCode);
}
/// <inheritdoc />
public virtual async Task ExecuteAsync(ActionContext context, PartialViewResult result)
{
ArgumentNullException.ThrowIfNull(context);
ArgumentNullException.ThrowIfNull(result);
var stopwatch = ValueStopwatch.StartNew();
var viewEngineResult = FindView(context, result);
viewEngineResult.EnsureSuccessful(originalLocations: null);
var view = viewEngineResult.View;
using (view as IDisposable)
{
await ExecuteAsync(context, view, result);
}
Log.PartialViewResultExecuted(Logger, result.ViewName, stopwatch.GetElapsedTime());
}
private static string? GetActionName(ActionContext context)
{
ArgumentNullException.ThrowIfNull(context);
if (!context.RouteData.Values.TryGetValue(ActionNameKey, out var routeValue))
{
return null;
}
var actionDescriptor = context.ActionDescriptor;
string? normalizedValue = null;
if (actionDescriptor.RouteValues.TryGetValue(ActionNameKey, out var value) &&
!string.IsNullOrEmpty(value))
{
normalizedValue = value;
}
var stringRouteValue = Convert.ToString(routeValue, CultureInfo.InvariantCulture);
if (string.Equals(normalizedValue, stringRouteValue, StringComparison.OrdinalIgnoreCase))
{
return normalizedValue;
}
return stringRouteValue;
}
private static partial class Log
{
[LoggerMessage(1, LogLevel.Information, "Executing PartialViewResult, running view {PartialViewName}.", EventName = "PartialViewResultExecuting")]
public static partial void PartialViewResultExecuting(ILogger logger, string partialViewName);
[LoggerMessage(2, LogLevel.Debug, "The partial view path '{PartialViewFilePath}' was found in {ElapsedMilliseconds}ms.", EventName = "PartialViewFound")]
private static partial void PartialViewFound(ILogger logger, string partialViewFilePath, double elapsedMilliseconds);
public static void PartialViewFound(ILogger logger, IView view, TimeSpan timespan)
{
PartialViewFound(logger, view.Path, timespan.TotalMilliseconds);
}
[LoggerMessage(3, LogLevel.Error, "The partial view '{PartialViewName}' was not found. Searched locations: {SearchedViewLocations}", EventName = "PartialViewNotFound")]
public static partial void PartialViewNotFound(ILogger logger, string partialViewName, IEnumerable<string> searchedViewLocations);
[LoggerMessage(4, LogLevel.Information, "Executed PartialViewResult - view {PartialViewName} executed in {ElapsedMilliseconds}ms.", EventName = "PartialViewResultExecuted")]
private static partial void PartialViewResultExecuted(ILogger logger, string? partialViewName, double elapsedMilliseconds);
public static void PartialViewResultExecuted(ILogger logger, string? partialViewName, TimeSpan timespan)
{
PartialViewResultExecuted(logger, partialViewName, timespan.TotalMilliseconds);
}
}
}
|