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.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);
}
}

View File

@ -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",

View File

@ -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<IUrlHelper>();
var viewContext = new ViewContext(_serviceProvider, actionContext.HttpContext, actionContext.RouteValues)
{

View File

@ -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<IUrlHelper>();
Injector.InjectProperty(controller, "Url", urlHelper);
Injector.CallInitializer(controller, _serviceProvider);

View File

@ -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<IActionInvokerFactory>();
}
return _actionInvokerFactory;
}
}
private IActionSelector ActionSelector
{
get
{
if (_actionSelector == null)
{
_actionSelector = _services.GetService<IActionSelector>();
}
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<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 invoker = ActionInvokerFactory.CreateInvoker(actionContext);
if (invoker == null)
var applicationServices = httpContext.ApplicationServices ?? _serviceProvider;
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");
// 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();
}
}
}
}

View File

@ -201,6 +201,38 @@ namespace Microsoft.AspNet.Mvc.Core
{
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)
{

View File

@ -153,4 +153,10 @@
<data name="ViewComponent_InvalidReturnValue" xml:space="preserve">
<value>View components only support returning {0}, {1} or {2}.</value>
</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 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<string, object> _ambientValues;
public UrlHelper([NotNull] HttpContext httpContext, [NotNull] IRouter router, [NotNull] IDictionary<string, object> ambientValues)
public UrlHelper(IContextAccessor<ActionContext> 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)

View File

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