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.
This commit is contained in:
Ryan Nowak 2014-03-21 14:40:35 -07:00
parent 21e48be06e
commit 469414c419
9 changed files with 133 additions and 62 deletions

View File

@ -2,6 +2,7 @@
using Microsoft.AspNet.DependencyInjection; using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.DependencyInjection.Fallback; using Microsoft.AspNet.DependencyInjection.Fallback;
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.RequestContainer;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
namespace MvcSample.Web namespace MvcSample.Web
@ -32,6 +33,7 @@ namespace MvcSample.Web
"{controller}", "{controller}",
new { controller = "Home" }); new { controller = "Home" });
builder.UseContainer(serviceProvider);
builder.UseRouter(routes); builder.UseRouter(routes);
} }
} }

View File

@ -5,6 +5,7 @@
"Microsoft.AspNet.Abstractions": "0.1-alpha-*", "Microsoft.AspNet.Abstractions": "0.1-alpha-*",
"Microsoft.AspNet.ConfigurationModel": "0.1-alpha-*", "Microsoft.AspNet.ConfigurationModel": "0.1-alpha-*",
"Microsoft.AspNet.DependencyInjection": "0.1-alpha-*", "Microsoft.AspNet.DependencyInjection": "0.1-alpha-*",
"Microsoft.AspNet.RequestContainer": "0.1-alpha-*",
"Microsoft.AspNet.Routing": "0.1-alpha-*", "Microsoft.AspNet.Routing": "0.1-alpha-*",
"Microsoft.AspNet.Server.WebListener": "0.1-alpha-*", "Microsoft.AspNet.Server.WebListener": "0.1-alpha-*",
"Microsoft.ComponentModel.DataAnnotations": "4.0.10.0", "Microsoft.ComponentModel.DataAnnotations": "4.0.10.0",

View File

@ -48,7 +48,7 @@ namespace Microsoft.AspNet.Mvc
private ViewContext CreateViewContext([NotNull] ActionContext actionContext, [NotNull] TextWriter writer) private ViewContext CreateViewContext([NotNull] ActionContext actionContext, [NotNull] TextWriter writer)
{ {
var urlHelper = new UrlHelper(actionContext.HttpContext, actionContext.Router, actionContext.RouteValues); var urlHelper = _serviceProvider.GetService<IUrlHelper>();
var viewContext = new ViewContext(_serviceProvider, actionContext.HttpContext, actionContext.RouteValues) var viewContext = new ViewContext(_serviceProvider, actionContext.HttpContext, actionContext.RouteValues)
{ {

View File

@ -54,10 +54,7 @@ namespace Microsoft.AspNet.Mvc
actionContext.ModelState); actionContext.ModelState);
Injector.InjectProperty(controller, "ViewData", viewData); Injector.InjectProperty(controller, "ViewData", viewData);
var urlHelper = new UrlHelper( var urlHelper = _serviceProvider.GetService<IUrlHelper>();
actionContext.HttpContext,
actionContext.Router,
actionContext.RouteValues);
Injector.InjectProperty(controller, "Url", urlHelper); Injector.InjectProperty(controller, "Url", urlHelper);
Injector.CallInitializer(controller, _serviceProvider); Injector.CallInitializer(controller, _serviceProvider);

View File

@ -1,82 +1,111 @@
 
using System; using System;
using System.Diagnostics.Contracts;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Abstractions;
using Microsoft.AspNet.DependencyInjection; using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
namespace Microsoft.AspNet.Mvc namespace Microsoft.AspNet.Mvc
{ {
public class MvcApplication : IRouter public class MvcApplication : IRouter
{ {
private readonly IServiceProvider _services; private readonly IServiceProvider _serviceProvider;
private IActionInvokerFactory _actionInvokerFactory;
private IActionSelector _actionSelector;
// Using service provider here to prevent ordering issues with configuration... public MvcApplication([NotNull] IServiceProvider serviceProvider)
// IE: creating routes before configuring services, vice-versa.
public MvcApplication(IServiceProvider services)
{ {
_services = services; _serviceProvider = serviceProvider;
} }
private IActionInvokerFactory ActionInvokerFactory public string GetVirtualPath([NotNull] VirtualPathContext context)
{
get
{
if (_actionInvokerFactory == null)
{
_actionInvokerFactory = _services.GetService<IActionInvokerFactory>();
}
return _actionInvokerFactory;
}
}
private IActionSelector ActionSelector
{
get
{
if (_actionSelector == null)
{
_actionSelector = _services.GetService<IActionSelector>();
}
return _actionSelector;
}
}
public string GetVirtualPath(VirtualPathContext context)
{ {
// For now just allow any values to target this application. // For now just allow any values to target this application.
context.IsBound = true; context.IsBound = true;
return null; return null;
} }
public async Task RouteAsync(RouteContext context) public async Task RouteAsync([NotNull] RouteContext context)
{ {
var requestContext = new RequestContext(context.HttpContext, context.Values); using (EnsureScopedServiceProvider(context.HttpContext))
var actionDescriptor = await ActionSelector.SelectAsync(requestContext);
if (actionDescriptor == null)
{ {
return; var services = context.HttpContext.RequestServices;
Contract.Assert(services != null);
var requestContext = new RequestContext(context.HttpContext, context.Values);
var actionSelector = services.GetService<IActionSelector>();
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<IContextAccessor<ActionContext>>();
using (contextAccessor.SetContextSource(() => actionContext, PreventExchange))
{
var invokerFactory = services.GetService<IActionInvokerFactory>();
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 applicationServices = httpContext.ApplicationServices ?? _serviceProvider;
var invoker = ActionInvokerFactory.CreateInvoker(actionContext);
if (invoker == null) var scopeFactory = applicationServices.GetService<IServiceScopeFactory>();
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"); _httpContext = httpContext;
_scope = scope;
// 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(); public void Dispose()
{
context.IsHandled = true; _httpContext.RequestServices = null;
_scope.Dispose();
}
} }
} }
} }

View File

@ -201,6 +201,38 @@ namespace Microsoft.AspNet.Mvc.Core
{ {
return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_InvalidReturnValue"), p0, p1, p2); return string.Format(CultureInfo.CurrentCulture, GetString("ViewComponent_InvalidReturnValue"), p0, p1, p2);
} }
/// <summary>
/// Replacing the action context is not supported.
/// </summary>
internal static string ActionContextAccessor_SetValueNotSupported
{
get { return GetString("ActionContextAccessor_SetValueNotSupported"); }
}
/// <summary>
/// Replacing the action context is not supported.
/// </summary>
internal static string FormatActionContextAccessor_SetValueNotSupported()
{
return GetString("ActionContextAccessor_SetValueNotSupported");
}
/// <summary>
/// An action invoker could not be created for action '{0}'.
/// </summary>
internal static string ActionInvokerFactory_CouldNotCreateInvoker
{
get { return GetString("ActionInvokerFactory_CouldNotCreateInvoker"); }
}
/// <summary>
/// An action invoker could not be created for action '{0}'.
/// </summary>
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) private static string GetString(string name, params string[] formatterNames)
{ {

View File

@ -153,4 +153,10 @@
<data name="ViewComponent_InvalidReturnValue" xml:space="preserve"> <data name="ViewComponent_InvalidReturnValue" xml:space="preserve">
<value>View components only support returning {0}, {1} or {2}.</value> <value>View components only support returning {0}, {1} or {2}.</value>
</data> </data>
</root> <data name="ActionContextAccessor_SetValueNotSupported" xml:space="preserve">
<value>Replacing the action context is not supported.</value>
</data>
<data name="ActionInvokerFactory_CouldNotCreateInvoker" xml:space="preserve">
<value>An action invoker could not be created for action '{0}'.</value>
</data>
</root>

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNet.Abstractions; using Microsoft.AspNet.Abstractions;
using Microsoft.AspNet.DependencyInjection;
using Microsoft.AspNet.Mvc.Rendering; using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.AspNet.Routing; using Microsoft.AspNet.Routing;
@ -11,11 +12,11 @@ namespace Microsoft.AspNet.Mvc
private readonly IRouter _router; private readonly IRouter _router;
private readonly IDictionary<string, object> _ambientValues; private readonly IDictionary<string, object> _ambientValues;
public UrlHelper([NotNull] HttpContext httpContext, [NotNull] IRouter router, [NotNull] IDictionary<string, object> ambientValues) public UrlHelper(IContextAccessor<ActionContext> contextAccessor)
{ {
_httpContext = httpContext; _httpContext = contextAccessor.Value.HttpContext;
_router = router; _router = contextAccessor.Value.Router;
_ambientValues = ambientValues; _ambientValues = contextAccessor.Value.RouteValues;
} }
public string Action(string action, string controller, object values) public string Action(string action, string controller, object values)

View File

@ -68,6 +68,8 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Transient<IModelValidatorProvider, DataAnnotationsModelValidatorProvider>(); yield return describe.Transient<IModelValidatorProvider, DataAnnotationsModelValidatorProvider>();
yield return describe.Transient<IModelValidatorProvider, DataMemberModelValidatorProvider>(); yield return describe.Transient<IModelValidatorProvider, DataMemberModelValidatorProvider>();
yield return describe.Scoped<IUrlHelper, UrlHelper>();
yield return describe.Transient<IViewComponentSelector, DefaultViewComponentSelector>(); yield return describe.Transient<IViewComponentSelector, DefaultViewComponentSelector>();
yield return describe.Transient<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>(); yield return describe.Transient<IViewComponentInvokerFactory, DefaultViewComponentInvokerFactory>();
yield return describe.Transient<INestedProvider<ViewComponentInvokerProviderContext>, DefaultViewComponentInvokerProvider>(); yield return describe.Transient<INestedProvider<ViewComponentInvokerProviderContext>, DefaultViewComponentInvokerProvider>();
@ -86,6 +88,7 @@ namespace Microsoft.AspNet.Mvc
typeof(NestedProviderManagerAsync<>), typeof(NestedProviderManagerAsync<>),
implementationInstance: null, implementationInstance: null,
lifecycle: LifecycleKind.Transient); lifecycle: LifecycleKind.Transient);
} }
} }
} }