From 469414c4197eb0834316aed4fc35dcd006afb309 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Fri, 21 Mar 2014 14:40:35 -0700 Subject: [PATCH] Adding support for scoped services in WebFX We'll create a scoped service provider if the middleware that does it isn't there. We resolve all of our stuff from the scoped service provider, allowing users to plug in any scoped things they want. Ported UrlHelper to be a scoped service. --- samples/MvcSample.Web/Startup.cs | 2 + samples/MvcSample.Web/project.json | 1 + .../ActionResults/ViewResult.cs | 2 +- .../DefaultControllerFactory.cs | 5 +- .../MvcApplication.cs | 133 +++++++++++------- .../Properties/Resources.Designer.cs | 32 +++++ src/Microsoft.AspNet.Mvc.Core/Resources.resx | 8 +- src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs | 9 +- src/Microsoft.AspNet.Mvc/MvcServices.cs | 3 + 9 files changed, 133 insertions(+), 62 deletions(-) diff --git a/samples/MvcSample.Web/Startup.cs b/samples/MvcSample.Web/Startup.cs index ba9177b49b..04413a785b 100644 --- a/samples/MvcSample.Web/Startup.cs +++ b/samples/MvcSample.Web/Startup.cs @@ -2,6 +2,7 @@ using Microsoft.AspNet.DependencyInjection; using Microsoft.AspNet.DependencyInjection.Fallback; using Microsoft.AspNet.Mvc; +using Microsoft.AspNet.RequestContainer; using Microsoft.AspNet.Routing; namespace MvcSample.Web @@ -32,6 +33,7 @@ namespace MvcSample.Web "{controller}", new { controller = "Home" }); + builder.UseContainer(serviceProvider); builder.UseRouter(routes); } } diff --git a/samples/MvcSample.Web/project.json b/samples/MvcSample.Web/project.json index 039d7d7aec..86b61292d2 100644 --- a/samples/MvcSample.Web/project.json +++ b/samples/MvcSample.Web/project.json @@ -5,6 +5,7 @@ "Microsoft.AspNet.Abstractions": "0.1-alpha-*", "Microsoft.AspNet.ConfigurationModel": "0.1-alpha-*", "Microsoft.AspNet.DependencyInjection": "0.1-alpha-*", + "Microsoft.AspNet.RequestContainer": "0.1-alpha-*", "Microsoft.AspNet.Routing": "0.1-alpha-*", "Microsoft.AspNet.Server.WebListener": "0.1-alpha-*", "Microsoft.ComponentModel.DataAnnotations": "4.0.10.0", diff --git a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs index b2b16cfceb..95516bf0ae 100644 --- a/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs +++ b/src/Microsoft.AspNet.Mvc.Core/ActionResults/ViewResult.cs @@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Mvc private ViewContext CreateViewContext([NotNull] ActionContext actionContext, [NotNull] TextWriter writer) { - var urlHelper = new UrlHelper(actionContext.HttpContext, actionContext.Router, actionContext.RouteValues); + var urlHelper = _serviceProvider.GetService(); var viewContext = new ViewContext(_serviceProvider, actionContext.HttpContext, actionContext.RouteValues) { diff --git a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs index b6f0fd477a..80a503b95c 100644 --- a/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs +++ b/src/Microsoft.AspNet.Mvc.Core/DefaultControllerFactory.cs @@ -54,10 +54,7 @@ namespace Microsoft.AspNet.Mvc actionContext.ModelState); Injector.InjectProperty(controller, "ViewData", viewData); - var urlHelper = new UrlHelper( - actionContext.HttpContext, - actionContext.Router, - actionContext.RouteValues); + var urlHelper = _serviceProvider.GetService(); Injector.InjectProperty(controller, "Url", urlHelper); Injector.CallInitializer(controller, _serviceProvider); diff --git a/src/Microsoft.AspNet.Mvc.Core/MvcApplication.cs b/src/Microsoft.AspNet.Mvc.Core/MvcApplication.cs index 49bfde2cc3..36040fca96 100644 --- a/src/Microsoft.AspNet.Mvc.Core/MvcApplication.cs +++ b/src/Microsoft.AspNet.Mvc.Core/MvcApplication.cs @@ -1,82 +1,111 @@  using System; +using System.Diagnostics.Contracts; using System.Threading.Tasks; +using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.DependencyInjection; +using Microsoft.AspNet.Mvc.Core; using Microsoft.AspNet.Routing; namespace Microsoft.AspNet.Mvc { public class MvcApplication : IRouter { - private readonly IServiceProvider _services; - private IActionInvokerFactory _actionInvokerFactory; - private IActionSelector _actionSelector; + private readonly IServiceProvider _serviceProvider; - // Using service provider here to prevent ordering issues with configuration... - // IE: creating routes before configuring services, vice-versa. - public MvcApplication(IServiceProvider services) + public MvcApplication([NotNull] IServiceProvider serviceProvider) { - _services = services; + _serviceProvider = serviceProvider; } - private IActionInvokerFactory ActionInvokerFactory - { - get - { - if (_actionInvokerFactory == null) - { - _actionInvokerFactory = _services.GetService(); - } - - return _actionInvokerFactory; - } - } - - private IActionSelector ActionSelector - { - get - { - if (_actionSelector == null) - { - _actionSelector = _services.GetService(); - } - - return _actionSelector; - } - } - - public string GetVirtualPath(VirtualPathContext context) + public string GetVirtualPath([NotNull] VirtualPathContext context) { // For now just allow any values to target this application. context.IsBound = true; return null; } - public async Task RouteAsync(RouteContext context) + public async Task RouteAsync([NotNull] RouteContext context) { - var requestContext = new RequestContext(context.HttpContext, context.Values); - - var actionDescriptor = await ActionSelector.SelectAsync(requestContext); - if (actionDescriptor == null) + using (EnsureScopedServiceProvider(context.HttpContext)) { - return; + var services = context.HttpContext.RequestServices; + Contract.Assert(services != null); + + var requestContext = new RequestContext(context.HttpContext, context.Values); + + var actionSelector = services.GetService(); + var actionDescriptor = await actionSelector.SelectAsync(requestContext); + if (actionDescriptor == null) + { + return; + } + + var actionContext = new ActionContext(context.HttpContext, context.Router, context.Values, actionDescriptor); + + var contextAccessor = services.GetService>(); + using (contextAccessor.SetContextSource(() => actionContext, PreventExchange)) + { + var invokerFactory = services.GetService(); + var invoker = invokerFactory.CreateInvoker(actionContext); + if (invoker == null) + { + var ex = new InvalidOperationException( + Resources.FormatActionInvokerFactory_CouldNotCreateInvoker(actionDescriptor)); + + // Add tracing/logging (what do we think of this pattern of tacking on extra data on the exception?) + ex.Data.Add("AD", actionDescriptor); + + throw ex; + } + + await invoker.InvokeActionAsync(); + + context.IsHandled = true; + } + } + } + + private IDisposable EnsureScopedServiceProvider([NotNull] HttpContext httpContext) + { + if (httpContext.RequestServices != null) + { + // There's already a request-scope, we don't need to create one. It's safe to return null + // here, and that makes sure that we don't accidentally dispose the scope. + return null; } - var actionContext = new ActionContext(context.HttpContext, context.Router, context.Values, actionDescriptor); - var invoker = ActionInvokerFactory.CreateInvoker(actionContext); - if (invoker == null) + var applicationServices = httpContext.ApplicationServices ?? _serviceProvider; + + var scopeFactory = applicationServices.GetService(); + var scope = scopeFactory.CreateScope(); + + var scopeHolder = new ScopeHolder(httpContext, scope); + httpContext.RequestServices = scope.ServiceProvider; + return scopeHolder; + } + + private ActionContext PreventExchange(ActionContext contex) + { + throw new InvalidOperationException(Resources.ActionContextAccessor_SetValueNotSupported); + } + + private class ScopeHolder : IDisposable + { + private readonly HttpContext _httpContext; + private readonly IServiceScope _scope; + + public ScopeHolder([NotNull] HttpContext httpContext, [NotNull] IServiceScope scope) { - var ex = new InvalidOperationException("Could not instantiate invoker for the actionDescriptor"); - - // Add tracing/logging (what do we think of this pattern of tacking on extra data on the exception?) - ex.Data.Add("AD", actionDescriptor); - - throw ex; + _httpContext = httpContext; + _scope = scope; } - await invoker.InvokeActionAsync(); - - context.IsHandled = true; + public void Dispose() + { + _httpContext.RequestServices = null; + _scope.Dispose(); + } } } } diff --git a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs index d072c945e9..9e54509e08 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Core/Properties/Resources.Designer.cs @@ -201,6 +201,38 @@ namespace Microsoft.AspNet.Mvc.Core { return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_InvalidReturnValue"), p0, p1, p2); } + + /// + /// Replacing the action context is not supported. + /// + internal static string ActionContextAccessor_SetValueNotSupported + { + get { return GetString("ActionContextAccessor_SetValueNotSupported"); } + } + + /// + /// Replacing the action context is not supported. + /// + internal static string FormatActionContextAccessor_SetValueNotSupported() + { + return GetString("ActionContextAccessor_SetValueNotSupported"); + } + + /// + /// An action invoker could not be created for action '{0}'. + /// + internal static string ActionInvokerFactory_CouldNotCreateInvoker + { + get { return GetString("ActionInvokerFactory_CouldNotCreateInvoker"); } + } + + /// + /// An action invoker could not be created for action '{0}'. + /// + internal static string FormatActionInvokerFactory_CouldNotCreateInvoker(object p0) + { + return string.Format(CultureInfo.CurrentCulture, GetString("ActionInvokerFactory_CouldNotCreateInvoker"), p0); + } private static string GetString(string name, params string[] formatterNames) { diff --git a/src/Microsoft.AspNet.Mvc.Core/Resources.resx b/src/Microsoft.AspNet.Mvc.Core/Resources.resx index c3d3b06696..86c20a3c52 100644 --- a/src/Microsoft.AspNet.Mvc.Core/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Core/Resources.resx @@ -153,4 +153,10 @@ View components only support returning {0}, {1} or {2}. - \ No newline at end of file + + Replacing the action context is not supported. + + + An action invoker could not be created for action '{0}'. + + diff --git a/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs b/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs index c42fe2b070..a46a506776 100644 --- a/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs +++ b/src/Microsoft.AspNet.Mvc.Core/UrlHelper.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Microsoft.AspNet.Abstractions; +using Microsoft.AspNet.DependencyInjection; using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Routing; @@ -11,11 +12,11 @@ namespace Microsoft.AspNet.Mvc private readonly IRouter _router; private readonly IDictionary _ambientValues; - public UrlHelper([NotNull] HttpContext httpContext, [NotNull] IRouter router, [NotNull] IDictionary ambientValues) + public UrlHelper(IContextAccessor contextAccessor) { - _httpContext = httpContext; - _router = router; - _ambientValues = ambientValues; + _httpContext = contextAccessor.Value.HttpContext; + _router = contextAccessor.Value.Router; + _ambientValues = contextAccessor.Value.RouteValues; } public string Action(string action, string controller, object values) diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 3779e4f65c..c0fa7ae963 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -68,6 +68,8 @@ namespace Microsoft.AspNet.Mvc yield return describe.Transient(); yield return describe.Transient(); + yield return describe.Scoped(); + yield return describe.Transient(); yield return describe.Transient(); yield return describe.Transient, DefaultViewComponentInvokerProvider>(); @@ -86,6 +88,7 @@ namespace Microsoft.AspNet.Mvc typeof(NestedProviderManagerAsync<>), implementationInstance: null, lifecycle: LifecycleKind.Transient); + } } }