From 53eea70c067e47763f3cbf3f9039116f77ed62fa Mon Sep 17 00:00:00 2001 From: Hao Kung Date: Fri, 3 Oct 2014 18:23:50 -0700 Subject: [PATCH] Startup: Call ConfigureServices if exists --- .../Startup/StartupLoader.cs | 118 +++++++++++------- src/Microsoft.AspNet.Hosting/project.json | 1 + .../Fakes/FakeOptions.cs | 25 ++++ .../Fakes/Startup.cs | 34 +++++ .../Fakes/StartupNoServices.cs | 34 +++++ .../StartupManagerTests.cs | 46 +++++++ 6 files changed, 214 insertions(+), 44 deletions(-) create mode 100644 test/Microsoft.AspNet.Hosting.Tests/Fakes/FakeOptions.cs create mode 100644 test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupNoServices.cs diff --git a/src/Microsoft.AspNet.Hosting/Startup/StartupLoader.cs b/src/Microsoft.AspNet.Hosting/Startup/StartupLoader.cs index cce003a648..c87a47cafc 100644 --- a/src/Microsoft.AspNet.Hosting/Startup/StartupLoader.cs +++ b/src/Microsoft.AspNet.Hosting/Startup/StartupLoader.cs @@ -20,8 +20,9 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using Microsoft.AspNet.Builder; -using Microsoft.AspNet.Http; using Microsoft.Framework.DependencyInjection; +using Microsoft.Framework.DependencyInjection.Fallback; +using Microsoft.Framework.OptionsModel; namespace Microsoft.AspNet.Hosting.Startup { @@ -38,6 +39,66 @@ namespace Microsoft.AspNet.Hosting.Startup _next = next; } + private MethodInfo FindMethod(Type startupType, string methodName, string environmentName, Type returnType = null, bool required = true) + { + var methodNameWithEnv = methodName + environmentName; + var methodInfo = startupType.GetTypeInfo().GetDeclaredMethod(methodNameWithEnv) + ?? startupType.GetTypeInfo().GetDeclaredMethod(methodName); + if (methodInfo == null) + { + if (required) + { + throw new Exception(string.Format("TODO: {0} or {1} method not found", + methodNameWithEnv, + methodName)); + + } + return null; + } + + if (returnType != null && methodInfo.ReturnType != returnType) + { + throw new Exception(string.Format("TODO: {0} method does not return " + returnType.Name, + methodInfo.Name)); + } + + return methodInfo; + } + + private void Invoke(MethodInfo methodInfo, object instance, IApplicationBuilder builder, IServiceCollection services = null) + { + var parameterInfos = methodInfo.GetParameters(); + var parameters = new object[parameterInfos.Length]; + for (var index = 0; index != parameterInfos.Length; ++index) + { + var parameterInfo = parameterInfos[index]; + if (parameterInfo.ParameterType == typeof(IApplicationBuilder)) + { + parameters[index] = builder; + } + else if (services != null && parameterInfo.ParameterType == typeof(IServiceCollection)) + { + parameters[index] = services; + } + else + { + try + { + parameters[index] = _services.GetService(parameterInfo.ParameterType); + } + catch (Exception) + { + throw new Exception(string.Format( + "TODO: Unable to resolve service for {0} method {1} {2}", + methodInfo.Name, + parameterInfo.Name, + parameterInfo.ParameterType.FullName)); + } + } + } + methodInfo.Invoke(instance, parameters); + } + public Action LoadStartup( string applicationName, string environmentName, @@ -81,65 +142,34 @@ namespace Microsoft.AspNet.Hosting.Startup if (type == null) { - throw new Exception(String.Format("TODO: {0} or {1} class not found in assembly {2}", + throw new Exception(String.Format("TODO: {0} or {1} class not found in assembly {2}", startupName1, startupName2, applicationName)); } - var configureMethod1 = "Configure" + environmentName; - var configureMethod2 = "Configure"; - var methodInfo = type.GetTypeInfo().GetDeclaredMethod(configureMethod1); - if (methodInfo == null) - { - methodInfo = type.GetTypeInfo().GetDeclaredMethod(configureMethod2); - } - if (methodInfo == null) - { - throw new Exception(string.Format("TODO: {0} or {1} method not found", - configureMethod1, - configureMethod2)); - } - - if (methodInfo.ReturnType != typeof(void)) - { - throw new Exception(string.Format("TODO: {0} method isn't void-returning.", - methodInfo.Name)); - } + var configureMethod = FindMethod(type, "Configure", environmentName, typeof(void), required: true); + // TODO: accept IServiceProvider method as well? + var servicesMethod = FindMethod(type, "ConfigureServices", environmentName, typeof(void), required: false); object instance = null; - if (!methodInfo.IsStatic) + if (!configureMethod.IsStatic || (servicesMethod != null && !servicesMethod.IsStatic)) { instance = ActivatorUtilities.GetServiceOrCreateInstance(_services, type); } - return builder => { - var parameterInfos = methodInfo.GetParameters(); - var parameters = new object[parameterInfos.Length]; - for (var index = 0; index != parameterInfos.Length; ++index) + if (servicesMethod != null) { - var parameterInfo = parameterInfos[index]; - if (parameterInfo.ParameterType == typeof(IApplicationBuilder)) + var services = new ServiceCollection(); + services.Add(OptionsServices.GetDefaultServices()); + Invoke(servicesMethod, instance, builder, services); + if (builder != null) { - parameters[index] = builder; - } - else - { - try - { - parameters[index] = _services.GetService(parameterInfo.ParameterType); - } - catch (Exception ex) - { - throw new Exception(string.Format( - "TODO: Unable to resolve service for startup method {0} {1}", - parameterInfo.Name, - parameterInfo.ParameterType.FullName)); - } + builder.ApplicationServices = services.BuildServiceProvider(builder.ApplicationServices); } } - methodInfo.Invoke(instance, parameters); + Invoke(configureMethod, instance, builder); }; } } diff --git a/src/Microsoft.AspNet.Hosting/project.json b/src/Microsoft.AspNet.Hosting/project.json index 143ab70d0f..3bc77a7eb6 100644 --- a/src/Microsoft.AspNet.Hosting/project.json +++ b/src/Microsoft.AspNet.Hosting/project.json @@ -8,6 +8,7 @@ "Microsoft.Framework.ConfigurationModel": "1.0.0-*", "Microsoft.Framework.DependencyInjection": "1.0.0-*", "Microsoft.Framework.Logging": "1.0.0-*", + "Microsoft.Framework.OptionsModel": "1.0.0-*", "Microsoft.Framework.Runtime.Interfaces": { "version": "1.0.0-*", "type": "build" }, "Newtonsoft.Json": "6.0.4" }, diff --git a/test/Microsoft.AspNet.Hosting.Tests/Fakes/FakeOptions.cs b/test/Microsoft.AspNet.Hosting.Tests/Fakes/FakeOptions.cs new file mode 100644 index 0000000000..6d6d2c0982 --- /dev/null +++ b/test/Microsoft.AspNet.Hosting.Tests/Fakes/FakeOptions.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +namespace Microsoft.AspNet.Hosting.Fakes +{ + public class FakeOptions + { + public bool Configured { get; set; } + public string Environment { get; set; } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Hosting.Tests/Fakes/Startup.cs b/test/Microsoft.AspNet.Hosting.Tests/Fakes/Startup.cs index fb05a63b60..1421544258 100644 --- a/test/Microsoft.AspNet.Hosting.Tests/Fakes/Startup.cs +++ b/test/Microsoft.AspNet.Hosting.Tests/Fakes/Startup.cs @@ -17,6 +17,7 @@ using Microsoft.AspNet.Builder; using Microsoft.AspNet.Http; +using Microsoft.Framework.DependencyInjection; namespace Microsoft.AspNet.Hosting.Fakes { @@ -26,6 +27,39 @@ namespace Microsoft.AspNet.Hosting.Fakes { } + public void ConfigureServices(IServiceCollection services) + { + services.ConfigureOptions(o => o.Configured = true); + } + + public void ConfigureServicesDev(IServiceCollection services) + { + services.ConfigureOptions(o => + { + o.Configured = true; + o.Environment = "Dev"; + }); + } + + public void ConfigureServicesRetail(IServiceCollection services) + { + services.ConfigureOptions(o => + { + o.Configured = true; + o.Environment = "Retail"; + }); + } + + public static void ConfigureServicesStatic(IServiceCollection services) + { + services.ConfigureOptions(o => + { + o.Configured = true; + o.Environment = "Static"; + }); + } + + public void Configure(IApplicationBuilder builder) { } diff --git a/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupNoServices.cs b/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupNoServices.cs new file mode 100644 index 0000000000..24a329073a --- /dev/null +++ b/test/Microsoft.AspNet.Hosting.Tests/Fakes/StartupNoServices.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Open Technologies, Inc. +// All Rights Reserved +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING +// WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF +// TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR +// NON-INFRINGEMENT. +// See the Apache 2 License for the specific language governing +// permissions and limitations under the License. + +using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Http; +using Microsoft.Framework.DependencyInjection; + +namespace Microsoft.AspNet.Hosting.Fakes +{ + public class StartupNoServices + { + public StartupNoServices() + { + } + + public void Configure(IApplicationBuilder builder) + { + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNet.Hosting.Tests/StartupManagerTests.cs b/test/Microsoft.AspNet.Hosting.Tests/StartupManagerTests.cs index 6684a56205..97248b78c6 100644 --- a/test/Microsoft.AspNet.Hosting.Tests/StartupManagerTests.cs +++ b/test/Microsoft.AspNet.Hosting.Tests/StartupManagerTests.cs @@ -21,6 +21,9 @@ using Microsoft.AspNet.Hosting.Startup; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.Fallback; using Xunit; +using Microsoft.Framework.OptionsModel; +using Microsoft.AspNet.Builder; +using System; namespace Microsoft.AspNet.Hosting { @@ -46,6 +49,49 @@ namespace Microsoft.AspNet.Hosting Assert.Equal(2, _configurationMethodCalledList.Count); } + [Theory] + [InlineData(null)] + [InlineData("Dev")] + [InlineData("Retail")] + [InlineData("Static")] + public void StartupClassAddsConfigureServicesToApplicationServices(string environment) + { + var serviceCollection = new ServiceCollection(); + serviceCollection.Add(HostingServices.GetDefaultServices()); + var services = serviceCollection.BuildServiceProvider(); + var manager = services.GetService(); + + var startup = manager.LoadStartup("Microsoft.AspNet.Hosting.Tests", environment ?? ""); + + var app = new ApplicationBuilder(services); + + startup.Invoke(app); + + var options = app.ApplicationServices.GetService>().Options; + Assert.NotNull(options); + Assert.True(options.Configured); + Assert.Equal(environment, options.Environment); + } + + [Fact] + public void StartupClassDoesNotRegisterOptionsWithNoConfigureServices() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.Add(HostingServices.GetDefaultServices()); + serviceCollection.AddInstance(this); + var services = serviceCollection.BuildServiceProvider(); + var manager = services.GetService(); + + var startup = manager.LoadStartup("Microsoft.AspNet.Hosting.Tests", "NoServices"); + + var app = new ApplicationBuilder(services); + + startup.Invoke(app); + + var ex = Assert.Throws(() => app.ApplicationServices.GetService>()); + Assert.True(ex.Message.Contains("No service for type 'Microsoft.Framework.OptionsModel.IOptionsAccessor")); + } + public void ConfigurationMethodCalled(object instance) { _configurationMethodCalledList.Add(instance);