Use/EnsureRequestServices changes

- Split UseServices overloads into UseRequestServices and UseServices
- Add RequestServicesContainer class which contains the old
ContainerMiddleware logic and exposes a new
EnsureRequestServices(HttpContext) method which can be called to
populate RequestServices
- ConfigureServices now scans for Configure{Env}Services instead of
ConfigureServices{Env}
- Add OptionsServices as part of default HostingServices
This commit is contained in:
Hao Kung 2014-10-14 19:01:01 -07:00
parent ebe4948a3e
commit 6466d1061e
16 changed files with 341 additions and 60 deletions

View File

@ -9,6 +9,7 @@ using Microsoft.AspNet.Security.DataProtection;
using Microsoft.Framework.ConfigurationModel;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Logging;
using Microsoft.Framework.OptionsModel;
namespace Microsoft.AspNet.Hosting
{
@ -41,6 +42,11 @@ namespace Microsoft.AspNet.Hosting
yield return describer.Scoped(typeof(IContextAccessor<>), typeof(ContextAccessor<>));
foreach (var service in OptionsServices.GetDefaultServices())
{
yield return service;
}
foreach (var service in DataProtectionServices.GetDefaultServices())
{
yield return service;

View File

@ -50,7 +50,7 @@ namespace Microsoft.AspNet.Hosting
// The application name is a "good enough" mechanism to identify this application
// on the machine and to prevent subkeys from being shared across multiple applications
// by default.
serviceCollection.ConfigureOptions<DataProtectionOptions>(options =>
serviceCollection.Configure<DataProtectionOptions>(options =>
{
options.ApplicationDiscriminator = appEnv.ApplicationName;
});

View File

@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using Microsoft.AspNet.Builder;
@ -27,16 +28,17 @@ namespace Microsoft.AspNet.Hosting.Startup
private MethodInfo FindMethod(Type startupType, string methodName, string environmentName, Type returnType = null, bool required = true)
{
var methodNameWithEnv = methodName + environmentName;
var methodNameWithEnv = string.Format(CultureInfo.InvariantCulture, methodName, environmentName);
var methodNameWithNoEnv = string.Format(CultureInfo.InvariantCulture, methodName, "");
var methodInfo = startupType.GetTypeInfo().GetDeclaredMethod(methodNameWithEnv)
?? startupType.GetTypeInfo().GetDeclaredMethod(methodName);
?? startupType.GetTypeInfo().GetDeclaredMethod(methodNameWithNoEnv);
if (methodInfo == null)
{
if (required)
{
throw new Exception(string.Format("TODO: {0} or {1} method not found",
methodNameWithEnv,
methodName));
methodNameWithNoEnv));
}
return null;
@ -134,9 +136,9 @@ namespace Microsoft.AspNet.Hosting.Startup
applicationName));
}
var configureMethod = FindMethod(type, "Configure", environmentName, typeof(void), required: true);
var configureMethod = FindMethod(type, "Configure{0}", environmentName, typeof(void), required: true);
// TODO: accept IServiceProvider method as well?
var servicesMethod = FindMethod(type, "ConfigureServices", environmentName, typeof(void), required: false);
var servicesMethod = FindMethod(type, "Configure{0}Services", environmentName, typeof(void), required: false);
object instance = null;
if (!configureMethod.IsStatic || (servicesMethod != null && !servicesMethod.IsStatic))

View File

@ -31,33 +31,33 @@ namespace Microsoft.AspNet.Builder
});
}
public static IApplicationBuilder UsePerRequestServices(this IApplicationBuilder builder)
public static IApplicationBuilder UseRequestServices(this IApplicationBuilder builder)
{
return builder.UseMiddleware(typeof(ContainerMiddleware));
}
public static IApplicationBuilder UsePerRequestServices(this IApplicationBuilder builder, IServiceProvider applicationServices)
public static IApplicationBuilder UseRequestServices(this IApplicationBuilder builder, IServiceProvider applicationServices)
{
builder.ApplicationServices = applicationServices;
return builder.UseMiddleware(typeof(ContainerMiddleware));
}
public static IApplicationBuilder UsePerRequestServices(this IApplicationBuilder builder, IEnumerable<IServiceDescriptor> applicationServices)
public static IApplicationBuilder UseServices(this IApplicationBuilder builder, IEnumerable<IServiceDescriptor> applicationServices)
{
return builder.UsePerRequestServices(services => services.Add(applicationServices));
return builder.UseServices(services => services.Add(applicationServices));
}
public static IApplicationBuilder UsePerRequestServices(this IApplicationBuilder builder, Action<ServiceCollection> configureServices)
public static IApplicationBuilder UseServices(this IApplicationBuilder builder, Action<ServiceCollection> configureServices)
{
return builder.UsePerRequestServices(serviceCollection =>
return builder.UseServices(serviceCollection =>
{
configureServices(serviceCollection);
return serviceCollection.BuildServiceProvider(builder.ApplicationServices);
});
}
public static IApplicationBuilder UsePerRequestServices(this IApplicationBuilder builder, Func<ServiceCollection, IServiceProvider> configureServices)
public static IApplicationBuilder UseServices(this IApplicationBuilder builder, Func<ServiceCollection, IServiceProvider> configureServices)
{
var serviceCollection = new ServiceCollection();

View File

@ -49,7 +49,7 @@ namespace Microsoft.AspNet.RequestContainer
_rootHttpContextAccessor.SetContextSource(AccessRootHttpContext, ExchangeRootHttpContext);
}
private HttpContext AccessRootHttpContext()
internal static HttpContext AccessRootHttpContext()
{
#if ASPNET50
var handle = CallContext.LogicalGetData(LogicalDataKey) as ObjectHandle;
@ -59,7 +59,7 @@ namespace Microsoft.AspNet.RequestContainer
#endif
}
private HttpContext ExchangeRootHttpContext(HttpContext httpContext)
internal static HttpContext ExchangeRootHttpContext(HttpContext httpContext)
{
#if ASPNET50
var prior = CallContext.LogicalGetData(LogicalDataKey) as ObjectHandle;
@ -92,29 +92,9 @@ namespace Microsoft.AspNet.RequestContainer
appHttpContextAccessor = priorApplicationServices.GetService<IContextAccessor<HttpContext>>();
}
using (var scope = appServiceScopeFactory.CreateScope())
using (var container = new RequestServicesContainer(httpContext, appServiceScopeFactory, appHttpContextAccessor, appServiceProvider))
{
var scopeServiceProvider = scope.ServiceProvider;
var scopeHttpContextAccessor = scopeServiceProvider.GetService<IContextAccessor<HttpContext>>();
httpContext.ApplicationServices = appServiceProvider;
httpContext.RequestServices = scopeServiceProvider;
var priorAppHttpContext = appHttpContextAccessor.SetValue(httpContext);
var priorScopeHttpContext = scopeHttpContextAccessor.SetValue(httpContext);
try
{
await _next.Invoke(httpContext);
}
finally
{
scopeHttpContextAccessor.SetValue(priorScopeHttpContext);
appHttpContextAccessor.SetValue(priorAppHttpContext);
httpContext.RequestServices = priorRequestServices;
httpContext.ApplicationServices = priorApplicationServices;
}
await _next.Invoke(httpContext);
}
}
}

View File

@ -0,0 +1,142 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.Http;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.RequestContainer
{
public class RequestServicesContainer : IDisposable
{
public RequestServicesContainer(
HttpContext context,
IServiceScopeFactory scopeFactory,
IContextAccessor<HttpContext> appContextAccessor,
IServiceProvider appServiceProvider)
{
if (scopeFactory == null)
{
throw new ArgumentNullException(nameof(scopeFactory));
}
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (appContextAccessor == null)
{
throw new ArgumentNullException(nameof(appContextAccessor));
}
AppContextAccessor = appContextAccessor;
Context = context;
PriorAppServices = context.ApplicationServices;
PriorRequestServices = context.RequestServices;
// Begin the scope
Scope = scopeFactory.CreateScope();
ScopeContextAccessor = Scope.ServiceProvider.GetService<IContextAccessor<HttpContext>>();
Context.ApplicationServices = appServiceProvider;
Context.RequestServices = Scope.ServiceProvider;
PriorAppHttpContext = AppContextAccessor.SetValue(context);
PriorScopeHttpContext = ScopeContextAccessor.SetValue(context);
}
private HttpContext Context { get; set; }
private IServiceProvider PriorAppServices { get; set; }
private IServiceProvider PriorRequestServices { get; set; }
private HttpContext PriorAppHttpContext { get; set; }
private HttpContext PriorScopeHttpContext { get; set; }
private IServiceScope Scope { get; set; }
private IContextAccessor<HttpContext> ScopeContextAccessor { get; set; }
private IContextAccessor<HttpContext> AppContextAccessor { get; set; }
// CONSIDER: this could be an extension method on HttpContext instead
public static RequestServicesContainer EnsureRequestServices(HttpContext httpContext)
{
// All done if we already have a request services
if (httpContext.RequestServices != null)
{
return null;
}
if (httpContext.ApplicationServices == null)
{
throw new InvalidOperationException("TODO: httpContext.ApplicationServices is null!");
}
// Matches constructor of RequestContainer
var rootServiceProvider = httpContext.ApplicationServices.GetService<IServiceProvider>();
var rootHttpContextAccessor = httpContext.ApplicationServices.GetService<IContextAccessor<HttpContext>>();
var rootServiceScopeFactory = httpContext.ApplicationServices.GetService<IServiceScopeFactory>();
rootHttpContextAccessor.SetContextSource(ContainerMiddleware.AccessRootHttpContext, ContainerMiddleware.ExchangeRootHttpContext);
// Pre Scope setup
var priorApplicationServices = httpContext.ApplicationServices;
var priorRequestServices = httpContext.RequestServices;
var appServiceProvider = rootServiceProvider;
var appServiceScopeFactory = rootServiceScopeFactory;
var appHttpContextAccessor = rootHttpContextAccessor;
if (priorApplicationServices != null &&
priorApplicationServices != appServiceProvider)
{
appServiceProvider = priorApplicationServices;
appServiceScopeFactory = priorApplicationServices.GetService<IServiceScopeFactory>();
appHttpContextAccessor = priorApplicationServices.GetService<IContextAccessor<HttpContext>>();
}
// Creates the scope and does the service swaps
return new RequestServicesContainer(httpContext, appServiceScopeFactory, appHttpContextAccessor, appServiceProvider);
}
#region IDisposable Support
private bool disposedValue = false; // To detect redundant calls
protected virtual void Dispose(bool disposing)
{
if (!disposedValue)
{
if (disposing)
{
ScopeContextAccessor.SetValue(PriorScopeHttpContext);
AppContextAccessor.SetValue(PriorAppHttpContext);
Context.RequestServices = PriorRequestServices;
Context.ApplicationServices = PriorAppServices;
}
if (Scope != null)
{
Scope.Dispose();
Scope = null;
}
Context = null;
PriorAppServices = null;
PriorRequestServices = null;
ScopeContextAccessor = null;
AppContextAccessor = null;
PriorAppHttpContext = null;
PriorScopeHttpContext = null;
disposedValue = true;
}
}
// This code added to correctly implement the disposable pattern.
public void Dispose()
{
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
Dispose(true);
}
#endregion
}
}

View File

@ -7,5 +7,6 @@ namespace Microsoft.AspNet.Hosting.Fakes
{
public bool Configured { get; set; }
public string Environment { get; set; }
public string Message { get; set; }
}
}

View File

@ -0,0 +1,8 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Hosting.Fakes
{
public interface IFakeService { }
public class FakeService : IFakeService { }
}

View File

@ -14,37 +14,37 @@ namespace Microsoft.AspNet.Hosting.Fakes
public void ConfigureServices(IServiceCollection services)
{
services.ConfigureOptions<FakeOptions>(o => o.Configured = true);
services.Configure<FakeOptions>(o => o.Configured = true);
}
public void ConfigureServicesDev(IServiceCollection services)
public void ConfigureDevServices(IServiceCollection services)
{
services.ConfigureOptions<FakeOptions>(o =>
services.Configure<FakeOptions>(o =>
{
o.Configured = true;
o.Environment = "Dev";
});
}
public void ConfigureServicesRetail(IServiceCollection services)
public void ConfigureRetailServices(IServiceCollection services)
{
services.ConfigureOptions<FakeOptions>(o =>
services.Configure<FakeOptions>(o =>
{
o.Configured = true;
o.Environment = "Retail";
});
}
public static void ConfigureServicesStatic(IServiceCollection services)
public static void ConfigureStaticServices(IServiceCollection services)
{
services.ConfigureOptions<FakeOptions>(o =>
services.Configure<FakeOptions>(o =>
{
o.Configured = true;
o.Environment = "Static";
});
}
public void Configure(IApplicationBuilder builder)
public virtual void Configure(IApplicationBuilder builder)
{
}
}

View File

@ -0,0 +1,12 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
namespace Microsoft.AspNet.Hosting.Fakes
{
public class StartupBoom
{
public StartupBoom()
{
}
}
}

View File

@ -0,0 +1,30 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Builder;
using Microsoft.Framework.DependencyInjection;
namespace Microsoft.AspNet.Hosting.Fakes
{
public class StartupUseServices
{
public StartupUseServices()
{
}
public void ConfigureUseServicesServices(IServiceCollection services)
{
services.Configure<FakeOptions>(o => o.Configured = true);
services.AddTransient<IFakeService, FakeService>();
}
public void Configure(IApplicationBuilder builder)
{
builder.UseServices(services =>
{
services.AddTransient<FakeService>();
services.Configure<FakeOptions>(o => o.Message = "Configured");
});
}
}
}

View File

@ -16,6 +16,7 @@
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>29216</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

@ -53,14 +53,37 @@ namespace Microsoft.AspNet.Hosting
startup.Invoke(app);
var options = app.ApplicationServices.GetService<IOptionsAccessor<FakeOptions>>().Options;
var options = app.ApplicationServices.GetService<IOptions<FakeOptions>>().Options;
Assert.NotNull(options);
Assert.True(options.Configured);
Assert.Equal(environment, options.Environment);
}
[Fact(Skip = "DataProtection registers default Options services; need to figure out what to do with this test.")]
public void StartupClassDoesNotRegisterOptionsWithNoConfigureServices()
[Fact]
public void StartupClassWithConfigureServicesAndUseServicesAddsBothToServices()
{
var serviceCollection = new ServiceCollection();
serviceCollection.Add(HostingServices.GetDefaultServices());
var services = serviceCollection.BuildServiceProvider();
var manager = services.GetService<IStartupManager>();
var startup = manager.LoadStartup("Microsoft.AspNet.Hosting.Tests", "UseServices");
var app = new ApplicationBuilder(services);
startup.Invoke(app);
Assert.NotNull(app.ApplicationServices.GetService<FakeService>());
Assert.NotNull(app.ApplicationServices.GetService<IFakeService>());
var options = app.ApplicationServices.GetService<IOptions<FakeOptions>>().Options;
Assert.NotNull(options);
Assert.Equal("Configured", options.Message);
Assert.False(options.Configured); // REVIEW: why doesn't the ConfigureServices ConfigureOptions get run?
}
[Fact]
public void StartupWithNoConfigureThrows()
{
var serviceCollection = new ServiceCollection();
serviceCollection.Add(HostingServices.GetDefaultServices());
@ -68,14 +91,8 @@ namespace Microsoft.AspNet.Hosting
var services = serviceCollection.BuildServiceProvider();
var manager = services.GetService<IStartupManager>();
var startup = manager.LoadStartup("Microsoft.AspNet.Hosting.Tests", "NoServices");
var app = new ApplicationBuilder(services);
startup.Invoke(app);
var ex = Assert.Throws<Exception>(() => app.ApplicationServices.GetService<IOptionsAccessor<FakeOptions>>());
Assert.True(ex.Message.Contains("No service for type 'Microsoft.Framework.OptionsModel.IOptionsAccessor"));
var ex = Assert.Throws<Exception>(() => manager.LoadStartup("Microsoft.AspNet.Hosting.Tests", "Boom"));
Assert.True(ex.Message.Contains("ConfigureBoom or Configure method not found"));
}
public void ConfigurationMethodCalled(object instance)

View File

@ -0,0 +1,79 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.Builder;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Xunit;
using Microsoft.AspNet.PipelineCore;
using Microsoft.AspNet.RequestContainer;
namespace Microsoft.AspNet.Hosting.Tests
{
public class RequestServicesContainerFacts
{
[Fact]
public void RequestServicesAvailableOnlyAfterRequestServices()
{
var baseServiceProvider = new ServiceCollection()
.Add(HostingServices.GetDefaultServices())
.BuildServiceProvider();
var builder = new ApplicationBuilder(baseServiceProvider);
bool foundRequestServicesBefore = false;
builder.Use(next => async c =>
{
foundRequestServicesBefore = c.RequestServices != null;
await next.Invoke(c);
});
builder.UseRequestServices();
bool foundRequestServicesAfter = false;
builder.Use(next => async c =>
{
foundRequestServicesAfter = c.RequestServices != null;
await next.Invoke(c);
});
var context = new DefaultHttpContext();
builder.Build().Invoke(context);
Assert.False(foundRequestServicesBefore);
Assert.True(foundRequestServicesAfter);
}
[Fact]
public void EnsureRequestServicesSetsRequestServices()
{
var baseServiceProvider = new ServiceCollection()
.Add(HostingServices.GetDefaultServices())
.BuildServiceProvider();
var builder = new ApplicationBuilder(baseServiceProvider);
bool foundRequestServicesBefore = false;
builder.Use(next => async c =>
{
foundRequestServicesBefore = c.RequestServices != null;
await next.Invoke(c);
});
builder.Use(next => async c =>
{
using (var container = RequestServicesContainer.EnsureRequestServices(c))
{
await next.Invoke(c);
}
});
bool foundRequestServicesAfter = false;
builder.Use(next => async c =>
{
foundRequestServicesAfter = c.RequestServices != null;
await next.Invoke(c);
});
var context = new DefaultHttpContext();
context.ApplicationServices = baseServiceProvider;
builder.Build().Invoke(context);
Assert.False(foundRequestServicesBefore);
Assert.True(foundRequestServicesAfter);
}
}
}

View File

@ -7,6 +7,8 @@ using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback;
using Microsoft.Framework.OptionsModel;
using Xunit;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.PipelineCore;
namespace Microsoft.AspNet.Hosting.Tests
{
@ -18,9 +20,9 @@ namespace Microsoft.AspNet.Hosting.Tests
var baseServiceProvider = new ServiceCollection().BuildServiceProvider();
var builder = new ApplicationBuilder(baseServiceProvider);
builder.UsePerRequestServices(serviceCollection => { });
builder.UseServices(serviceCollection => { });
var optionsAccessor = builder.ApplicationServices.GetService<IOptionsAccessor<object>>();
var optionsAccessor = builder.ApplicationServices.GetService<IOptions<object>>();
Assert.NotNull(optionsAccessor);
}
@ -32,14 +34,14 @@ namespace Microsoft.AspNet.Hosting.Tests
var builder = new ApplicationBuilder(baseServiceProvider);
IServiceProvider serviceProvider = null;
builder.UsePerRequestServices(serviceCollection =>
builder.UseServices(serviceCollection =>
{
serviceProvider = serviceCollection.BuildServiceProvider(builder.ApplicationServices);
return serviceProvider;
});
Assert.Same(serviceProvider, builder.ApplicationServices);
var optionsAccessor = builder.ApplicationServices.GetService<IOptionsAccessor<object>>();
var optionsAccessor = builder.ApplicationServices.GetService<IOptions<object>>();
Assert.NotNull(optionsAccessor);
}
}

View File

@ -16,6 +16,7 @@
</PropertyGroup>
<PropertyGroup>
<SchemaVersion>2.0</SchemaVersion>
<DevelopmentServerPort>29215</DevelopmentServerPort>
</PropertyGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>