JasperFx / lamar

Fast Inversion of Control Tool and Successor to StructureMap
https://jasperfx.github.io/lamar
MIT License
572 stars 119 forks source link

Lamar & Alba ServiceRegistry not working in Release mode #98

Closed Pondidum closed 6 years ago

Pondidum commented 6 years ago

For the record, I am not certain if this is a bug in Alba or Lamar.

The problem occurs when an object is registered outside the Startup class in Alba, using .ConfigureServices, and a ServiceRegistrywithin the application makes use of that registered type to register another type. This works fine when run in Debug mode, but under Release, the second type registration is not found. Probably better explained with a test...

Package versions used:

<PackageReference Include="Alba.AspNetCore2" Version="1.4.3" />
<PackageReference Include="Lamar" Version="1.1.2" />
<PackageReference Include="Lamar.Microsoft.DependencyInjection" Version="1.1.2" />
<PackageReference Include="Microsoft.AspNetCore.All" Version="2.1.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
<PackageReference Include="xunit" Version="2.3.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />

I can upload the test project if that would help.

[Fact]
public Task<IScenarioResult> Test1()
{
    var storage = new InMemoryStorage();
    var system = SystemUnderTest.ForStartup<Startup>();

    system.Configure(builder => builder.UseLamar());
    system.ConfigureServices(services =>
    {
        services.AddSingleton<IStorage>(storage);
    });

    return system.Scenario(_ =>
    {
        _.Get.Url("/Toggles");
        _.StatusCodeShouldBe(HttpStatusCode.OK);
        _.ContentShouldBe("{\"message\":\"LamarBug.InMemorySession\"}");
    });
}

public interface IStorage
{
    IStorageSession CreateSession();
}

public interface IStorageSession
{
}

public class InMemoryStorage : IStorage
{
    public IStorageSession CreateSession() => new InMemorySession();
}

public class InMemorySession : IStorageSession
{
}

public class Startup
{
    public void ConfigureContainer(ServiceRegistry services)
    {
        services.AddMvc();

        services.Scan(_ =>
        {
            _.TheCallingAssembly();
            _.WithDefaultConventions();
            _.LookForRegistries();
        });

        //services.For<IStorageSession>().Use(c => c.GetInstance<IStorage>().CreateSession()).Scoped();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.UseMvc();
    }
}

[Route("Toggles")]
public class TogglesController : Controller
{
    private readonly IStorageSession _session;

    public TogglesController(IStorageSession session)
    {
        _session = session;
    }

    [Route("")]
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        return new JsonResult(new { Message = _session.GetType().ToString() });
    }
}

public class RestRegistry : ServiceRegistry
{
    public RestRegistry()
    {
        Scan(a =>
        {
            a.TheCallingAssembly();
            a.WithDefaultConventions();
        });

        For<IStorageSession>().Use(c => c.GetInstance<IStorage>().CreateSession()).Scoped();
    }
}
CodingGorilla commented 6 years ago

When you say it fails in Release mode, what is the actual failure?

Pondidum commented 6 years ago

🤦‍♂️ I really should have included that!

System.InvalidOperationException : Unable to resolve service for type 'LamarBug.IStorageSession' while attempting to activate 'LamarBug.TogglesController'.
   at Microsoft.Extensions.DependencyInjection.ActivatorUtilities.GetService(IServiceProvider sp, Type type, Type requiredBy, Boolean isDefaultParameterRequired)
   at lambda_method(Closure , IServiceProvider , Object[] )
   at Microsoft.AspNetCore.Mvc.Controllers.ControllerActivatorProvider.<>c__DisplayClass4_0.<CreateActivator>b__0(ControllerContext controllerContext)
   at Microsoft.AspNetCore.Mvc.Controllers.ControllerFactoryProvider.<>c__DisplayClass5_0.<CreateControllerFactory>g__CreateController|0(ControllerContext controllerContext)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.InvokeInnerFilterAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
   at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
   at Alba.SystemUnderTestExtensions.Scenario(ISystemUnderTest system, Action`1 configure)
   at Alba.SystemUnderTestExtensions.Scenario(ISystemUnderTest system, Action`1 configure)
--- End of stack trace from previous location where exception was thrown ---
CodingGorilla commented 6 years ago

My initial guess is that it has to do with the scan of TheCallingAssembly. I would bet that the calling assembly is actually Alba (or maybe XUnit) and not the assembly that contains the actual service registry, so the service registry never actually gets picked up. The thing that makes me scratch my head is: Why does it work in debug mode?

The services.For<>.Use() syntax will always work because you're directly adding the registration, so that makes sense.

Pondidum commented 6 years ago

You might be onto something there.

If I change the Startup to explicitly use the RestRegistry, everything works too:

public void ConfigureContainer(ServiceRegistry services)
{
    services.AddMvc();

    services.Scan(_ =>
    {
        _.TheCallingAssembly();
        _.WithDefaultConventions();
        //_.LookForRegistries();
    });

    services.IncludeRegistry<RestRegistry>();
}

What I want to do is see the output of .WhatDidIScan(), but I can't figure out how to get at the container instance in the test 😞

CodingGorilla commented 6 years ago

You can do it in the Startup.Configure() method by casting the app.ApplicationServices to a Container. This is what I do in my start up class(es):

if(IsDebugMode)
{
    var container = (IContainer)app.ApplicationServices;
    Log.Logger.Verbose(container.WhatDidIScan());
    Log.Logger.Verbose(container.WhatDoIHave());
}
Pondidum commented 6 years ago

Well that is a helpful snippet even if we don't figure this out! Anyway the output of WhatDidIScan() shows the difference between Debug and Release:

Debug:

2018-10-22 21:17:54 [Verbose] All Scanners
================================================================

Assemblies
----------
* LamarBug, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Conventions
--------
* Default I[Name]/[Name] registration convention
* Lamar.Scanning.Conventions.FindRegistriesScanner

Assemblies
----------
* LamarBug, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null

Conventions
--------
* Default I[Name]/[Name] registration convention

No problems were encountered in exporting types from Assemblies

And Release

2018-10-22 21:17:02 [Verbose] All Scanners
================================================================

Assemblies
----------
* Microsoft.AspNetCore.Hosting, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60

Conventions
--------
* Default I[Name]/[Name] registration convention
* Lamar.Scanning.Conventions.FindRegistriesScanner

No problems were encountered in exporting types from Assemblies

The remaining question I guess is...why the initial scan only finds the Microsoft.AspNetCore.Hosting in Release, but my actual assembly in Debug?

CodingGorilla commented 6 years ago

For the record, I am not certain if this is a bug in Alba or Lamar.

Given all the information we have, I would venture to guess this has something to do with the way Alba is running the tests in the different configurations. As I said, the _.TheCallingAssembly() is only going to check the assembly which is actually executing the code.

So you can work around this, for both modes by adding into your scan: _..AssemblyContainingType<Startup>();. This will force it to scan the assembly which contains your StartupClass. Alternatively, if your registries are in a different assembly, then you can simply use one of your registry types as the generic argument.

There are a bunch of other methods on the scanner that you can use to add different assemblies to the scanning if neither of those is perfectly suitable, but the bottom line is that you have to make sure you are scanning all the right assemblies.

Pondidum commented 6 years ago

Yes, I am aware of how the scanning works, so have updated my test to get around this. I've "moved" this issue to the Alba repo as that seems to be the more likely cuplrit, so will shut this issue.

Thanks for your help in diagnosing!