dotnet / orleans

Cloud Native application framework for .NET
https://docs.microsoft.com/dotnet/orleans
MIT License
10.07k stars 2.03k forks source link

Dependency Injection configuration runs in Host AppDomain instead of Silo AppDomain #1577

Closed Maarten88 closed 7 years ago

Maarten88 commented 8 years ago

In a Grain class I want to use the new DI, and inject a class into the constructor, so I created a Startup class where I configure the services, and use these classes in the constructor. This Startup class lives in the Grain assembly, it references Grain classes. In my test project, that is configured like the test project in Orleans itself, and the tests work fine.

config.Defaults.StartupTypeName = "Grains.Startup,Grains";

However, when running the same grains in Dev inside our OrleansHost, which also self-hosts the client website using OWIN, the Startup class is not found and a startup error occurs. In this project the Grains assemblies are copied to the Application folder; they are loaded in a separate AppDomain, there is no direct reference from the host to the grains.

It seems that the host is looking in the main appdomain, but the startup class is in another appdomain. If I directly reference the Grain project from the host, the Startup class is located - but I get assembly conflicts with other parts of the project causing many other errors.

I think locating the Startup class happens too early (in the Silo constructor); maybe it should happen later (in DoStart?)

attilah commented 7 years ago

I think that this scenario is not supported by the current implementation and since we're moving away from AppDomains to be able to support .Net Core too in future versions it is not something we're currently pursuing.

The assembly conflicts you have can it be fixed with assembly redirects? That is a very common scenario with our customers, since Orleans is depending on the lowest possible versions of the external dependencies and our customers are in many cases using newer libraries because of the requirements.

Is there a particular reason - beside the conflicts you see - why you're loading Grain assemblies to a different AppDomain?

It means that from your grains you don't plan to use any Orleans service like logging, etc?

Maarten88 commented 7 years ago

I tried doing that because I wanted to run a web/orleans server on a single Azure Web Role for testing.

It didn't work, but I found another way using Yams on a Worker Role, I can even run multiple websites and the Orleans host on a single instance now.

I resolved those loading conflicts, moved to dotnetcore on full framework 4.6.1 and project.json / xproj style projects that make it somewhat easier to figure out the dependencies. I'm loading everything in one appdomain now so DI is working as expected (however it does not use DI abstractions, so can't use other DI frameworks like Autofac as I found recently while trying to run a Bot App inside Orleans)

Anyway, this issue has been there a long time, nobody has ever responded or confirmed the issue so I guess nobody cares about loading grains from the Applications folder combined with the newer DI. Maybe it's better to remove scanning the Application folder completely, as it is not working correctly?

sergeybykov commented 7 years ago

Maybe it's better to remove scanning the Application folder completely, as it is not working correctly?

We are moving in that direction with the AssemblyLoader being redone for .NET Core.

attilah commented 7 years ago

so can't use other DI frameworks like Autofac as I found recently while trying to run a Bot App inside Orleans)

What is the problem with plugging in Autofac? It is a supported scenario when you're building your own container in the startup type ConfigureServices method.

Maarten88 commented 7 years ago

It is a supported scenario when you're building your own container in the startup type ConfigureServices method.

I tried exactly that but it seemed to conflict with other DI stuff inside Orleans. I saw there were direct dependencies on Microsoft.Extensions.DependencyInjection which seemed to clash with Autofac.Extensions.DependencyInjection. I think for other frameworks you'd only reference Microsoft.Extensions.DependencyInjection.Abstractions. I did not dive in too deeply and I'm not a DI fan, probably I did it wrong.

jason-bragg commented 7 years ago

The DI system is still new so it's quite possible we've used some hard bindings that we shouldn't have. I would very much appreciate if you recorded the errors your seeing when trying to use Autofac in a compatibility issue. I'm not convinced compatibility with Autofac is necessary to resolve this issue, but it sounds like an important problem to track.

jdom commented 7 years ago

Agree, using Microsoft.Extensions.DependencyInjection should not be a blocker, since we (in theory) allow using other containers like Autofac, but still by default use the default service provider. I believe we tried to be diligent to expose types available in the Abstractions library, so you are not locked in. In practice we might have broken this, so if you can give us more details, we could try to fix it. I think we should also have some test coverage of using some other container, so that this is no longer theory and we avoid regressions.

attilah commented 7 years ago

We're referencing Microsoft.Extensions.DependencyInjection because we need a default implementation of IServiceCollection to fulfill the needs of our DI. It is possible that in the future we'll change to Autofac as default container instead of the above, but now it is not the case. However we still do support Autofac, nothing is broken, here is how to do it:

Add reference to Autofac and Autofac.Extensions.DependencyInjection packages.

Lets assume that you've an IMyService and MyService pair to inject in the ctor of the grains.

With this Startup class, it just works:

public class MyStartup
{
    public IServiceProvider ConfigureServices(IServiceCollection services)
    {
        // Create the container builder.
        var builder = new ContainerBuilder();

        // Register dependencies, populate the services from
        // the collection, and build the container. If you want
        // to dispose of the container at the end of the app,
        // be sure to keep a reference to it as a property or field.
        builder.Populate(services);

        // Register your own services.
        builder.RegisterType<MyService>().As<IMyService>();

        var container = builder.Build();

        // Create the IServiceProvider based on the container.
        return new AutofacServiceProvider(container);
    }
}

The configuration example was taken from the Autofac ASP.NET Core documentation page.

Please try it out and let me know if it does not work.

attilah commented 7 years ago

@Maarten88 did you have a chance to try out my step-by-step solution?

Maarten88 commented 7 years ago

I haven't tried yet - I'll get to it tomorrow. I'm not sure why this failed before as this is close to what I tried before. But please do not make Autofac the default in Orleans! Keep it simple. The only reason I wanted Autofac was because that is heavily (over)used in Microsoft BotBuilder which I am running in Orleans.

Maarten88 commented 7 years ago

Ok, I think this works indeed. I think the reason this did not work for me before was that the nuget I depended on referenced an older Autofac version, 3.5.2. I upgraded everything to the latest Autofac (4.2.0), added Autofac.Extensions.DependencyInjection 4.0.0 and followed your recipe, and everything seems to work correctly.