Add support for executing IHostingStartup in specified assemblies (#961)

* Add support for executing IHostingStartup in specified assemblies
- Assemblies that are specified in the "hostingStartupAssemblies" configuration (; delimited)
  setting can specify assemblies that use an assembly level attribute (HostingStartupAttribute)
  to specify a type that implements IHostingStartup. This allows hosting environments to
  extend the IWebHostBuilder with platform specific behavior before the application runs.
- Added tests
- Log errors that occur during load and execution of the IHostingStartup
when capture startup errors is off. This happens on start of the application.
- Added debug logging on startup to print out the hosted startup assemblies hosting
processed

#951
This commit is contained in:
David Fowler 2017-03-22 20:23:04 -07:00 committed by GitHub
parent 387e2d8ad1
commit ddb1bfeb20
10 changed files with 311 additions and 9 deletions

View File

@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Reflection;
namespace Microsoft.AspNetCore.Hosting
{
/// <summary>
/// Marker attribute indicating an implementation of <see cref="IHostingStartup"/> that will be loaded and executed when building an <see cref="IWebHost"/>.
/// </summary>
[AttributeUsage(AttributeTargets.Assembly, Inherited = false, AllowMultiple = true)]
public sealed class HostingStartupAttribute : Attribute
{
/// <summary>
/// Constructs the <see cref="HostingStartupAttribute"/> with the specified type.
/// </summary>
/// <param name="hostingStartupType">A type that implements <see cref="IHostingStartup"/>.</param>
public HostingStartupAttribute(Type hostingStartupType)
{
if (hostingStartupType == null)
{
throw new ArgumentNullException(nameof(hostingStartupType));
}
if (!typeof(IHostingStartup).GetTypeInfo().IsAssignableFrom(hostingStartupType.GetTypeInfo()))
{
throw new ArgumentException($@"""{hostingStartupType}"" does not implement {typeof(IHostingStartup)}.", nameof(hostingStartupType));
}
HostingStartupType = hostingStartupType;
}
/// <summary>
/// The implementation of <see cref="IHostingStartup"/> that should be loaded when
/// starting an application.
/// </summary>
public Type HostingStartupType { get; }
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
namespace Microsoft.AspNetCore.Hosting
{
/// <summary>
/// Represents platform specific configuration that will be applied to a <see cref="IWebHostBuilder"/> when building an <see cref="IWebHost"/>.
/// </summary>
public interface IHostingStartup
{
/// <summary>
/// Configure the <see cref="IWebHostBuilder"/>.
/// </summary>
/// <remarks>
/// Configure is intended to be called before user code, allowing a user to overwrite any changes made.
/// </remarks>
/// <param name="builder"></param>
void Configure(IWebHostBuilder builder);
}
}

View File

@ -7,7 +7,8 @@ namespace Microsoft.AspNetCore.Hosting
{
public static readonly string ApplicationKey = "applicationName";
public static readonly string StartupAssemblyKey = "startupAssembly";
public static readonly string HostingStartupAssembliesKey = "hostingStartupAssemblies";
public static readonly string DetailedErrorsKey = "detailedErrors";
public static readonly string EnvironmentKey = "environment";
public static readonly string WebRootKey = "webroot";

View File

@ -26,6 +26,14 @@ namespace Microsoft.AspNetCore.Hosting.Internal
exception: exception);
}
public static void HostingStartupAssemblyError(this ILogger logger, Exception exception)
{
logger.ApplicationError(
eventId: LoggerEventIds.HostingStartupAssemblyException,
message: "Hosting startup assembly exception",
exception: exception);
}
public static void ApplicationError(this ILogger logger, EventId eventId, string message, Exception exception)
{
var reflectionTypeLoadException = exception as ReflectionTypeLoadException;

View File

@ -15,5 +15,6 @@ namespace Microsoft.AspNetCore.Hosting.Internal
public const int ApplicationStoppedException = 8;
public const int HostedServiceStartException = 9;
public const int HostedServiceStopException = 10;
public const int HostingStartupAssemblyException = 11;
}
}

View File

@ -33,6 +33,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
private readonly IServiceProvider _hostingServiceProvider;
private readonly WebHostOptions _options;
private readonly IConfiguration _config;
private readonly AggregateException _hostingStartupErrors;
private IServiceProvider _applicationServices;
private RequestDelegate _application;
@ -47,7 +48,8 @@ namespace Microsoft.AspNetCore.Hosting.Internal
IServiceCollection appServices,
IServiceProvider hostingServiceProvider,
WebHostOptions options,
IConfiguration config)
IConfiguration config,
AggregateException hostingStartupErrors)
{
if (appServices == null)
{
@ -65,6 +67,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
}
_config = config;
_hostingStartupErrors = hostingStartupErrors;
_options = options;
_applicationServiceCollection = appServices;
_hostingServiceProvider = hostingServiceProvider;
@ -118,6 +121,26 @@ namespace Microsoft.AspNetCore.Hosting.Internal
_hostedServiceExecutor.Start();
_logger.Started();
// REVIEW: Is this the right place to log these errors?
if (_hostingStartupErrors != null)
{
foreach (var exception in _hostingStartupErrors.InnerExceptions)
{
_logger.HostingStartupAssemblyError(exception);
}
}
else
{
// If there were no errors then just log the fact that we did load hosting startup assemblies.
if (_logger.IsEnabled(LogLevel.Debug))
{
foreach (var assembly in _options.HostingStartupAssemblies)
{
_logger.LogDebug("Loaded hosting startup assembly {assemblyName}", assembly);
}
}
}
}
private void EnsureApplicationServices()

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using Microsoft.Extensions.Configuration;
namespace Microsoft.AspNetCore.Hosting.Internal
@ -26,10 +27,13 @@ namespace Microsoft.AspNetCore.Hosting.Internal
Environment = configuration[WebHostDefaults.EnvironmentKey];
WebRoot = configuration[WebHostDefaults.WebRootKey];
ContentRootPath = configuration[WebHostDefaults.ContentRootKey];
HostingStartupAssemblies = configuration[WebHostDefaults.HostingStartupAssembliesKey]?.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries) ?? new string[0];
}
public string ApplicationName { get; set; }
public IReadOnlyList<string> HostingStartupAssemblies { get; set; }
public bool DetailedErrors { get; set; }
public bool CaptureStartupErrors { get; set; }

View File

@ -158,7 +158,7 @@ namespace Microsoft.AspNetCore.Hosting
Console.WriteLine("The environment variable 'ASPNETCORE_SERVER.URLS' is obsolete and has been replaced with 'ASPNETCORE_URLS'");
}
var hostingServices = BuildCommonServices();
var hostingServices = BuildCommonServices(out var hostingStartupErrors);
var applicationServices = hostingServices.Clone();
var hostingServiceProvider = hostingServices.BuildServiceProvider();
@ -168,15 +168,18 @@ namespace Microsoft.AspNetCore.Hosting
applicationServices,
hostingServiceProvider,
_options,
_config);
_config,
hostingStartupErrors);
host.Initialize();
return host;
}
private IServiceCollection BuildCommonServices()
private IServiceCollection BuildCommonServices(out AggregateException hostingStartupErrors)
{
hostingStartupErrors = null;
_options = new WebHostOptions(_config);
var appEnvironment = PlatformServices.Default.Application;
@ -200,6 +203,39 @@ namespace Microsoft.AspNetCore.Hosting
services.AddSingleton(_loggerFactory);
}
var exceptions = new List<Exception>();
// Execute the hosting startup assemblies
foreach (var assemblyName in _options.HostingStartupAssemblies)
{
try
{
var assembly = Assembly.Load(new AssemblyName(assemblyName));
foreach (var attribute in assembly.GetCustomAttributes<HostingStartupAttribute>())
{
var hostingStartup = (IHostingStartup)Activator.CreateInstance(attribute.HostingStartupType);
hostingStartup.Configure(this);
}
}
catch (Exception ex)
{
// Capture any errors that happen during startup
exceptions.Add(new InvalidOperationException($"Startup assembly {assemblyName} failed to execute. See the inner exception for more details.", ex));
}
}
if (exceptions.Count > 0)
{
hostingStartupErrors = new AggregateException(exceptions);
// Throw directly if we're not capturing startup errors
if (!_options.CaptureStartupErrors)
{
throw hostingStartupErrors;
}
}
foreach (var configureLogging in _configureLoggingDelegates)
{
configureLogging(_loggerFactory);

View File

@ -18,6 +18,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Owin" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.AspNetCore.Testing" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.Options" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.Extensions.PlatformAbstractions" Version="$(AspNetCoreVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />

View File

@ -4,9 +4,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Hosting.Fakes;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Hosting.Server;
@ -16,10 +18,13 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Testing;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.PlatformAbstractions;
using Xunit;
[assembly: HostingStartup(typeof(WebHostBuilderTests.TestHostingStartup))]
namespace Microsoft.AspNetCore.Hosting
{
public class WebHostBuilderTests
@ -588,6 +593,121 @@ namespace Microsoft.AspNetCore.Hosting
Assert.Equal(factory, factoryFromHost);
}
[Fact]
public void Build_RunsHostingStartupAssembliesIfSpecified()
{
var builder = CreateWebHostBuilder()
.CaptureStartupErrors(false)
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(WebHostBuilderTests).GetTypeInfo().Assembly.FullName)
.Configure(app => { })
.UseServer(new TestServer());
var host = (WebHost)builder.Build();
Assert.Equal("1", builder.GetSetting("testhostingstartup"));
}
[Fact]
public void Build_RunsHostingStartupAssembliesBeforeApplication()
{
var startup = new StartupVerifyServiceA();
var startupAssemblyName = typeof(WebHostBuilderTests).GetTypeInfo().Assembly.GetName().Name;
var builder = CreateWebHostBuilder()
.CaptureStartupErrors(false)
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(WebHostBuilderTests).GetTypeInfo().Assembly.FullName)
.UseSetting(WebHostDefaults.ApplicationKey, startupAssemblyName)
.ConfigureServices(services =>
{
services.AddSingleton<IStartup>(startup);
})
.UseServer(new TestServer());
var host = (WebHost)builder.Build();
Assert.NotNull(startup.ServiceADescriptor);
Assert.NotNull(startup.ServiceA);
}
[Fact]
public void Build_ConfigureLoggingInHostingStartupWorks()
{
var builder = CreateWebHostBuilder()
.CaptureStartupErrors(false)
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, typeof(WebHostBuilderTests).GetTypeInfo().Assembly.FullName)
.Configure(app =>
{
var loggerFactory = app.ApplicationServices.GetService<ILoggerFactory>();
var logger = loggerFactory.CreateLogger(nameof(WebHostBuilderTests));
logger.LogInformation("From startup");
})
.UseServer(new TestServer());
var host = (WebHost)builder.Build();
var sink = host.Services.GetRequiredService<ITestSink>();
Assert.True(sink.Writes.Any(w => w.State.ToString() == "From startup"));
}
[Fact]
public void Build_DoesNotRunHostingStartupAssembliesDoNotRunIfNotSpecified()
{
var builder = CreateWebHostBuilder()
.Configure(app => { })
.UseServer(new TestServer());
var host = (WebHost)builder.Build();
Assert.Null(builder.GetSetting("testhostingstartup"));
}
[Fact]
public void Build_ThrowsIfUnloadableAssemblyNameInHostingStartupAssemblies()
{
var builder = CreateWebHostBuilder()
.CaptureStartupErrors(false)
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "SomeBogusName")
.Configure(app => { })
.UseServer(new TestServer());
var ex = Assert.Throws<AggregateException>(() => (WebHost)builder.Build());
Assert.IsType<InvalidOperationException>(ex.InnerExceptions[0]);
Assert.IsType<FileNotFoundException>(ex.InnerExceptions[0].InnerException);
}
[Fact]
public void Build_DoesNotThrowIfUnloadableAssemblyNameInHostingStartupAssembliesAndCaptureStartupErrorsTrue()
{
var provider = new TestLoggerProvider();
var builder = CreateWebHostBuilder()
.ConfigureLogging(factory =>
{
factory.AddProvider(provider);
})
.CaptureStartupErrors(true)
.UseSetting(WebHostDefaults.HostingStartupAssembliesKey, "SomeBogusName")
.Configure(app => { })
.UseServer(new TestServer());
using (var host = builder.Build())
{
host.Start();
var context = provider.Sink.Writes.FirstOrDefault(s => s.EventId.Id == LoggerEventIds.HostingStartupAssemblyException);
Assert.NotNull(context);
}
}
[Fact]
public void HostingStartupTypeCtorThrowsIfNull()
{
Assert.Throws<ArgumentNullException>(() => new HostingStartupAttribute(null));
}
[Fact]
public void HostingStartupTypeCtorThrowsIfNotIHosting()
{
Assert.Throws<ArgumentException>(() => new HostingStartupAttribute(typeof(WebHostTests)));
}
private static void StaticConfigureMethod(IApplicationBuilder app)
{ }
@ -643,6 +763,52 @@ namespace Microsoft.AspNetCore.Hosting
}
}
internal class StartupVerifyServiceA : IStartup
{
internal ServiceA ServiceA { get; set; }
internal ServiceDescriptor ServiceADescriptor { get; set; }
public IServiceProvider ConfigureServices(IServiceCollection services)
{
ServiceADescriptor = services.FirstOrDefault(s => s.ServiceType == typeof(ServiceA));
return services.BuildServiceProvider();
}
public void Configure(IApplicationBuilder app)
{
ServiceA = app.ApplicationServices.GetService<ServiceA>();
}
}
public class TestHostingStartup : IHostingStartup
{
public void Configure(IWebHostBuilder builder)
{
var loggerProvider = new TestLoggerProvider();
builder.UseSetting("testhostingstartup", "1")
.ConfigureServices(services => services.AddSingleton<ServiceA>())
.ConfigureServices(services => services.AddSingleton<ITestSink>(loggerProvider.Sink))
.ConfigureLogging(lf => lf.AddProvider(loggerProvider));
}
}
public class TestLoggerProvider : ILoggerProvider
{
public TestSink Sink { get; set; } = new TestSink();
public ILogger CreateLogger(string categoryName)
{
return new TestLogger(categoryName, Sink, enabled: true);
}
public void Dispose()
{
}
}
private class ServiceC
{
public ServiceC(ServiceD serviceD)
@ -651,17 +817,17 @@ namespace Microsoft.AspNetCore.Hosting
}
}
private class ServiceD
internal class ServiceD
{
}
private class ServiceA
internal class ServiceA
{
}
private class ServiceB
internal class ServiceB
{
}