Finbuckle / Finbuckle.MultiTenant

Finbuckle.MultiTenant is an open-source multitenancy middleware library for .NET. It enables tenant resolution, per-tenant app behavior, and per-tenant data isolation.
https://www.finbuckle.com/multitenant
Apache License 2.0
1.33k stars 265 forks source link

Finbuckle implementation on Worker Service (.Net Core 3.1) #288

Closed hbermani closed 4 years ago

hbermani commented 4 years ago

Can I just start by saying I absolutely love this library, I use it for one of my APIs and it works a treat.

I am now tying to configure a worker service as it's own project and I want it to connect to different SQL Server databases using Finbuckle and EF Core. I have configured everything to the best of what I can, but I note that as I don't have a Startup file and there is no middleware as I am not dealing with a request pipeline. Where do I need to add app.UseMultiTenant()? Alternatively is there a way I can pass a TenantInfo I obtained from a Configuration Store / Worker activity Directly to a dbcontext as part of a worker task?

Here is an extract from Program.cs code for the worker service

public static IHostBuilder CreateHostBuilder(string[] args) =>
                Host.CreateDefaultBuilder(args)
                .ConfigureLogging((context, logging) => {
                        logging.AddSerilog();
                    })
                .ConfigureAppConfiguration((hostContext, config) =>
                {
                   code to obtain configurations
                })
                .ConfigureServices((hostContext, services) =>
                {
                    services.AddMultiTenant()
                            .WithStaticStrategy("dev")
                            .WithConfigurationStore(hostContext.Configuration, "FinbuckleSection");
                    services.AddDbContext<AppContext>();
                    services.AddHostedService<Worker>();
                });

This is currently throwing a null reference exception for Connection String in the EF Core context which is inheriting from MultiTenantDbContext , and overriding OnConfiguring. This makes sense to me as at no point have I been able to pass the TenantInfo for a Worker Service task.

Thanks

H.

AndrewTriesToCode commented 4 years ago

hi @hbermani Thanks, I'm glad to hear you are finding the library helpful. Spread the word :)

It is possible to use outside of ASP.NET Core... I need to update the docs for this and provide a sample.

First, I recommend using the preview nuget release for version 6. It requires a few changes (defining your TenatnInfo class) but it specifically improves this scenario. Check the sticky thread for details on the preview release.

You'll need to create a service scope in your service as described here.

With that you can "simulate" the middleware like this:

var sp; // service provider obtained as shown in link above

// here TTenatInfo is the type of your custom tenant info object
var resolver = sp.GetRequiredService<ITenantResolver<TTenantInfo>>();

// here context is whatever your strategy needs passed to it
// in ASP.Net Core it's usually HttpContext, but it can be anything
// you can then use this multitenant context as needed in your service
var multiTenantContext = await resolver.ResolveAsync(context);

// optionally, if you need to use IMultiTenantContextAccessor in any of your code it needs to now be set
var accessor = sp.GetRequiredService<IMultiTenantContextAccessor<TTenantInfo>>();
accessor.MultiTenantContext = multiTenantContext;

Be advised that this will likely change slightly for the final release of version 6 because I am trying to remove the need to have all the generic types specified since TTenantInfo isn't actually used here.

hbermani commented 4 years ago

Oooo exciting.... I'll crack on and try and implement it, will let you know how it goes. :-p

Ps. Thanks for the super-fast response!

hbermani commented 4 years ago

Hi Andrew, a bit stuck here and would welcome your help.

I am getting this exception when the application attempts to execute Program.cs , CreateHostBuilder(args).Build().Run()... "No constructor for type 'WorkflowService.EFContext.WorkflowContext' can be instantiated using services from the service container and default values."

I think what is happening here is it's failing at AddDbContext, probably to do with how I configured it in Program.cs. I have included all relevant extracts but I believe the issue is in instanting the Dbcontext.

Here is my configure services

  .ConfigureServices((hostContext, services) =>
                {
                    services.Configure<Settings>(hostContext.Configuration.GetSection("Workflow"));
                    services.AddMultiTenant<CustomTenantInfo>()
                                .WithStrategy<CustomMultiTenancyStrategy>(ServiceLifetime.Scoped, new object())
                                .WithConfigurationStore(hostContext.Configuration, "Workflow:Finbuckle");
                    services.AddScoped<ITenantResolver<CustomTenantInfo>, ScopedMultiTenancyResolver>();
                    services.AddDbContext<WorkflowContext>();
                    services.AddHostedService<Worker>();
                });

Here is my WorkflowContext

 public class WorkflowContext : MultiTenantDbContext
    {
        public WorkflowContext(CustomTenantInfo tenantInfo) : base(tenantInfo)
        {

        }

        public WorkflowContext(CustomTenantInfo tenantInfo, DbContextOptions<WorkflowContext> options) :
        base(tenantInfo, options)
        {

        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        {
            optionsBuilder.UseSqlServer(TenantInfo.ConnectionString, options => options.EnableRetryOnFailure(
                    maxRetryCount: 10,
                    maxRetryDelay: TimeSpan.FromSeconds(30),
                    errorNumbersToAdd: null
                ));

            base.OnConfiguring(optionsBuilder);
        }
...then Dbset statements...

I also have a WorkflowContextFactory but won't be in use..

    public class WorkflowContextFactory : IDesignTimeDbContextFactory<WorkflowContext>
        {

        WorkflowContext IDesignTimeDbContextFactory<WorkflowContext>.CreateDbContext(string[] args) 
        {
            var optionsBuilder = new DbContextOptionsBuilder<WorkflowContext>();
            var connectionString = "#####";
            optionsBuilder.UseSqlServer(connectionString, options => options.EnableRetryOnFailure());

            var dummyTenant = new CustomTenantInfo()
            {
                Id = "#####",
                Identifier = "#####",
                Name = "#####",
                ConnectionString = connectionString
            };

            return new WorkflowContext(dummyTenant, optionsBuilder.Options);
        }

Here is CustomTenantInfo.cs

  public class CustomTenantInfo : ITenantInfo
    {

        public string Id { get; set; }
        public string Identifier { get; set; }
        public string Name { get; set; }
        public string ConnectionString { get; set; }
    }

Here is CustomMultiTenentContext

    class CustomMultiTenantContext : IMultiTenantContext<CustomTenantInfo>
    {
        public CustomTenantInfo TenantInfo { get; set; }
        public StrategyInfo StrategyInfo { get; set; }
        public StoreInfo<CustomTenantInfo> StoreInfo { get; set; }
    }

Here is the strategy

 public class CustomMultiTenancyStrategy : IMultiTenantStrategy
    {
        private readonly Settings _settings;
        public CustomMultiTenancyStrategy(object con, IOptionsSnapshot<Settings> settings)
        {
            _settings = settings.Value;
        }

        public Task<string> GetIdentifierAsync(object ti)
        {
            var t = (TenantInfo)ti;
            return Task.FromResult(t.identifier);

        }
    }

And finally the ScopedMultiTenancyResolver

   class ScopedMultiTenancyResolver : ITenantResolver<CustomTenantInfo>
    {
        public IEnumerable<IMultiTenantStrategy> Strategies => throw new NotImplementedException();

        public IEnumerable<IMultiTenantStore<CustomTenantInfo>> Stores => throw new NotImplementedException();

        public Task<IMultiTenantContext<CustomTenantInfo>> ResolveAsync(object tenantInfo)
        {

            IMultiTenantContext<CustomTenantInfo> context = new CustomMultiTenantContext();
            context.TenantInfo = (CustomTenantInfo)tenantInfo;
            return Task.FromResult(context);
        }
    }
hbermani commented 4 years ago

@AndrewTriesToCode I replaced services.AddDbContext() with services.AddScoped(s => new WorkflowContext(new CustomTenantInfo())); and now it's starting up and progressing to the worker service.

Now I note that the ScopedMultiTenancyResolver is not being passed in DI Engine.

AndrewTriesToCode commented 4 years ago

Hi, I see you got the DB context working -- the issue there was that DI can only pass ITenantInfo to the db context ctor (at this time). So simply changing the constructor to take that should fix it that issue, Your workaround in the above post will not send the tenant info that was resolved with the library.

A few notes:

You shouldn't (normally) need to define your own IMultiTenatContext or ITenantResolver -- the default ones should work. The interfaces are there to allow easier test mocking and stuff. These defaults are registered into DI in the AddMultiTenant method.

I'm also not sure what your strategy is intending to do there--just parrot back the tenant info passed into it? Just to get things working initally I'd recommend using WithStaticStrategy with a string for the identifier for one of your tenants in your configuration.

If you have a link to a repo I'd be happy to take a closer look.

hbermani commented 4 years ago

Hi Andrew, turned out I complicated this for myself and a good friend managed to have a look and give me a helping hand.

I just needed to pass a TTenantInfo instance directly to the dbcontext constructor and execute it from there.

We(my friend) also managed to get it working with dependency injection... I have provided below few code snippets should someone come across this in the future.

-ConfigureServices in Program.cs

.ConfigureServices((hostContext, services) =>
                {
                    services.Configure<Settings>(hostContext.Configuration.GetSection("Workflow"));
                    services.AddMultiTenant<CustomTenantInfo>()
                                   .WithStrategy<CustomMultiTenancyStrategy>(ServiceLifetime.Scoped, new object())
                                  .WithConfigurationStore(hostContext.Configuration, "Workflow:Finbuckle");
                    //services.AddScoped<WorkflowContext>(s => new WorkflowContext(s.GetService<CustomTenantInfo>()));
                    services.AddDbContext<WorkflowContext>();
                    services.AddHostedService<Worker>();
                    services.AddScoped<ScopedMultiTenancyResolver>();
                });

-Resolver code in worker service

   using (var scope = Services.CreateScope())
            {
                var resolver =
                       scope.ServiceProvider
                       .GetRequiredService<ScopedMultiTenancyResolver>();
                var multiTenantContext = await resolver.ResolveAsync(tenantInfo);
                var accessor = scope.ServiceProvider.GetRequiredService<IMultiTenantContextAccessor<CustomTenantInfo>>();
                accessor.MultiTenantContext = multiTenantContext;
                var _context = scope.ServiceProvider.GetRequiredService<WorkflowContext>();

               // using (var _context = new WorkflowContext(tenantInfo))
               //{
               //     var result = await _context......ToListAsync();
               //    return result;
               //}

            }
hbermani commented 4 years ago

Thank you for all the help, I'll close this now. Whoever is reading this, please get in touch if you face this obstacle with your project and need help.
H.

hhuseyinpay commented 4 years ago

Hey @AndrewTriesToCode , I'm using the repository pattern and I want to iterate all tenants in worker service. Is it possible? (The project is multitenant but some tasks are common so I want to make them worker service.) Screen Shot 2020-09-19 at 11 05 37 Screen Shot 2020-09-19 at 11 04 10

AndrewTriesToCode commented 4 years ago

hi @hhuseyinpay

It is possible, but not all of the tenant stores implement IMultiTenantStore.GetAllAsync. What type of store are you using or did you create a custom one?

hhuseyinpay commented 4 years ago

I'm using Configuration Store for the development stage. Tenant Infos are not the problem. The problem is how could I get tenants repository from DI. One solution is:

Screen Shot 2020-09-19 at 21 09 38

But I think there should be a nicer solution for this :)

hhuseyinpay commented 4 years ago

My final solution is: Screen Shot 2020-09-20 at 19 22 49

It seems working for now but I don't know what happens for the long-running. Any suggestion?

AndrewTriesToCode commented 4 years ago

hi @hhuseyinpay

I think this will work. I think it will run nonstop though -- you might want to put it on a timer as described here: https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services?view=aspnetcore-3.1&tabs=visual-studio#timed-background-tasks

Also you might want to include the look that sets up the tenantRepo list inside the timed while look so it picks up any updates to the tenant in the tenant store.

I'm not sure exactly if the lessions and servers items work as you intend since that's specific to your app, but overall look good!