From 306776ff635d9cb0c0337650d30031403f35c63c Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Wed, 30 Sep 2015 13:48:27 -0700 Subject: [PATCH] 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 --- .../Controllers/FilterActionInvoker.cs | 6 +- ...MvcViewFeaturesMvcCoreBuilderExtensions.cs | 2 + .../PartialViewResult.cs | 43 +-- .../ViewFeatures/PartialViewResultExecutor.cs | 143 ++++++++ .../ViewFeatures/ViewExecutor.cs | 127 +++++-- .../ViewFeatures/ViewResultExecutor.cs | 143 ++++++++ .../ViewResult.cs | 58 +--- .../IProxyViewContext.cs | 9 + .../TestTelemetryListener.cs | 138 +++++++- .../PartialViewResultTest.cs | 219 ++---------- .../PartialViewResultExecutorTest.cs | 227 +++++++++++++ .../ViewFeatures/ViewExecutorTest.cs | 198 ++++++++--- .../ViewFeatures/ViewResultExecutorTest.cs | 226 +++++++++++++ .../ViewResultTest.cs | 320 +++--------------- 14 files changed, 1221 insertions(+), 638 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs create mode 100644 src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs create mode 100644 test/Microsoft.AspNet.Mvc.TestTelemetryListener.Sources/IProxyViewContext.cs create mode 100644 test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs create mode 100644 test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs diff --git a/src/Microsoft.AspNet.Mvc.Core/Controllers/FilterActionInvoker.cs b/src/Microsoft.AspNet.Mvc.Core/Controllers/FilterActionInvoker.cs index afd9b8637d..8c062f7489 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Controllers/FilterActionInvoker.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Controllers/FilterActionInvoker.cs @@ -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 }); } } } diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs index 1b1a63ee12..44978a6898 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/DependencyInjection/MvcViewFeaturesMvcCoreBuilderExtensions.cs @@ -88,6 +88,8 @@ namespace Microsoft.Framework.DependencyInjection // View Engine and related infrastructure // services.TryAddSingleton(); + services.TryAddSingleton(); + services.TryAddSingleton(); // Support for activating ViewDataDictionary services.TryAddEnumerable( diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/PartialViewResult.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/PartialViewResult.cs index d5ba5890a0..2f7bf94a22 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/PartialViewResult.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/PartialViewResult.cs @@ -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(); + var services = context.HttpContext.RequestServices; + var executor = services.GetRequiredService(); - var logger = context.HttpContext.RequestServices.GetRequiredService>(); - - var options = context.HttpContext.RequestServices.GetRequiredService>(); - - 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); } } } diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs new file mode 100644 index 0000000000..a8c8a9014c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/PartialViewResultExecutor.cs @@ -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 +{ + /// + /// Finds and executes an for a . + /// + public class PartialViewResultExecutor : ViewExecutor + { + /// + /// Creates a new . + /// + /// The . + /// The . + /// The . + /// The . + public PartialViewResultExecutor( + IOptions viewOptions, + ICompositeViewEngine viewEngine, + TelemetrySource telemetry, + ILoggerFactory loggerFactory) + : base(viewOptions, viewEngine, telemetry) + { + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + Logger = loggerFactory.CreateLogger(); + } + + /// + /// Gets the . + /// + protected ILogger Logger { get; } + + /// + /// Attempts to find the associated with . + /// + /// The associated with the current request. + /// The . + /// A . + 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; + } + + /// + /// Executes the asynchronously. + /// + /// The associated with the current request. + /// The . + /// The . + /// A which will complete when view execution is completed. + 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); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs index a44603608f..5487e254d6 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewExecutor.cs @@ -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 { /// - /// Utility type for rendering a to the response. + /// Executes an . /// - public static class ViewExecutor + public class ViewExecutor { + /// + /// The default content-type header value for views, text/html; charset=utf8. + /// public static readonly MediaTypeHeaderValue DefaultContentType = new MediaTypeHeaderValue("text/html") { Encoding = Encoding.UTF8 }.CopyAsReadOnly(); /// - /// Asynchronously renders the specified to the response body. + /// Creates a new . /// - /// The to render. - /// The for the current executing action. - /// The for the view being rendered. - /// The for the view being rendered. - /// A that represents the asynchronous rendering. - public static async Task ExecuteAsync( - IView view, - ActionContext actionContext, - ViewDataDictionary viewData, - ITempDataDictionary tempData, - HtmlHelperOptions htmlHelperOptions, - MediaTypeHeaderValue contentType) + /// The . + /// The . + /// The . + public ViewExecutor( + IOptions 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; + } + + /// + /// Gets the . + /// + protected TelemetrySource Telemetry { get; } + + /// + /// Gets the default . + /// + protected IViewEngine ViewEngine { get; } + + /// + /// Gets the . + /// + protected MvcViewOptions ViewOptions { get; } + + /// + /// Executes a view asynchronously. + /// + /// The associated with the current request. + /// The . + /// The . + /// The . + /// + /// The content-type header value to set in the response. If null, will be used. + /// + /// + /// The HTTP status code to set in the response. May be null. + /// + /// A which will complete when view execution is completed. + 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(); } } diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs new file mode 100644 index 0000000000..9058add058 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewFeatures/ViewResultExecutor.cs @@ -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 +{ + /// + /// Finds and executes an for a . + /// + public class ViewResultExecutor : ViewExecutor + { + /// + /// Creates a new . + /// + /// The . + /// The . + /// The . + /// The . + public ViewResultExecutor( + IOptions viewOptions, + ICompositeViewEngine viewEngine, + TelemetrySource telemetry, + ILoggerFactory loggerFactory) + : base(viewOptions, viewEngine, telemetry) + { + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + Logger = loggerFactory.CreateLogger(); + } + + /// + /// Gets the . + /// + protected ILogger Logger { get; } + + /// + /// Attempts to find the associated with . + /// + /// The associated with the current request. + /// The . + /// A . + 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; + } + + /// + /// Executes the asynchronously. + /// + /// The associated with the current request. + /// The . + /// The . + /// A which will complete when view execution is completed. + 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); + } + } +} diff --git a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewResult.cs b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewResult.cs index 5671286ca7..6ecaf772f1 100644 --- a/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewResult.cs +++ b/src/Microsoft.AspNet.Mvc.ViewFeatures/ViewResult.cs @@ -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(); + var executor = services.GetRequiredService(); - var logger = services.GetRequiredService>(); - var telemetry = services.GetRequiredService(); - - var options = services.GetRequiredService>(); - - 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); } } } diff --git a/test/Microsoft.AspNet.Mvc.TestTelemetryListener.Sources/IProxyViewContext.cs b/test/Microsoft.AspNet.Mvc.TestTelemetryListener.Sources/IProxyViewContext.cs new file mode 100644 index 0000000000..dd5a4881c5 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.TestTelemetryListener.Sources/IProxyViewContext.cs @@ -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 + { + } +} diff --git a/test/Microsoft.AspNet.Mvc.TestTelemetryListener.Sources/TestTelemetryListener.cs b/test/Microsoft.AspNet.Mvc.TestTelemetryListener.Sources/TestTelemetryListener.cs index 232278ccf5..cd7dcfc017 100644 --- a/test/Microsoft.AspNet.Mvc.TestTelemetryListener.Sources/TestTelemetryListener.cs +++ b/test/Microsoft.AspNet.Mvc.TestTelemetryListener.Sources/TestTelemetryListener.cs @@ -51,56 +51,174 @@ namespace Microsoft.AspNet.Mvc }; } - public class OnViewResultViewFoundEventData + public class OnBeforeActionMethodEventData { public IProxyActionContext ActionContext { get; set; } + public IReadOnlyDictionary Arguments { get; set; } + } + + public OnBeforeActionMethodEventData BeforeActionMethod { get; set; } + + [TelemetryName("Microsoft.AspNet.Mvc.BeforeActionMethod")] + public virtual void OnBeforeActionMethod( + IProxyActionContext actionContext, + IReadOnlyDictionary 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 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 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, + }; + } } } diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs index 249ca3049a..c0b2db148d 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/PartialViewResultTest.cs @@ -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(); viewEngine .Setup(v => v.FindPartialView(It.IsAny(), It.IsAny())) @@ -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(); - var view = Mock.Of(); + var context = GetActionContext(); + var view = new Mock(MockBehavior.Strict); + view + .Setup(v => v.RenderAsync(It.IsAny())) + .Returns(Task.FromResult(0)) + .Verifiable(); + + view + .As() + .Setup(v => v.Dispose()) + .Verifiable(); + + var viewEngine = new Mock(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 PartialViewResultContentTypeData + private ActionContext GetActionContext() { - get - { - return new TheoryData - { - { - 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(); - var view = Mock.Of(); - - 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(), - }; - - // 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(); - var view = Mock.Of(); - - 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(), - }; - - // 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(); - viewEngine - .Setup(e => e.FindPartialView(context, viewName)) - .Returns(ViewEngineResult.Found(viewName, Mock.Of())) - .Verifiable(); - - var viewResult = new PartialViewResult - { - ViewEngine = viewEngine.Object, - ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()), - TempData = Mock.Of(), - }; - - // 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(); - viewEngine - .Setup(e => e.FindPartialView(It.IsAny(), viewName)) - .Returns(ViewEngineResult.Found(viewName, Mock.Of())) - .Verifiable(); - - var serviceProvider = new Mock(); - serviceProvider - .Setup(p => p.GetService(typeof(ICompositeViewEngine))) - .Returns(viewEngine.Object); - serviceProvider - .Setup(p => p.GetService(typeof(ILogger))) - .Returns(new Mock>().Object); - serviceProvider.Setup(s => s.GetService(typeof(IOptions))) - .Returns(() => { - var optionsAccessor = new Mock>(); - 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(), - }; - - // Act - await viewResult.ExecuteResultAsync(context); - - // Assert - viewEngine.Verify(); + return new ActionContext(GetHttpContext(), new RouteData(), new ActionDescriptor()); } private HttpContext GetHttpContext() { - var serviceProvider = new Mock(); - serviceProvider.Setup(s => s.GetService(typeof(ILogger))) - .Returns(new Mock>().Object); + var options = new TestOptionsManager(); + var viewExecutor = new PartialViewResultExecutor( + options, + new CompositeViewEngine(options), + new TelemetryListener("Microsoft.AspNet"), + NullLoggerFactory.Instance); - serviceProvider.Setup(s => s.GetService(typeof(IOptions))) - .Returns(() => { - var optionsAccessor = new Mock>(); - 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; } } diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs new file mode 100644 index 0000000000..d2f1531832 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/PartialViewResultExecutorTest.cs @@ -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(); + viewEngine + .Setup(e => e.FindPartialView(context, viewName)) + .Returns(ViewEngineResult.Found(viewName, Mock.Of())) + .Verifiable(); + + var viewResult = new PartialViewResult + { + ViewEngine = viewEngine.Object, + ViewName = viewName, + ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()), + TempData = Mock.Of(), + }; + + // 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(), + }; + + // 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(), + }; + + // 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(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(), + }; + + // 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(), + }; + + // Act + await executor.ExecuteAsync(context, Mock.Of(), 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(), + }; + + // Act + await executor.ExecuteAsync(context, Mock.Of(), 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(MockBehavior.Strict); + viewEngine + .Setup(e => e.FindPartialView(It.IsAny(), It.IsAny())) + .Returns((_, name) => ViewEngineResult.Found(name, Mock.Of())); + + var options = new TestOptionsManager(); + options.Value.ViewEngines.Add(viewEngine.Object); + + var viewExecutor = new PartialViewResultExecutor( + options, + new CompositeViewEngine(options), + telemetry, + NullLoggerFactory.Instance); + + return viewExecutor; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs index 89a80361ec..b514bb5eec 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewExecutorTest.cs @@ -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 ViewExecutorSetsContentTypeAndEncodingData + public static TheoryData ViewExecutorSetsContentTypeAndEncodingData { get { - return new TheoryData + return new TheoryData { { 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(); - view.Setup(v => v.RenderAsync(It.IsAny())) - .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(), - 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(), + 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(), + 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(), + 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(); - view.Setup(v => v.RenderAsync(It.IsAny())) - .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(); @@ -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(), - new HtmlHelperOptions(), - ViewExecutor.DefaultContentType); + contentType: null, + statusCode: null); // Assert stream.Verify(s => s.FlushAsync(It.IsAny()), Times.Once()); stream.Verify(s => s.Write(It.IsAny(), It.IsAny(), It.IsAny()), Times.Never()); } + + private IView CreateView(Func action) + { + var view = new Mock(MockBehavior.Strict); + view + .Setup(v => v.RenderAsync(It.IsAny())) + .Returns(action); + + return view.Object; + } + + private ViewExecutor CreateViewExecutor(TelemetryListener listener = null) + { + if (listener == null) + { + listener = new TelemetryListener("Test"); + } + + return new ViewExecutor( + new TestOptionsManager(), + new Mock(MockBehavior.Strict).Object, + listener); + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs new file mode 100644 index 0000000000..6c0b569a34 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewFeatures/ViewResultExecutorTest.cs @@ -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(); + viewEngine + .Setup(e => e.FindView(context, viewName)) + .Returns(ViewEngineResult.Found(viewName, Mock.Of())) + .Verifiable(); + + var viewResult = new ViewResult + { + ViewEngine = viewEngine.Object, + ViewName = viewName, + ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()), + TempData = Mock.Of(), + }; + + // 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(), + }; + + // 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(), + }; + + // 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(); + 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(), + }; + + // 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(), + }; + + // Act + await executor.ExecuteAsync(context, Mock.Of(), 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(), + }; + + // Act + await executor.ExecuteAsync(context, Mock.Of(), 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(MockBehavior.Strict); + viewEngine + .Setup(e => e.FindView(It.IsAny(), It.IsAny())) + .Returns((_, name) => ViewEngineResult.Found(name, Mock.Of())); + + var options = new TestOptionsManager(); + options.Value.ViewEngines.Add(viewEngine.Object); + + var viewExecutor = new ViewResultExecutor( + options, + new CompositeViewEngine(options), + telemetry, + NullLoggerFactory.Instance); + + return viewExecutor; + } + } +} diff --git a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs index 25915ffa80..052e00b914 100644 --- a/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs +++ b/test/Microsoft.AspNet.Mvc.ViewFeatures.Test/ViewResultTest.cs @@ -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(); - viewEngine.Setup(v => v.FindView(It.IsAny(), It.IsAny())) - .Returns(ViewEngineResult.NotFound("MyView", new[] { "Location1", "Location2" })) - .Verifiable(); + viewEngine + .Setup(v => v.FindView(It.IsAny(), It.IsAny())) + .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(); - var view = Mock.Of(); + var context = GetActionContext(); + var view = new Mock(MockBehavior.Strict); + view + .Setup(v => v.RenderAsync(It.IsAny())) + .Returns(Task.FromResult(0)) + .Verifiable(); + + view + .As() + .Setup(v => v.Dispose()) + .Verifiable(); + + var viewEngine = new Mock(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 ViewResultContentTypeData + private ActionContext GetActionContext() { - get - { - return new TheoryData - { - { - 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(); - var view = Mock.Of(); - 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(), - }; - - // 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(); - var view = Mock.Of(); - - 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(), - }; - - // 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(); - viewEngine.Setup(e => e.FindView(context, viewName)) - .Returns(ViewEngineResult.Found(viewName, Mock.Of())) - .Verifiable(); - - var viewResult = new ViewResult - { - ViewEngine = viewEngine.Object, - ViewData = new ViewDataDictionary(new EmptyModelMetadataProvider()), - TempData = Mock.Of(), - }; - - // 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(); - viewEngine.Setup(e => e.FindView(context, viewName)) - .Returns(ViewEngineResult.Found(viewName, Mock.Of())) - .Verifiable(); - - var serviceProvider = new Mock(); - - 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))) - .Returns(new Mock>().Object); - serviceProvider.Setup(s => s.GetService(typeof(IOptions))) - .Returns(() => { - var optionsAccessor = new Mock>(); - 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(), - }; - - // 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().SubscribeWithAdapter(listener); - - var viewEngine = new Mock(); - var view = Mock.Of(); - - 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(), - }; - - // 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().SubscribeWithAdapter(listener); - - var viewEngine = new Mock(); - var view = Mock.Of(); - - 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(), - }; - - // Act - await Assert.ThrowsAsync( - 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(); - serviceProvider.Setup(s => s.GetService(typeof(ILogger))) - .Returns(new Mock>().Object); + var options = new TestOptionsManager(); + var viewExecutor = new ViewResultExecutor( + options, + new CompositeViewEngine(options), + new TelemetryListener("Microsoft.AspNet"), + NullLoggerFactory.Instance); - var optionsAccessor = new Mock>(); - optionsAccessor.SetupGet(o => o.Value) - .Returns(new MvcViewOptions()); + var services = new ServiceCollection(); + services.AddInstance(viewExecutor); - serviceProvider.Setup(s => s.GetService(typeof(IOptions))) - .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; } }