Closed hbermani closed 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.
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!
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);
}
}
@AndrewTriesToCode
I replaced services.AddDbContext
Now I note that the ScopedMultiTenancyResolver is not being passed in DI Engine.
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.
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;
//}
}
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.
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.)
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?
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:
But I think there should be a nicer solution for this :)
My final solution is:
It seems working for now but I don't know what happens for the long-running. Any suggestion?
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!
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
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.