Minor cleanup/refactor of ViewResult

- Make ViewExecutor a service
- Add facades for ViewResult/PartialViewResult
- Add eventing for ViewFound/NotFound in PartialViewResult
- Add eventing around view execution
- Cleanup of some various eventing & our tests code
This commit is contained in:
Ryan Nowak 2015-09-30 13:48:27 -07:00
parent eef6c3883a
commit 306776ff63
14 changed files with 1221 additions and 638 deletions

View File

@ -636,7 +636,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
{
_telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.AfterActionMethod",
new { actionContext = ActionContext, result });
new { actionContext = ActionContext, result = result });
}
}
@ -789,7 +789,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
{
_telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.BeforeActionResult",
new { actionContext = ActionContext, result });
new { actionContext = ActionContext, result = result });
}
try
@ -802,7 +802,7 @@ namespace Microsoft.AspNet.Mvc.Controllers
{
_telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.AfterActionResult",
new { actionContext = ActionContext, result });
new { actionContext = ActionContext, result = result });
}
}
}

View File

@ -88,6 +88,8 @@ namespace Microsoft.Framework.DependencyInjection
// View Engine and related infrastructure
//
services.TryAddSingleton<ICompositeViewEngine, CompositeViewEngine>();
services.TryAddSingleton<ViewResultExecutor>();
services.TryAddSingleton<PartialViewResultExecutor>();
// Support for activating ViewDataDictionary
services.TryAddEnumerable(

View File

@ -3,12 +3,10 @@
using System;
using System.Threading.Tasks;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using Microsoft.Net.Http.Headers;
using Microsoft.Framework.OptionsModel;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc
{
@ -60,41 +58,16 @@ namespace Microsoft.AspNet.Mvc
throw new ArgumentNullException(nameof(context));
}
var viewEngine = ViewEngine ??
context.HttpContext.RequestServices.GetRequiredService<ICompositeViewEngine>();
var services = context.HttpContext.RequestServices;
var executor = services.GetRequiredService<PartialViewResultExecutor>();
var logger = context.HttpContext.RequestServices.GetRequiredService<ILogger<PartialViewResult>>();
var options = context.HttpContext.RequestServices.GetRequiredService<IOptions<MvcViewOptions>>();
var viewName = ViewName ?? context.ActionDescriptor.Name;
var viewEngineResult = viewEngine.FindPartialView(context, viewName);
if (!viewEngineResult.Success)
{
logger.LogError(
"The partial view '{PartialViewName}' was not found. Searched locations: {SearchedViewLocations}",
viewName,
viewEngineResult.SearchedLocations);
}
var view = viewEngineResult.EnsureSuccessful().View;
logger.LogVerbose("The partial view '{PartialViewName}' was found.", viewName);
if (StatusCode != null)
{
context.HttpContext.Response.StatusCode = StatusCode.Value;
}
var result = executor.FindView(context, this);
result.EnsureSuccessful();
var view = result.View;
using (view as IDisposable)
{
await ViewExecutor.ExecuteAsync(
view,
context,
ViewData,
TempData,
options.Value.HtmlHelperOptions,
ContentType);
await executor.ExecuteAsync(context, view, this);
}
}
}

View File

@ -0,0 +1,143 @@
// 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.Diagnostics.Tracing;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.ViewFeatures
{
/// <summary>
/// Finds and executes an <see cref="IView"/> for a <see cref="PartialViewResult"/>.
/// </summary>
public class PartialViewResultExecutor : ViewExecutor
{
/// <summary>
/// Creates a new <see cref="PartialViewResultExecutor"/>.
/// </summary>
/// <param name="viewOptions">The <see cref="IOptions{MvcViewOptions}"/>.</param>
/// <param name="viewEngine">The <see cref="ICompositeViewEngine"/>.</param>
/// <param name="telemetry">The <see cref="TelemetrySource"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public PartialViewResultExecutor(
IOptions<MvcViewOptions> viewOptions,
ICompositeViewEngine viewEngine,
TelemetrySource telemetry,
ILoggerFactory loggerFactory)
: base(viewOptions, viewEngine, telemetry)
{
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(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)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
if (viewResult == null)
{
throw new ArgumentNullException(nameof(viewResult));
}
var viewEngine = viewResult.ViewEngine ?? ViewEngine;
var viewName = viewResult.ViewName ?? actionContext.ActionDescriptor.Name;
var result = viewEngine.FindPartialView(actionContext, viewName);
if (result.Success)
{
if (Telemetry.IsEnabled("Microsoft.AspNet.Mvc.ViewFound"))
{
Telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.ViewFound",
new
{
actionContext = actionContext,
isPartial = true,
result = viewResult,
viewName = viewName,
view = result.View,
});
}
Logger.LogVerbose("The partial view '{PartialViewName}' was found.", viewName);
}
else
{
if (Telemetry.IsEnabled("Microsoft.AspNet.Mvc.ViewNotFound"))
{
Telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.ViewNotFound",
new
{
actionContext = actionContext,
isPartial = true,
result = viewResult,
viewName = viewName,
searchedLocations = result.SearchedLocations
});
}
Logger.LogError(
"The partial view '{PartialViewName}' was not found. Searched locations: {SearchedViewLocations}",
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)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
if (view == null)
{
throw new ArgumentNullException(nameof(view));
}
if (viewResult == null)
{
throw new ArgumentNullException(nameof(viewResult));
}
return ExecuteAsync(
actionContext,
view,
viewResult.ViewData,
viewResult.TempData,
viewResult.ContentType,
viewResult.StatusCode);
}
}
}

View File

@ -2,50 +2,107 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.Tracing;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.Framework.OptionsModel;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc.ViewFeatures
{
/// <summary>
/// Utility type for rendering a <see cref="IView"/> to the response.
/// Executes an <see cref="IView"/>.
/// </summary>
public static class ViewExecutor
public class ViewExecutor
{
/// <summary>
/// The default content-type header value for views, <c>text/html; charset=utf8</c>.
/// </summary>
public static readonly MediaTypeHeaderValue DefaultContentType = new MediaTypeHeaderValue("text/html")
{
Encoding = Encoding.UTF8
}.CopyAsReadOnly();
/// <summary>
/// Asynchronously renders the specified <paramref name="view"/> to the response body.
/// Creates a new <see cref="ViewExecutor"/>.
/// </summary>
/// <param name="view">The <see cref="IView"/> to render.</param>
/// <param name="actionContext">The <see cref="ActionContext"/> for the current executing action.</param>
/// <param name="viewData">The <see cref="ViewDataDictionary"/> for the view being rendered.</param>
/// <param name="tempData">The <see cref="ITempDataDictionary"/> for the view being rendered.</param>
/// <returns>A <see cref="Task"/> that represents the asynchronous rendering.</returns>
public static async Task ExecuteAsync(
IView view,
ActionContext actionContext,
ViewDataDictionary viewData,
ITempDataDictionary tempData,
HtmlHelperOptions htmlHelperOptions,
MediaTypeHeaderValue contentType)
/// <param name="viewOptions">The <see cref="IOptions{MvcViewOptions}"/>.</param>
/// <param name="viewEngine">The <see cref="ICompositeViewEngine"/>.</param>
/// <param name="telemetry">The <see cref="TelemetrySource"/>.</param>
public ViewExecutor(
IOptions<MvcViewOptions> viewOptions,
ICompositeViewEngine viewEngine,
TelemetrySource telemetry)
{
if (view == null)
if (viewOptions == null)
{
throw new ArgumentNullException(nameof(view));
throw new ArgumentNullException(nameof(viewOptions));
}
if (viewEngine == null)
{
throw new ArgumentNullException(nameof(viewEngine));
}
if (telemetry == null)
{
throw new ArgumentNullException(nameof(telemetry));
}
ViewOptions = viewOptions.Value;
ViewEngine = viewEngine;
Telemetry = telemetry;
}
/// <summary>
/// Gets the <see cref="TelemetrySource"/>.
/// </summary>
protected TelemetrySource Telemetry { get; }
/// <summary>
/// Gets the default <see cref="IViewEngine"/>.
/// </summary>
protected IViewEngine ViewEngine { get; }
/// <summary>
/// Gets the <see cref="MvcViewOptions"/>.
/// </summary>
protected MvcViewOptions ViewOptions { get; }
/// <summary>
/// Executes a view 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="viewData">The <see cref="ViewDataDictionary"/>.</param>
/// <param name="tempData">The <see cref="ITempDataDictionary"/>.</param>
/// <param name="contentType">
/// The content-type header value to set in the response. If <c>null</c>, <see cref="DefaultContentType"/> will be used.
/// </param>
/// <param name="statusCode">
/// The HTTP status code to set in the response. May be <c>null</c>.
/// </param>
/// <returns>A <see cref="Task"/> which will complete when view execution is completed.</returns>
public virtual async Task ExecuteAsync(
ActionContext actionContext,
IView view,
ViewDataDictionary viewData,
ITempDataDictionary tempData,
MediaTypeHeaderValue contentType,
int? statusCode)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
if (view == null)
{
throw new ArgumentNullException(nameof(view));
}
if (viewData == null)
{
throw new ArgumentNullException(nameof(viewData));
@ -56,11 +113,6 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
throw new ArgumentNullException(nameof(tempData));
}
if (htmlHelperOptions == null)
{
throw new ArgumentNullException(nameof(htmlHelperOptions));
}
var response = actionContext.HttpContext.Response;
if (contentType != null && contentType.Encoding == null)
@ -76,7 +128,13 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
// 3. ViewExecutor.DefaultContentType (sensible default)
response.ContentType = contentType?.ToString() ?? response.ContentType ?? DefaultContentType.ToString();
using (var writer = new HttpResponseStreamWriter(response.Body, contentType?.Encoding ?? DefaultContentType.Encoding))
if (statusCode != null)
{
response.StatusCode = statusCode.Value;
}
var encoding = contentType?.Encoding ?? DefaultContentType.Encoding;
using (var writer = new HttpResponseStreamWriter(response.Body, encoding))
{
var viewContext = new ViewContext(
actionContext,
@ -84,12 +142,27 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
viewData,
tempData,
writer,
htmlHelperOptions);
ViewOptions.HtmlHelperOptions);
if (Telemetry.IsEnabled("Microsoft.AspNet.Mvc.BeforeView"))
{
Telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.BeforeView",
new { view = view, viewContext = viewContext, });
}
await view.RenderAsync(viewContext);
// Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying
// response. In the absence of this line, the buffer gets synchronously written to the response
// as part of the Dispose which has a perf impact.
if (Telemetry.IsEnabled("Microsoft.AspNet.Mvc.AfterView"))
{
Telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.AfterView",
new { view = view, viewContext = viewContext, });
}
// Perf: Invoke FlushAsync to ensure any buffered content is asynchronously written to the underlying
// response asynchronously. In the absence of this line, the buffer gets synchronously written to the
// response as part of the Dispose which has a perf impact.
await writer.FlushAsync();
}
}

View File

@ -0,0 +1,143 @@
// 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.Diagnostics.Tracing;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Mvc.ViewFeatures
{
/// <summary>
/// Finds and executes an <see cref="IView"/> for a <see cref="ViewResult"/>.
/// </summary>
public class ViewResultExecutor : ViewExecutor
{
/// <summary>
/// Creates a new <see cref="ViewResultExecutor"/>.
/// </summary>
/// <param name="viewOptions">The <see cref="IOptions{MvcViewOptions}"/>.</param>
/// <param name="viewEngine">The <see cref="ICompositeViewEngine"/>.</param>
/// <param name="telemetry">The <see cref="TelemetrySource"/>.</param>
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/>.</param>
public ViewResultExecutor(
IOptions<MvcViewOptions> viewOptions,
ICompositeViewEngine viewEngine,
TelemetrySource telemetry,
ILoggerFactory loggerFactory)
: base(viewOptions, viewEngine, telemetry)
{
if (loggerFactory == null)
{
throw new ArgumentNullException(nameof(loggerFactory));
}
Logger = loggerFactory.CreateLogger<ViewResultExecutor>();
}
/// <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="ViewResult"/>.</param>
/// <returns>A <see cref="ViewEngineResult"/>.</returns>
public virtual ViewEngineResult FindView(ActionContext actionContext, ViewResult viewResult)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
if (viewResult == null)
{
throw new ArgumentNullException(nameof(viewResult));
}
var viewEngine = viewResult.ViewEngine ?? ViewEngine;
var viewName = viewResult.ViewName ?? actionContext.ActionDescriptor.Name;
var result = viewEngine.FindView(actionContext, viewName);
if (result.Success)
{
if (Telemetry.IsEnabled("Microsoft.AspNet.Mvc.ViewFound"))
{
Telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.ViewFound",
new
{
actionContext = actionContext,
isPartial = false,
result = viewResult,
viewName = viewName,
view = result.View,
});
}
Logger.LogVerbose("The view '{ViewName}' was found.", viewName);
}
else
{
if (Telemetry.IsEnabled("Microsoft.AspNet.Mvc.ViewNotFound"))
{
Telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.ViewNotFound",
new
{
actionContext = actionContext,
isPartial = false,
result = viewResult,
viewName = viewName,
searchedLocations = result.SearchedLocations
});
}
Logger.LogError(
"The view '{ViewName}' was not found. Searched locations: {SearchedViewLocations}",
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="ViewResult"/>.</param>
/// <returns>A <see cref="Task"/> which will complete when view execution is completed.</returns>
public virtual Task ExecuteAsync(ActionContext actionContext, IView view, ViewResult viewResult)
{
if (actionContext == null)
{
throw new ArgumentNullException(nameof(actionContext));
}
if (view == null)
{
throw new ArgumentNullException(nameof(view));
}
if (viewResult == null)
{
throw new ArgumentNullException(nameof(viewResult));
}
return ExecuteAsync(
actionContext,
view,
viewResult.ViewData,
viewResult.TempData,
viewResult.ContentType,
viewResult.StatusCode);
}
}
}

View File

@ -2,13 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics.Tracing;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNet.Mvc
@ -62,60 +59,15 @@ namespace Microsoft.AspNet.Mvc
}
var services = context.HttpContext.RequestServices;
var viewEngine = ViewEngine ?? services.GetRequiredService<ICompositeViewEngine>();
var executor = services.GetRequiredService<ViewResultExecutor>();
var logger = services.GetRequiredService<ILogger<ViewResult>>();
var telemetry = services.GetRequiredService<TelemetrySource>();
var options = services.GetRequiredService<IOptions<MvcViewOptions>>();
var viewName = ViewName ?? context.ActionDescriptor.Name;
var viewEngineResult = viewEngine.FindView(context, viewName);
if (!viewEngineResult.Success)
{
if (telemetry.IsEnabled("Microsoft.AspNet.Mvc.ViewResultViewNotFound"))
{
telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.ViewResultViewNotFound",
new
{
actionContext = context,
result = this,
viewName = viewName,
searchedLocations = viewEngineResult.SearchedLocations
});
}
logger.LogError(
"The view '{ViewName}' was not found. Searched locations: {SearchedViewLocations}",
viewName,
viewEngineResult.SearchedLocations);
}
var view = viewEngineResult.EnsureSuccessful().View;
if (telemetry.IsEnabled("Microsoft.AspNet.Mvc.ViewResultViewFound"))
{
telemetry.WriteTelemetry(
"Microsoft.AspNet.Mvc.ViewResultViewFound",
new { actionContext = context, result = this, viewName, view = view });
}
logger.LogVerbose("The view '{ViewName}' was found.", viewName);
if (StatusCode != null)
{
context.HttpContext.Response.StatusCode = StatusCode.Value;
}
var result = executor.FindView(context, this);
result.EnsureSuccessful();
var view = result.View;
using (view as IDisposable)
{
await ViewExecutor.ExecuteAsync(
view,
context,
ViewData,
TempData,
options.Value.HtmlHelperOptions,
ContentType);
await executor.ExecuteAsync(context, view, this);
}
}
}

View File

@ -0,0 +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.
namespace Microsoft.AspNet.Mvc
{
public interface IProxyViewContext
{
}
}

View File

@ -51,56 +51,174 @@ namespace Microsoft.AspNet.Mvc
};
}
public class OnViewResultViewFoundEventData
public class OnBeforeActionMethodEventData
{
public IProxyActionContext ActionContext { get; set; }
public IReadOnlyDictionary<string, object> Arguments { get; set; }
}
public OnBeforeActionMethodEventData BeforeActionMethod { get; set; }
[TelemetryName("Microsoft.AspNet.Mvc.BeforeActionMethod")]
public virtual void OnBeforeActionMethod(
IProxyActionContext actionContext,
IReadOnlyDictionary<string, object> arguments)
{
BeforeActionMethod = new OnBeforeActionMethodEventData()
{
ActionContext = actionContext,
Arguments = arguments,
};
}
public class OnAfterActionMethodEventData
{
public IProxyActionContext ActionContext { get; set; }
public IProxyActionResult Result { get; set; }
}
public OnAfterActionMethodEventData AfterActionMethod { get; set; }
[TelemetryName("Microsoft.AspNet.Mvc.AfterActionMethod")]
public virtual void OnAfterActionMethod(
IProxyActionContext actionContext,
IProxyActionResult result)
{
AfterActionMethod = new OnAfterActionMethodEventData()
{
ActionContext = actionContext,
Result = result,
};
}
public class OnBeforeActionResultEventData
{
public IProxyActionContext ActionContext { get; set; }
public IProxyActionResult Result { get; set; }
}
public OnBeforeActionResultEventData BeforeActionResult { get; set; }
[TelemetryName("Microsoft.AspNet.Mvc.BeforeActionResult")]
public virtual void OnBeforeActionResult(IProxyActionContext actionContext, IProxyActionResult result)
{
BeforeActionResult = new OnBeforeActionResultEventData()
{
ActionContext = actionContext,
Result = result,
};
}
public class OnAfterActionResultEventData
{
public IProxyActionContext ActionContext { get; set; }
public IProxyActionResult Result { get; set; }
}
public OnAfterActionResultEventData AfterActionResult { get; set; }
[TelemetryName("Microsoft.AspNet.Mvc.AfterActionResult")]
public virtual void OnAfterActionResult(IProxyActionContext actionContext, IProxyActionResult result)
{
AfterActionResult = new OnAfterActionResultEventData()
{
ActionContext = actionContext,
Result = result,
};
}
public class OnViewFoundEventData
{
public IProxyActionContext ActionContext { get; set; }
public bool IsPartial { get; set; }
public IProxyActionResult Result { get; set; }
public string ViewName { get; set; }
public IProxyView View { get; set; }
}
public OnViewResultViewFoundEventData ViewResultViewFound { get; set; }
public OnViewFoundEventData ViewFound { get; set; }
[TelemetryName("Microsoft.AspNet.Mvc.ViewResultViewFound")]
public virtual void OnViewResultViewFound(
[TelemetryName("Microsoft.AspNet.Mvc.ViewFound")]
public virtual void OnViewFound(
IProxyActionContext actionContext,
bool isPartial,
IProxyActionResult result,
string viewName,
IProxyView view)
{
ViewResultViewFound = new OnViewResultViewFoundEventData()
ViewFound = new OnViewFoundEventData()
{
ActionContext = actionContext,
IsPartial = isPartial,
Result = result,
ViewName = viewName,
View = view,
};
}
public class OnViewResultViewNotFoundEventData
public class OnViewNotFoundEventData
{
public IProxyActionContext ActionContext { get; set; }
public bool IsPartial { get; set; }
public IProxyActionResult Result { get; set; }
public string ViewName { get; set; }
public IEnumerable<string> SearchedLocations { get; set; }
}
public OnViewResultViewNotFoundEventData ViewResultViewNotFound { get; set; }
public OnViewNotFoundEventData ViewNotFound { get; set; }
[TelemetryName("Microsoft.AspNet.Mvc.ViewResultViewNotFound")]
public virtual void OnViewResultViewNotFound(
[TelemetryName("Microsoft.AspNet.Mvc.ViewNotFound")]
public virtual void OnViewNotFound(
IProxyActionContext actionContext,
bool isPartial,
IProxyActionResult result,
string viewName,
IEnumerable<string> searchedLocations)
{
ViewResultViewNotFound = new OnViewResultViewNotFoundEventData()
ViewNotFound = new OnViewNotFoundEventData()
{
ActionContext = actionContext,
IsPartial = isPartial,
Result = result,
ViewName = viewName,
SearchedLocations = searchedLocations,
};
}
public class OnBeforeViewEventData
{
public IProxyView View { get; set; }
public IProxyViewContext ViewContext { get; set; }
}
public OnBeforeViewEventData BeforeView { get; set; }
[TelemetryName("Microsoft.AspNet.Mvc.BeforeView")]
public virtual void OnBeforeView(IProxyView view, IProxyViewContext viewContext)
{
BeforeView = new OnBeforeViewEventData()
{
View = view,
ViewContext = viewContext,
};
}
public class OnAfterViewEventData
{
public IProxyView View { get; set; }
public IProxyViewContext ViewContext { get; set; }
}
public OnAfterViewEventData AfterView { get; set; }
[TelemetryName("Microsoft.AspNet.Mvc.AfterView")]
public virtual void OnAfterView(IProxyView view, IProxyViewContext viewContext)
{
AfterView = new OnAfterViewEventData()
{
View = view,
ViewContext = viewContext,
};
}
}
}

View File

@ -2,27 +2,29 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Text;
using System.Diagnostics.Tracing;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Microsoft.Net.Http.Headers;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc
{
// These tests cover the logic included in PartialViewResult.ExecuteResultAsync - see PartialViewResultExecutorTest
// and ViewExecutorTest for more comprehensive tests.
public class PartialViewResultTest
{
[Fact]
public async Task ExecuteResultAsync_ReturnsError_IfViewCouldNotBeFound()
public async Task ExecuteResultAsync_Throws_IfViewCouldNotBeFound()
{
// Arrange
var expected = string.Join(
@ -30,10 +32,9 @@ namespace Microsoft.AspNet.Mvc
"The view 'MyView' was not found. The following locations were searched:",
"Location1",
"Location2.");
var actionContext = new ActionContext(
GetHttpContext(),
new RouteData(),
new ActionDescriptor());
var actionContext = GetActionContext();
var viewEngine = new Mock<IViewEngine>();
viewEngine
.Setup(v => v.FindPartialView(It.IsAny<ActionContext>(), It.IsAny<string>()))
@ -56,17 +57,27 @@ namespace Microsoft.AspNet.Mvc
}
[Fact]
public async Task PartialViewResult_UsesFindPartialViewOnSpecifiedViewEngineToLocateViews()
public async Task ExecuteResultAsync_FindsAndExecutesView()
{
// Arrange
var viewName = "myview";
var context = new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor());
var viewEngine = new Mock<IViewEngine>();
var view = Mock.Of<IView>();
var context = GetActionContext();
var view = new Mock<IView>(MockBehavior.Strict);
view
.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Returns(Task.FromResult(0))
.Verifiable();
view
.As<IDisposable>()
.Setup(v => v.Dispose())
.Verifiable();
var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
viewEngine
.Setup(e => e.FindPartialView(context, "myview"))
.Returns(ViewEngineResult.Found("myview", view))
.Returns(ViewEngineResult.Found("myview", view.Object))
.Verifiable();
var viewResult = new PartialViewResult
@ -81,185 +92,29 @@ namespace Microsoft.AspNet.Mvc
await viewResult.ExecuteResultAsync(context);
// Assert
view.Verify();
viewEngine.Verify();
}
public static TheoryData<MediaTypeHeaderValue, string> PartialViewResultContentTypeData
private ActionContext GetActionContext()
{
get
{
return new TheoryData<MediaTypeHeaderValue, string>
{
{
null,
"text/html; charset=utf-8"
},
{
new MediaTypeHeaderValue("text/foo"),
"text/foo; charset=utf-8"
},
{
new MediaTypeHeaderValue("text/foo") { Encoding = Encoding.ASCII },
"text/foo; charset=us-ascii"
}
};
}
}
[Theory]
[MemberData(nameof(PartialViewResultContentTypeData))]
public async Task PartialViewResult_SetsContentTypeHeader(
MediaTypeHeaderValue contentType,
string expectedContentTypeHeaderValue)
{
// Arrange
var viewName = "myview";
var httpContext = GetHttpContext();
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewEngine = new Mock<IViewEngine>();
var view = Mock.Of<IView>();
viewEngine
.Setup(e => e.FindPartialView(context, "myview"))
.Returns(ViewEngineResult.Found("myview", view));
var viewResult = new PartialViewResult
{
ViewName = viewName,
ViewEngine = viewEngine.Object,
ContentType = contentType,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
Assert.Equal(expectedContentTypeHeaderValue, httpContext.Response.ContentType);
}
[Fact]
public async Task PartialViewResult_SetsStatusCode()
{
// Arrange
var viewName = "myview";
var httpContext = GetHttpContext();
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewEngine = new Mock<IViewEngine>();
var view = Mock.Of<IView>();
viewEngine
.Setup(e => e.FindPartialView(context, "myview"))
.Returns(ViewEngineResult.Found("myview", view));
var viewResult = new PartialViewResult
{
ViewName = viewName,
ViewEngine = viewEngine.Object,
StatusCode = 404,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
Assert.Equal(404, httpContext.Response.StatusCode);
}
[Fact]
public async Task ExecuteResultAsync_UsesActionDescriptorName_IfViewNameIsNull()
{
// Arrange
var viewName = "some-view-name";
var context = new ActionContext(
GetHttpContext(),
new RouteData(),
new ActionDescriptor { Name = viewName });
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine
.Setup(e => e.FindPartialView(context, viewName))
.Returns(ViewEngineResult.Found(viewName, Mock.Of<IView>()))
.Verifiable();
var viewResult = new PartialViewResult
{
ViewEngine = viewEngine.Object,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
viewEngine.Verify();
}
[Fact]
public async Task ExecuteResultAsync_UsesCompositeViewEngineFromServices_IfViewEngineIsNotSpecified()
{
// Arrange
var viewName = "partial-view-name";
var context = new ActionContext(
new DefaultHttpContext(),
new RouteData(),
new ActionDescriptor { Name = viewName });
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine
.Setup(e => e.FindPartialView(It.IsAny<ActionContext>(), viewName))
.Returns(ViewEngineResult.Found(viewName, Mock.Of<IView>()))
.Verifiable();
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider
.Setup(p => p.GetService(typeof(ICompositeViewEngine)))
.Returns(viewEngine.Object);
serviceProvider
.Setup(p => p.GetService(typeof(ILogger<PartialViewResult>)))
.Returns(new Mock<ILogger<PartialViewResult>>().Object);
serviceProvider.Setup(s => s.GetService(typeof(IOptions<MvcViewOptions>)))
.Returns(() => {
var optionsAccessor = new Mock<IOptions<MvcViewOptions>>();
optionsAccessor.SetupGet(o => o.Value)
.Returns(new MvcViewOptions());
return optionsAccessor.Object;
});
context.HttpContext.RequestServices = serviceProvider.Object;
var viewResult = new PartialViewResult
{
ViewName = viewName,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
viewEngine.Verify();
return new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor());
}
private HttpContext GetHttpContext()
{
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider.Setup(s => s.GetService(typeof(ILogger<PartialViewResult>)))
.Returns(new Mock<ILogger<PartialViewResult>>().Object);
var options = new TestOptionsManager<MvcViewOptions>();
var viewExecutor = new PartialViewResultExecutor(
options,
new CompositeViewEngine(options),
new TelemetryListener("Microsoft.AspNet"),
NullLoggerFactory.Instance);
serviceProvider.Setup(s => s.GetService(typeof(IOptions<MvcViewOptions>)))
.Returns(() => {
var optionsAccessor = new Mock<IOptions<MvcViewOptions>>();
optionsAccessor.SetupGet(o => o.Value)
.Returns(new MvcViewOptions());
return optionsAccessor.Object;
});
var services = new ServiceCollection();
services.AddInstance(viewExecutor);
var httpContext = new DefaultHttpContext();
httpContext.RequestServices = serviceProvider.Object;
httpContext.RequestServices = services.BuildServiceProvider();
return httpContext;
}
}

View File

@ -0,0 +1,227 @@
// 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.Diagnostics.Tracing;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.Logging;
using Microsoft.Framework.Logging.Testing;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ViewFeatures
{
public class PartialViewResultExecutorTest
{
[Fact]
public void FindView_UsesViewEngine_FromPartialViewResult()
{
// Arrange
var context = GetActionContext();
var executor = GetViewExecutor();
var viewName = "my-view";
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine
.Setup(e => e.FindPartialView(context, viewName))
.Returns(ViewEngineResult.Found(viewName, Mock.Of<IView>()))
.Verifiable();
var viewResult = new PartialViewResult
{
ViewEngine = viewEngine.Object,
ViewName = viewName,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
var viewEngineResult = executor.FindView(context, viewResult);
// Assert
Assert.Equal(viewName, viewEngineResult.ViewName);
viewEngine.Verify();
}
[Fact]
public void FindView_UsesActionDescriptorName_IfViewNameIsNull()
{
// Arrange
var context = GetActionContext();
var executor = GetViewExecutor();
var viewName = "some-view-name";
context.ActionDescriptor.Name = viewName;
var viewResult = new PartialViewResult
{
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
var viewEngineResult = executor.FindView(context, viewResult);
// Assert
Assert.Equal(viewName, viewEngineResult.ViewName);
}
[Fact]
public void FindView_Notifies_ViewFound()
{
// Arrange
var telemetry = new TelemetryListener("Test");
var listener = new TestTelemetryListener();
telemetry.SubscribeWithAdapter(listener);
var context = GetActionContext();
var executor = GetViewExecutor(telemetry);
var viewName = "myview";
var viewResult = new PartialViewResult
{
ViewName = viewName,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
var viewEngineResult = executor.FindView(context, viewResult);
// Assert
Assert.Equal(viewName, viewEngineResult.ViewName);
Assert.NotNull(listener.ViewFound);
Assert.NotNull(listener.ViewFound.ActionContext);
Assert.NotNull(listener.ViewFound.Result);
Assert.NotNull(listener.ViewFound.View);
Assert.True(listener.ViewFound.IsPartial);
Assert.Equal("myview", listener.ViewFound.ViewName);
}
[Fact]
public void FindView_Notifies_ViewNotFound()
{
// Arrange
var telemetry = new TelemetryListener("Test");
var listener = new TestTelemetryListener();
telemetry.SubscribeWithAdapter(listener);
var context = GetActionContext();
var executor = GetViewExecutor(telemetry);
var viewName = "myview";
var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
viewEngine
.Setup(e => e.FindPartialView(context, "myview"))
.Returns(ViewEngineResult.NotFound("myview", new string[] { "location/myview" }));
var viewResult = new PartialViewResult
{
ViewName = viewName,
ViewEngine = viewEngine.Object,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
var viewEngineResult = executor.FindView(context, viewResult);
// Assert
Assert.False(viewEngineResult.Success);
Assert.NotNull(listener.ViewNotFound);
Assert.NotNull(listener.ViewNotFound.ActionContext);
Assert.NotNull(listener.ViewNotFound.Result);
Assert.Equal(new string[] { "location/myview" }, listener.ViewNotFound.SearchedLocations);
Assert.Equal("myview", listener.ViewNotFound.ViewName);
}
[Fact]
public async Task ExecuteAsync_UsesContentType_FromPartialViewResult()
{
// Arrange
var context = GetActionContext();
var executor = GetViewExecutor();
var contentType = MediaTypeHeaderValue.Parse("application/x-my-content-type");
var viewResult = new PartialViewResult
{
ViewName = "my-view",
ContentType = contentType,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await executor.ExecuteAsync(context, Mock.Of<IView>(), viewResult);
// Assert
Assert.Equal("application/x-my-content-type; charset=utf-8", context.HttpContext.Response.ContentType);
// Check if the original instance provided by the user has not changed.
// Since we do not have access to the new instance created within the view executor,
// check if at least the content is the same.
Assert.Null(contentType.Encoding);
}
[Fact]
public async Task ExecuteAsync_UsesStatusCode_FromPartialViewResult()
{
// Arrange
var context = GetActionContext();
var executor = GetViewExecutor();
var contentType = MediaTypeHeaderValue.Parse("application/x-my-content-type");
var viewResult = new PartialViewResult
{
ViewName = "my-view",
StatusCode = 404,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await executor.ExecuteAsync(context, Mock.Of<IView>(), viewResult);
// Assert
Assert.Equal(404, context.HttpContext.Response.StatusCode);
}
private ActionContext GetActionContext()
{
return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
}
private PartialViewResultExecutor GetViewExecutor(TelemetrySource telemetry = null)
{
if (telemetry == null)
{
telemetry = new TelemetryListener("Test");
}
var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
viewEngine
.Setup(e => e.FindPartialView(It.IsAny<ActionContext>(), It.IsAny<string>()))
.Returns<ActionContext, string>((_, name) => ViewEngineResult.Found(name, Mock.Of<IView>()));
var options = new TestOptionsManager<MvcViewOptions>();
options.Value.ViewEngines.Add(viewEngine.Object);
var viewExecutor = new PartialViewResultExecutor(
options,
new CompositeViewEngine(options),
telemetry,
NullLoggerFactory.Instance);
return viewExecutor;
}
}
}

View File

@ -2,7 +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.Tracing;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
@ -19,59 +21,51 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
{
public class ViewExecutorTest
{
public static TheoryData<MediaTypeHeaderValue, string, string, byte[]> ViewExecutorSetsContentTypeAndEncodingData
public static TheoryData<MediaTypeHeaderValue, string, string> ViewExecutorSetsContentTypeAndEncodingData
{
get
{
return new TheoryData<MediaTypeHeaderValue, string, string, byte[]>
return new TheoryData<MediaTypeHeaderValue, string, string>
{
{
null,
null,
"text/html; charset=utf-8",
new byte[] { 97, 98, 99, 100 }
"text/html; charset=utf-8"
},
{
new MediaTypeHeaderValue("text/foo"),
null,
"text/foo; charset=utf-8",
new byte[] { 97, 98, 99, 100 }
"text/foo; charset=utf-8"
},
{
MediaTypeHeaderValue.Parse("text/foo; p1=p1-value"),
null,
"text/foo; p1=p1-value; charset=utf-8",
new byte[] { 97, 98, 99, 100 }
"text/foo; p1=p1-value; charset=utf-8"
},
{
new MediaTypeHeaderValue("text/foo") { Charset = "us-ascii" },
null,
"text/foo; charset=us-ascii",
new byte[] { 97, 98, 99, 100 }
"text/foo; charset=us-ascii"
},
{
null,
"text/bar",
"text/bar",
new byte[] { 97, 98, 99, 100 }
"text/bar"
},
{
null,
"application/xml; charset=us-ascii",
"application/xml; charset=us-ascii",
new byte[] { 97, 98, 99, 100 }
"application/xml; charset=us-ascii"
},
{
null,
"Invalid content type",
"Invalid content type",
new byte[] { 97, 98, 99, 100 }
"Invalid content type"
},
{
new MediaTypeHeaderValue("text/foo") { Charset = "us-ascii" },
"text/bar",
"text/foo; charset=us-ascii",
new byte[] { 97, 98, 99, 100 }
"text/foo; charset=us-ascii"
},
};
}
@ -82,17 +76,13 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
public async Task ExecuteAsync_SetsContentTypeAndEncoding(
MediaTypeHeaderValue contentType,
string responseContentType,
string expectedContentType,
byte[] expectedContentData)
string expectedContentType)
{
// Arrange
var view = new Mock<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Callback((ViewContext v) =>
{
v.Writer.Write("abcd");
})
.Returns(Task.FromResult(0));
var view = CreateView(async (v) =>
{
await v.Writer.WriteAsync("abcd");
});
var context = new DefaultHttpContext();
var memoryStream = new MemoryStream();
@ -105,18 +95,99 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
new ActionDescriptor());
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
var viewExecutor = CreateViewExecutor();
// Act
await ViewExecutor.ExecuteAsync(
view.Object,
await viewExecutor.ExecuteAsync(
actionContext,
view,
viewData,
Mock.Of<ITempDataDictionary>(),
new HtmlHelperOptions(),
contentType);
contentType,
statusCode: null);
// Assert
Assert.Equal(expectedContentType, context.Response.ContentType);
Assert.Equal(expectedContentData, memoryStream.ToArray());
Assert.Equal("abcd", Encoding.UTF8.GetString(memoryStream.ToArray()));
}
[Fact]
public async Task ExecuteAsync_SetsStatusCode()
{
// Arrange
var view = CreateView(async (v) =>
{
await v.Writer.WriteAsync("abcd");
});
var context = new DefaultHttpContext();
var memoryStream = new MemoryStream();
context.Response.Body = memoryStream;
var actionContext = new ActionContext(
context,
new RouteData(),
new ActionDescriptor());
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
var viewExecutor = CreateViewExecutor();
// Act
await viewExecutor.ExecuteAsync(
actionContext,
view,
viewData,
Mock.Of<ITempDataDictionary>(),
contentType: null,
statusCode: 500);
// Assert
Assert.Equal(500, context.Response.StatusCode);
Assert.Equal("abcd", Encoding.UTF8.GetString(memoryStream.ToArray()));
}
[Fact]
public async Task ExecuteAsync_WritesTelemetry()
{
// Arrange
var view = CreateView(async (v) =>
{
await v.Writer.WriteAsync("abcd");
});
var context = new DefaultHttpContext();
var memoryStream = new MemoryStream();
context.Response.Body = memoryStream;
var actionContext = new ActionContext(
context,
new RouteData(),
new ActionDescriptor());
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
var adapter = new TestTelemetryListener();
var telemetryListener = new TelemetryListener("Test");
telemetryListener.SubscribeWithAdapter(adapter);
var viewExecutor = CreateViewExecutor(telemetryListener);
// Act
await viewExecutor.ExecuteAsync(
actionContext,
view,
viewData,
Mock.Of<ITempDataDictionary>(),
contentType: null,
statusCode: null);
// Assert
Assert.Equal("abcd", Encoding.UTF8.GetString(memoryStream.ToArray()));
Assert.NotNull(adapter.BeforeView?.View);
Assert.NotNull(adapter.BeforeView?.ViewContext);
Assert.NotNull(adapter.AfterView?.View);
Assert.NotNull(adapter.AfterView?.ViewContext);
}
[Fact]
@ -142,15 +213,16 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
new ActionDescriptor());
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
var viewExecutor = CreateViewExecutor();
// Act
await Record.ExceptionAsync(
() => ViewExecutor.ExecuteAsync(
view.Object,
actionContext,
viewData,
null,
new HtmlHelperOptions(),
contentType: null));
await Record.ExceptionAsync(() => viewExecutor.ExecuteAsync(
actionContext,
view.Object,
viewData,
Mock.Of<ITempDataDictionary>(),
contentType: null,
statusCode: null));
// Assert
Assert.Equal(expectedLength, memoryStream.Length);
@ -163,14 +235,11 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
public async Task ExecuteAsync_AsynchronouslyFlushesToTheResponseStream_PriorToDispose(int writeLength)
{
// Arrange
var view = new Mock<IView>();
view.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Returns((ViewContext v) =>
Task.Run(async () =>
{
var text = new string('a', writeLength);
await v.Writer.WriteAsync(text);
}));
var view = CreateView(async (v) =>
{
var text = new string('a', writeLength);
await v.Writer.WriteAsync(text);
});
var context = new DefaultHttpContext();
var stream = new Mock<Stream>();
@ -182,18 +251,43 @@ namespace Microsoft.AspNet.Mvc.ViewFeatures
new ActionDescriptor());
var viewData = new ViewDataDictionary(new EmptyModelMetadataProvider());
var viewExecutor = CreateViewExecutor();
// Act
await ViewExecutor.ExecuteAsync(
view.Object,
await viewExecutor.ExecuteAsync(
actionContext,
view,
viewData,
Mock.Of<ITempDataDictionary>(),
new HtmlHelperOptions(),
ViewExecutor.DefaultContentType);
contentType: null,
statusCode: null);
// Assert
stream.Verify(s => s.FlushAsync(It.IsAny<CancellationToken>()), Times.Once());
stream.Verify(s => s.Write(It.IsAny<byte[]>(), It.IsAny<int>(), It.IsAny<int>()), Times.Never());
}
private IView CreateView(Func<ViewContext, Task> action)
{
var view = new Mock<IView>(MockBehavior.Strict);
view
.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Returns(action);
return view.Object;
}
private ViewExecutor CreateViewExecutor(TelemetryListener listener = null)
{
if (listener == null)
{
listener = new TelemetryListener("Test");
}
return new ViewExecutor(
new TestOptionsManager<MvcViewOptions>(),
new Mock<ICompositeViewEngine>(MockBehavior.Strict).Object,
listener);
}
}
}

View File

@ -0,0 +1,226 @@
// 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.Diagnostics.Tracing;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.Logging.Testing;
using Microsoft.Net.Http.Headers;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.ViewFeatures
{
public class ViewResultExecutorTest
{
[Fact]
public void FindView_UsesViewEngine_FromViewResult()
{
// Arrange
var context = GetActionContext();
var executor = GetViewExecutor();
var viewName = "my-view";
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine
.Setup(e => e.FindView(context, viewName))
.Returns(ViewEngineResult.Found(viewName, Mock.Of<IView>()))
.Verifiable();
var viewResult = new ViewResult
{
ViewEngine = viewEngine.Object,
ViewName = viewName,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
var viewEngineResult = executor.FindView(context, viewResult);
// Assert
Assert.Equal(viewName, viewEngineResult.ViewName);
viewEngine.Verify();
}
[Fact]
public void FindView_UsesActionDescriptorName_IfViewNameIsNull()
{
// Arrange
var context = GetActionContext();
var executor = GetViewExecutor();
var viewName = "some-view-name";
context.ActionDescriptor.Name = viewName;
var viewResult = new ViewResult
{
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
var viewEngineResult = executor.FindView(context, viewResult);
// Assert
Assert.Equal(viewName, viewEngineResult.ViewName);
}
[Fact]
public void FindView_Notifies_ViewFound()
{
// Arrange
var telemetry = new TelemetryListener("Test");
var listener = new TestTelemetryListener();
telemetry.SubscribeWithAdapter(listener);
var context = GetActionContext();
var executor = GetViewExecutor(telemetry);
var viewName = "myview";
var viewResult = new ViewResult
{
ViewName = viewName,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
var viewEngineResult = executor.FindView(context, viewResult);
// Assert
Assert.Equal(viewName, viewEngineResult.ViewName);
Assert.NotNull(listener.ViewFound);
Assert.NotNull(listener.ViewFound.ActionContext);
Assert.NotNull(listener.ViewFound.Result);
Assert.NotNull(listener.ViewFound.View);
Assert.False(listener.ViewFound.IsPartial);
Assert.Equal("myview", listener.ViewFound.ViewName);
}
[Fact]
public void FindView_Notifies_ViewNotFound()
{
// Arrange
var telemetry = new TelemetryListener("Test");
var listener = new TestTelemetryListener();
telemetry.SubscribeWithAdapter(listener);
var context = GetActionContext();
var executor = GetViewExecutor(telemetry);
var viewName = "myview";
var viewEngine = new Mock<IViewEngine>();
viewEngine
.Setup(e => e.FindView(context, "myview"))
.Returns(ViewEngineResult.NotFound("myview", new string[] { "location/myview" }));
var viewResult = new ViewResult
{
ViewName = viewName,
ViewEngine = viewEngine.Object,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
var viewEngineResult = executor.FindView(context, viewResult);
// Assert
Assert.False(viewEngineResult.Success);
Assert.NotNull(listener.ViewNotFound);
Assert.NotNull(listener.ViewNotFound.ActionContext);
Assert.NotNull(listener.ViewNotFound.Result);
Assert.Equal(new string[] { "location/myview" }, listener.ViewNotFound.SearchedLocations);
Assert.Equal("myview", listener.ViewNotFound.ViewName);
}
[Fact]
public async Task ExecuteAsync_UsesContentType_FromViewResult()
{
// Arrange
var context = GetActionContext();
var executor = GetViewExecutor();
var contentType = MediaTypeHeaderValue.Parse("application/x-my-content-type");
var viewResult = new ViewResult
{
ViewName = "my-view",
ContentType = contentType,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await executor.ExecuteAsync(context, Mock.Of<IView>(), viewResult);
// Assert
Assert.Equal("application/x-my-content-type; charset=utf-8", context.HttpContext.Response.ContentType);
// Check if the original instance provided by the user has not changed.
// Since we do not have access to the new instance created within the view executor,
// check if at least the content is the same.
Assert.Null(contentType.Encoding);
}
[Fact]
public async Task ExecuteAsync_UsesStatusCode_FromViewResult()
{
// Arrange
var context = GetActionContext();
var executor = GetViewExecutor();
var contentType = MediaTypeHeaderValue.Parse("application/x-my-content-type");
var viewResult = new ViewResult
{
ViewName = "my-view",
StatusCode = 404,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await executor.ExecuteAsync(context, Mock.Of<IView>(), viewResult);
// Assert
Assert.Equal(404, context.HttpContext.Response.StatusCode);
}
private ActionContext GetActionContext()
{
return new ActionContext(new DefaultHttpContext(), new RouteData(), new ActionDescriptor());
}
private ViewResultExecutor GetViewExecutor(TelemetrySource telemetry = null)
{
if (telemetry == null)
{
telemetry = new TelemetryListener("Test");
}
var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
viewEngine
.Setup(e => e.FindView(It.IsAny<ActionContext>(), It.IsAny<string>()))
.Returns<ActionContext, string>((_, name) => ViewEngineResult.Found(name, Mock.Of<IView>()));
var options = new TestOptionsManager<MvcViewOptions>();
options.Value.ViewEngines.Add(viewEngine.Object);
var viewExecutor = new ViewResultExecutor(
options,
new CompositeViewEngine(options),
telemetry,
NullLoggerFactory.Instance);
return viewExecutor;
}
}
}

View File

@ -3,42 +3,44 @@
using System;
using System.Diagnostics.Tracing;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Mvc.Abstractions;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Mvc.ViewEngines;
using Microsoft.AspNet.Mvc.ViewFeatures;
using Microsoft.AspNet.Routing;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
using Microsoft.Net.Http.Headers;
using Microsoft.Framework.Logging.Testing;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc
{
// These tests cover the logic included in ViewResult.ExecuteResultAsync - see ViewResultExecutorTest
// and ViewExecutorTest for more comprehensive tests.
public class ViewResultTest
{
[Fact]
public async Task ExecuteResultAsync_ReturnsError_IfViewCouldNotBeFound()
public async Task ExecuteResultAsync_Throws_IfViewCouldNotBeFound()
{
// Arrange
var expected = string.Join(Environment.NewLine,
"The view 'MyView' was not found. The following locations were searched:",
"Location1",
"Location2.");
var actionContext = new ActionContext(GetHttpContext(),
new RouteData(),
new ActionDescriptor());
var expected = string.Join(
Environment.NewLine,
"The view 'MyView' was not found. The following locations were searched:",
"Location1",
"Location2.");
var actionContext = GetActionContext();
var viewEngine = new Mock<IViewEngine>();
viewEngine.Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>()))
.Returns(ViewEngineResult.NotFound("MyView", new[] { "Location1", "Location2" }))
.Verifiable();
viewEngine
.Setup(v => v.FindView(It.IsAny<ActionContext>(), It.IsAny<string>()))
.Returns(ViewEngineResult.NotFound("MyView", new[] { "Location1", "Location2" }))
.Verifiable();
var viewResult = new ViewResult
{
@ -56,17 +58,27 @@ namespace Microsoft.AspNet.Mvc
}
[Fact]
public async Task ViewResult_UsesFindViewOnSpecifiedViewEngineToLocateViews()
public async Task ExecuteResultAsync_FindsAndExecutesView()
{
// Arrange
var viewName = "myview";
var context = new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor());
var viewEngine = new Mock<IViewEngine>();
var view = Mock.Of<IView>();
var context = GetActionContext();
var view = new Mock<IView>(MockBehavior.Strict);
view
.Setup(v => v.RenderAsync(It.IsAny<ViewContext>()))
.Returns(Task.FromResult(0))
.Verifiable();
view
.As<IDisposable>()
.Setup(v => v.Dispose())
.Verifiable();
var viewEngine = new Mock<IViewEngine>(MockBehavior.Strict);
viewEngine
.Setup(e => e.FindView(context, "myview"))
.Returns(ViewEngineResult.Found("myview", view))
.Returns(ViewEngineResult.Found("myview", view.Object))
.Verifiable();
var viewResult = new ViewResult
@ -81,273 +93,29 @@ namespace Microsoft.AspNet.Mvc
await viewResult.ExecuteResultAsync(context);
// Assert
view.Verify();
viewEngine.Verify();
}
public static TheoryData<MediaTypeHeaderValue, string> ViewResultContentTypeData
private ActionContext GetActionContext()
{
get
{
return new TheoryData<MediaTypeHeaderValue, string>
{
{
null,
"text/html; charset=utf-8"
},
{
new MediaTypeHeaderValue("text/foo"),
"text/foo; charset=utf-8"
},
{
MediaTypeHeaderValue.Parse("text/foo;p1=p1-value"),
"text/foo; p1=p1-value; charset=utf-8"
},
{
new MediaTypeHeaderValue("text/foo") { Encoding = Encoding.ASCII },
"text/foo; charset=us-ascii"
}
};
}
}
[Theory]
[MemberData(nameof(ViewResultContentTypeData))]
public async Task ViewResult_SetsContentTypeHeader(
MediaTypeHeaderValue contentType,
string expectedContentTypeHeaderValue)
{
// Arrange
var viewName = "myview";
var httpContext = GetHttpContext();
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewEngine = new Mock<IViewEngine>();
var view = Mock.Of<IView>();
var contentTypeBeforeViewResultExecution = contentType?.ToString();
viewEngine.Setup(e => e.FindView(context, "myview"))
.Returns(ViewEngineResult.Found("myview", view));
var viewResult = new ViewResult
{
ViewName = viewName,
ViewEngine = viewEngine.Object,
ContentType = contentType,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
Assert.Equal(expectedContentTypeHeaderValue, httpContext.Response.ContentType);
// Check if the original instance provided by the user has not changed.
// Since we do not have access to the new instance created within the view executor,
// check if at least the content is the same.
var contentTypeAfterViewResultExecution = contentType?.ToString();
Assert.Equal(contentTypeBeforeViewResultExecution, contentTypeAfterViewResultExecution);
}
[Fact]
public async Task ViewResult_SetsStatusCode()
{
// Arrange
var viewName = "myview";
var httpContext = GetHttpContext();
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var viewEngine = new Mock<IViewEngine>();
var view = Mock.Of<IView>();
viewEngine.Setup(e => e.FindView(context, "myview"))
.Returns(ViewEngineResult.Found("myview", view));
var viewResult = new ViewResult
{
ViewName = viewName,
ViewEngine = viewEngine.Object,
StatusCode = 404,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
Assert.Equal(404, httpContext.Response.StatusCode);
}
[Fact]
public async Task ExecuteResultAsync_UsesActionDescriptorName_IfViewNameIsNull()
{
// Arrange
var viewName = "some-view-name";
var context = new ActionContext(GetHttpContext(),
new RouteData(),
new ActionDescriptor { Name = viewName });
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(e => e.FindView(context, viewName))
.Returns(ViewEngineResult.Found(viewName, Mock.Of<IView>()))
.Verifiable();
var viewResult = new ViewResult
{
ViewEngine = viewEngine.Object,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
viewEngine.Verify();
}
[Fact]
public async Task ExecuteResultAsync_UsesCompositeViewEngineFromServices_IfViewEngineIsNotSpecified()
{
// Arrange
var viewName = "some-view-name";
var context = new ActionContext(new DefaultHttpContext(),
new RouteData(),
new ActionDescriptor { Name = viewName });
var viewEngine = new Mock<ICompositeViewEngine>();
viewEngine.Setup(e => e.FindView(context, viewName))
.Returns(ViewEngineResult.Found(viewName, Mock.Of<IView>()))
.Verifiable();
var serviceProvider = new Mock<IServiceProvider>();
var telemetry = new TelemetryListener("Microsoft.AspNet");
serviceProvider
.Setup(s => s.GetService(typeof(TelemetrySource)))
.Returns(telemetry);
serviceProvider
.Setup(s => s.GetService(typeof(TelemetryListener)))
.Returns(telemetry);
serviceProvider.Setup(p => p.GetService(typeof(ICompositeViewEngine)))
.Returns(viewEngine.Object);
serviceProvider.Setup(p => p.GetService(typeof(ILogger<ViewResult>)))
.Returns(new Mock<ILogger<ViewResult>>().Object);
serviceProvider.Setup(s => s.GetService(typeof(IOptions<MvcViewOptions>)))
.Returns(() => {
var optionsAccessor = new Mock<IOptions<MvcViewOptions>>();
optionsAccessor.SetupGet(o => o.Value)
.Returns(new MvcViewOptions());
return optionsAccessor.Object;
});
context.HttpContext.RequestServices = serviceProvider.Object;
var viewResult = new ViewResult
{
ViewName = viewName,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
viewEngine.Verify();
}
[Fact]
public async Task ViewResult_NotifiesViewFound()
{
// Arrange
var viewName = "myview";
var httpContext = GetHttpContext();
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var listener = new TestTelemetryListener();
httpContext.RequestServices.GetRequiredService<TelemetryListener>().SubscribeWithAdapter(listener);
var viewEngine = new Mock<IViewEngine>();
var view = Mock.Of<IView>();
viewEngine.Setup(e => e.FindView(context, "myview"))
.Returns(ViewEngineResult.Found("myview", view));
var viewResult = new ViewResult
{
ViewName = viewName,
ViewEngine = viewEngine.Object,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await viewResult.ExecuteResultAsync(context);
// Assert
Assert.NotNull(listener.ViewResultViewFound);
Assert.NotNull(listener.ViewResultViewFound.ActionContext);
Assert.NotNull(listener.ViewResultViewFound.Result);
Assert.NotNull(listener.ViewResultViewFound.View);
Assert.Equal("myview", listener.ViewResultViewFound.ViewName);
}
[Fact]
public async Task ViewResult_NotifiesViewNotFound()
{
// Arrange
var viewName = "myview";
var httpContext = GetHttpContext();
var context = new ActionContext(httpContext, new RouteData(), new ActionDescriptor());
var listener = new TestTelemetryListener();
httpContext.RequestServices.GetRequiredService<TelemetryListener>().SubscribeWithAdapter(listener);
var viewEngine = new Mock<IViewEngine>();
var view = Mock.Of<IView>();
viewEngine.Setup(e => e.FindView(context, "myview"))
.Returns(ViewEngineResult.NotFound("myview", new string[] { "location/myview" }));
var viewResult = new ViewResult
{
ViewName = viewName,
ViewEngine = viewEngine.Object,
ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()),
TempData = Mock.Of<ITempDataDictionary>(),
};
// Act
await Assert.ThrowsAsync<InvalidOperationException>(
async () => await viewResult.ExecuteResultAsync(context));
// Assert
Assert.NotNull(listener.ViewResultViewNotFound);
Assert.NotNull(listener.ViewResultViewNotFound.ActionContext);
Assert.NotNull(listener.ViewResultViewNotFound.Result);
Assert.Equal(new string[] { "location/myview" }, listener.ViewResultViewNotFound.SearchedLocations);
Assert.Equal("myview", listener.ViewResultViewNotFound.ViewName);
return new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor());
}
private HttpContext GetHttpContext()
{
var serviceProvider = new Mock<IServiceProvider>();
serviceProvider.Setup(s => s.GetService(typeof(ILogger<ViewResult>)))
.Returns(new Mock<ILogger<ViewResult>>().Object);
var options = new TestOptionsManager<MvcViewOptions>();
var viewExecutor = new ViewResultExecutor(
options,
new CompositeViewEngine(options),
new TelemetryListener("Microsoft.AspNet"),
NullLoggerFactory.Instance);
var optionsAccessor = new Mock<IOptions<MvcViewOptions>>();
optionsAccessor.SetupGet(o => o.Value)
.Returns(new MvcViewOptions());
var services = new ServiceCollection();
services.AddInstance(viewExecutor);
serviceProvider.Setup(s => s.GetService(typeof(IOptions<MvcViewOptions>)))
.Returns(optionsAccessor.Object);
var telemetry = new TelemetryListener("Microsoft.AspNet");
serviceProvider.Setup(s => s.GetService(typeof(TelemetryListener)))
.Returns(telemetry);
serviceProvider.Setup(s => s.GetService(typeof(TelemetrySource)))
.Returns(telemetry);
var httpContext = new DefaultHttpContext();
httpContext.RequestServices = serviceProvider.Object;
httpContext.RequestServices = services.BuildServiceProvider();
return httpContext;
}
}