Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@ protected virtual void Configure(IModuleContext<TModule> moduleContext, IApplica
BootstrapContext.Advanced.FrameworkServices.GetServices<IWebModuleConfigureFilter>(),
(ctx, appBuilder) =>
{
ModuleMethodInvoker.CallOptionalMethod(BootstrapContext.ToModuleContext(app.ApplicationServices), "Configure", appBuilder);
var innerContext = BootstrapContext.ToModuleContext(app.ApplicationServices);
if (innerContext.Module is IApplicationConfiguringModule configuringModule)
configuringModule.Configure(innerContext.ModulesHostServices, appBuilder);
else
ModuleMethodInvoker.CallOptionalMethod(innerContext, "Configure", appBuilder);

})(moduleContext, app);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using Microsoft.AspNetCore.Builder;

namespace Dbosoft.Hosuto.Modules
{
/// <summary>
/// Opt-in, statically typed alternative to a web module's conventional <c>Configure</c> method.
/// When a module implements this interface the framework invokes it directly instead of locating
/// the method via reflection. Modules that do not implement it keep working through the
/// reflection convention.
/// </summary>
/// <remarks>
/// If a module implements this interface, the framework calls it and ignores any conventional
/// <c>Configure</c> method. The <paramref name="serviceProvider"/> matches what the convention
/// injects as a method's <em>first</em> <see cref="IServiceProvider"/> parameter (the modules
/// host's service provider); a conventional method that placed <see cref="IServiceProvider"/> in
/// a later position could receive a different provider.
/// </remarks>
public interface IApplicationConfiguringModule
{
/// <param name="serviceProvider">The modules host's service provider.</param>
/// <param name="app">The module's application pipeline builder.</param>
void Configure(IServiceProvider serviceProvider, IApplicationBuilder app);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ protected virtual void ConfigureServices(HostBuilderContext hostBuilderContext,
.Append(moduleServicesFilterAdapter)
, (ctx, s) =>
{
ModuleMethodInvoker.CallOptionalMethod(BootstrapContext.ToModuleContext(serviceProvider), "ConfigureServices", serviceProvider, s);
var moduleContext = BootstrapContext.ToModuleContext(serviceProvider);
if (moduleContext.Module is IServiceConfiguringModule configuringModule)
configuringModule.ConfigureServices(moduleContext.ModulesHostServices, s);
else
ModuleMethodInvoker.CallOptionalMethod(moduleContext, "ConfigureServices", serviceProvider, s);
})
(BootstrapContext.ToModuleHostBuilderContext(hostBuilderContext), services);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@ public Action<IModulesHostBuilderContext, IServiceCollection> Invoke(Action<IMod
.Append(GenericModuleHostBuilderContextAdapter<SimpleInjectorAddOptions>.Create(typeof(IAddSimpleInjectorFilter<>))),
(ctx, o) =>
{
ModuleMethodInvoker.CallOptionalMethod(containerContext, "AddSimpleInjector", o);
if (containerContext.Module is IAddSimpleInjectorModule module)
module.AddSimpleInjector(o);
else
ModuleMethodInvoker.CallOptionalMethod(containerContext, "AddSimpleInjector", o);

})(context, options);

Expand Down
10 changes: 8 additions & 2 deletions src/Hosuto.SimpleInjector/Modules/Hosting/BootstrapHostFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,10 @@ private static void UseSimpleInjector(IModuleContext moduleContext, SimpleInject
.Append(useSimpleInjectorAdapter),
(ctx, o) =>
{
ModuleMethodInvoker.CallOptionalMethod(ctx, "UseSimpleInjector", o);
if (ctx.Module is IUseSimpleInjectorModule module)
module.UseSimpleInjector(o);
else
ModuleMethodInvoker.CallOptionalMethod(ctx, "UseSimpleInjector", o);

})(moduleContext, options);

Expand All @@ -36,7 +39,10 @@ private static void ConfigureContainer(IModuleContext moduleContext, Container c
.Append(configureContainerAdapter),
(ctx, c) =>
{
ModuleMethodInvoker.CallOptionalMethod(ctx, "ConfigureContainer", container);
if (ctx.Module is IContainerConfiguringModule module)
module.ConfigureContainer(ctx.ModulesHostServices, container);
else
ModuleMethodInvoker.CallOptionalMethod(ctx, "ConfigureContainer", container);

})(moduleContext, container);

Expand Down
39 changes: 39 additions & 0 deletions src/Hosuto.SimpleInjector/Modules/ModuleContainerContracts.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System;
using SimpleInjector;
using SimpleInjector.Integration.ServiceCollection;

namespace Dbosoft.Hosuto.Modules
{
/// <summary>
/// Opt-in, statically typed alternative to a module's conventional <c>ConfigureContainer</c>
/// method. Implementing this interface lets the framework invoke it directly instead of via
/// reflection. If a module implements it, any conventional <c>ConfigureContainer</c> method is
/// ignored.
/// </summary>
public interface IContainerConfiguringModule
{
/// <param name="serviceProvider">The modules host's service provider - the same instance the
/// convention injects as a method's first <see cref="IServiceProvider"/> parameter. This is
/// distinct from the <paramref name="container"/> being configured.</param>
/// <param name="container">The module container to configure.</param>
void ConfigureContainer(IServiceProvider serviceProvider, Container container);
}

/// <summary>
/// Opt-in, statically typed alternative to a module's conventional <c>UseSimpleInjector</c>
/// method. If a module implements it, any conventional <c>UseSimpleInjector</c> method is ignored.
/// </summary>
public interface IUseSimpleInjectorModule
{
void UseSimpleInjector(SimpleInjectorUseOptions options);
}

/// <summary>
/// Opt-in, statically typed alternative to a module's conventional <c>AddSimpleInjector</c>
/// method. If a module implements it, any conventional <c>AddSimpleInjector</c> method is ignored.
/// </summary>
public interface IAddSimpleInjectorModule
{
void AddSimpleInjector(SimpleInjectorAddOptions options);
}
}
25 changes: 25 additions & 0 deletions src/Hosuto/Modules/IServiceConfiguringModule.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using System;
using Microsoft.Extensions.DependencyInjection;

namespace Dbosoft.Hosuto.Modules
{
/// <summary>
/// Opt-in, statically typed alternative to a module's conventional <c>ConfigureServices</c>
/// method. When a module implements this interface the framework invokes it directly instead of
/// locating the method via reflection - which is trimming/AOT friendly and checked at compile
/// time. Modules that do not implement it keep working through the reflection convention.
/// </summary>
/// <remarks>
/// If a module implements this interface, the framework calls it and ignores any conventional
/// <c>ConfigureServices</c> method. The <paramref name="serviceProvider"/> matches what the
/// convention injects as a method's <em>first</em> <see cref="IServiceProvider"/> parameter; a
/// conventional method that declared <see cref="IServiceProvider"/> in a later position would
/// receive a different provider.
/// </remarks>
public interface IServiceConfiguringModule
{
/// <param name="serviceProvider">The modules host's service provider.</param>
/// <param name="services">The module host's service collection.</param>
void ConfigureServices(IServiceProvider serviceProvider, IServiceCollection services);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Dbosoft.Hosuto.HostedServices;
using Dbosoft.Hosuto.Modules;
using Dbosoft.Hosuto.Modules.Hosting;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Moq;
using SimpleInjector;
using SimpleInjector.Integration.ServiceCollection;
using Xunit;

namespace Hosuto.SimpleInjector.Tests.Modules.Hosting
{
// The module methods are implemented EXPLICITLY on purpose: an explicit interface implementation
// is not discoverable via Type.GetMethod("<name>"), so the reflection convention in
// ModuleMethodInvoker cannot find them. If the interface-dispatch branch at any call site were
// removed or broken, these tests would fail (the reflection fallback would find nothing) - which
// is what proves the interface path, not the fallback, is exercised.
public class ModuleInterfaceContractsTests
{
[Fact]
public async Task Interface_based_module_is_configured_and_runs()
{
var container = new Container();

var serviceMock = new Mock<IService>();
serviceMock.Setup(x => x.CallMe()).Verifiable();

var builder = ModulesHost.CreateDefaultBuilder();
builder.UseSimpleInjector(container);
container.RegisterInstance(serviceMock.Object);
builder.HostModule<InterfaceModule>();

var host = builder.Build();
await host.StartAsync();

serviceMock.Verify(x => x.CallMe());
host.Dispose();
}

[Fact]
public void SimpleInjector_hook_interfaces_are_invoked()
{
SimpleInjectorHooksModule.Reset();

var builder = ModulesHost.CreateDefaultBuilder();
builder.UseSimpleInjector(new Container());
builder.HostModule<SimpleInjectorHooksModule>();

builder.Build().Dispose();

Assert.True(SimpleInjectorHooksModule.AddCalled, "IAddSimpleInjectorModule.AddSimpleInjector was not invoked");
Assert.True(SimpleInjectorHooksModule.UseCalled, "IUseSimpleInjectorModule.UseSimpleInjector was not invoked");
Assert.True(SimpleInjectorHooksModule.ContainerConfigured, "IContainerConfiguringModule.ConfigureContainer was not invoked");
}

[Fact]
public async Task Interface_based_web_module_Configure_is_invoked()
{
InterfaceWebModule.Configured = false;

var builder = ModulesHost.CreateDefaultBuilder();
builder.UseSimpleInjector(new Container());
builder.UseAspNetCoreWithDefaults((_, webHostBuilder) =>
webHostBuilder.UseUrls("http://127.0.0.1:0")); // ephemeral port, no collision
builder.HostModule<InterfaceWebModule>();

var host = builder.Build();
await host.StartAsync(); // Configure runs when the module's request pipeline is built
await host.StopAsync();
host.Dispose();

Assert.True(InterfaceWebModule.Configured);
}

public interface IService
{
void CallMe();
}

public class HostedServiceHandler : IHostedServiceHandler
{
private readonly IService _service;

public HostedServiceHandler(IService service)
{
_service = service;
}

public Task Execute(CancellationToken stoppingToken)
{
_service.CallMe();
return Task.CompletedTask;
}
}

// Explicit implementations -> invisible to Type.GetMethod, so only interface dispatch works.
public class InterfaceModule : IServiceConfiguringModule, IContainerConfiguringModule
{
void IServiceConfiguringModule.ConfigureServices(IServiceProvider serviceProvider, IServiceCollection services)
{
services.AddHostedHandler<HostedServiceHandler>();
}

void IContainerConfiguringModule.ConfigureContainer(IServiceProvider serviceProvider, Container container)
{
var service = serviceProvider.GetRequiredService<IService>();
container.RegisterInstance(service);
container.Register<HostedServiceHandler>();
}
}

public class SimpleInjectorHooksModule : IContainerConfiguringModule, IUseSimpleInjectorModule, IAddSimpleInjectorModule
{
public static bool ContainerConfigured;
public static bool UseCalled;
public static bool AddCalled;

public static void Reset() => ContainerConfigured = UseCalled = AddCalled = false;

void IContainerConfiguringModule.ConfigureContainer(IServiceProvider serviceProvider, Container container)
=> ContainerConfigured = true;

void IUseSimpleInjectorModule.UseSimpleInjector(SimpleInjectorUseOptions options)
=> UseCalled = true;

void IAddSimpleInjectorModule.AddSimpleInjector(SimpleInjectorAddOptions options)
=> AddCalled = true;
}

public class InterfaceWebModule : WebModule, IApplicationConfiguringModule
{
public static bool Configured;

public override string Path { get; } = "";

void IApplicationConfiguringModule.Configure(IServiceProvider serviceProvider, IApplicationBuilder app)
{
Configured = true;
}
}
}
}