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:
parent
387e2d8ad1
commit
ddb1bfeb20
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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)" />
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue