Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Asp.NET 5 - Provide a multi-tenant story #973

Closed
Alxandr opened this issue Oct 6, 2015 · 27 comments
Closed

Asp.NET 5 - Provide a multi-tenant story #973

Alxandr opened this issue Oct 6, 2015 · 27 comments

Comments

@Alxandr
Copy link
Contributor

Alxandr commented Oct 6, 2015

I'm trying to create a multi-tenant project using DNX, and there are a few pain points (to say the least). I'd like a discussion on the ability to do multi-tenant applications using Asp.NET 5, and some of the problems/things that works. I'll also compile a list of issues regarding this across the other aspnet repos, and if I miss someone (which I probably will), please let me know in the comments.


Open issues regarding multi-tenant projects in DNX:

Note: I'm not certain that all of these issues are the right way to go about things, but they all deal with multi-tenancy, so I'm listing them here. I don't even agree with some of what's written in some of them, but I'm trying to be unbiased and objective in this information, and as such, this is simply a list of open issues regarding multi-tenancy that @PinpointTownes managed to dig up for me (cause I'm lazy, and he's been looking into this for a lot longer).


Simple use-cases:

A simple web-store, where each "tenant" is a store. A few things are required to make this work (parenthesizes at the end of the points are repos this should concern):

  1. Users (both admin and customers) accounts needs to be per tenant. Just because a user has registered for musicstore.myapp.com doesn't mean they are registered for moviestore.myapp.com. And even more importantly, admins can't log in on other stores. (Security/Identity)
  2. Parts of the applications needs to display different values. For insistence, the store name needs to be rendered differently. In some cases, this can be simply injected string values (like store name). In other cases, it needs to be full blown Views (ie, different tenants have different views). (MVC?)
  3. Some routes are only enabled for some tenants. For instance, while the music store have support for listing music by tags, the movie store does not have tags at all. (Routing, MVC?)
  4. Different database/repository used per tenant. (EntityFramework)

Some problems that I or others have solved (at least partially):

Tenant middleware. The sample code here uses the library https://github.com/YoloDev/YoloDev.MultiTenant.

app.UseMultiTenant(); // Pupulates the `Tenant` property on the `ITenantService`.
app.UseRequireTenant(); // 404s if a tenant is not set.

There is other things I can do with this, like make database contexts from EF7 use different databases depending on which tenant I'm currently using:

// NOTE: this should probably be grouped into a single call
services.AddScoped(s =>
{
    var tenantService = s.GetRequiredService<ITenantService<Tenant>>();
    var tenant = tenantService.Tenant;
    if (tenant == null)
        return null;

    var optionsBuilder = new DbContextOptionsBuilder<LoginContext>();
    optionsBuilder.UseSqlServer(tenant.DbConnectionString);
    return optionsBuilder.Options;
});
services.AddScoped<DbContextOptions>(s => {
    return s.GetRequiredService<DbContextOptions<LoginContext>>();
});
services.AddScoped(typeof(LoginContext), DbContextActivator.CreateInstance<LoginContext>);

Using this, if I inject LoginContext into my app, it depends on what tenant I'm currently using.


Summary

None of the lists above are complete. This is not the whole story, just a few things I jolted down in a hurry. Nor am I an expert on any of this. The only multi-tenant application I've ever dealt with was written in classic ASP, and used the feature in IIS where you can map virtual folders to achieve it's multi-tenancy. And it stored all the connection strings in session storage.... But yeah, if I've missed anything (which I definitely have), please leave a comment, and I will amend it. And if this is important to you, please say so. Maybe we can bump multi-tenancy on the priority list of the Asp.NET team :).

@davidfowl
Copy link
Member

We absolutely want to do this but it will not happen for v1

@Alxandr
Copy link
Contributor Author

Alxandr commented Oct 6, 2015

@davidfowl Yeah, I figured as much. But I still figured it'd be good to have a place to (try to) collect issues from the different repos, and have discussions.

@rynowak
Copy link
Member

rynowak commented Oct 6, 2015

I think to sum up some of our internal discussions about this, @Alxandr you are absolutely right that doing a small set of work items piecemeal is not a good solution for this. We're definitely thinking about this as a large surface area and not as a few small fixes.

@Eilon
Copy link
Member

Eilon commented Oct 7, 2015

@sebastienros @lodejard for additional FYI information.

@tugberkugurlu
Copy link
Contributor

I am sure @benfoster will be interested in this discussion (refer to https://github.com/saaskit/saaskit).

@streetwiseherc
Copy link

@Alxandr "Component Developer" (or at least that's what we called them in the VB Classic days) Syncfusion puts out some really valuable, but succinct ebooks on various subjects. I've read quite a few and enjoyed them. Best of all they are free (with name/email/company registration)! I mention these books because recently they've come out with a Multi-tenant book for ASP.NET (MVC 5 and Web Forms) that covers many of the issues that you have brought up. Check it out (and some other great books) over at: https://www.syncfusion.com/resources/techportal/ebooks

The link to the actual ebook is -- ASP.NET Multitenant Applications Succinctly by Ricardo Peres: https://www.syncfusion.com/resources/techportal/details/ebooks/aspnetmultitenant

@sebastienros
Copy link
Member

We have multi-tenancy working in Orchard vNext (https://github.com/OrchardCMS/Brochard), with dynamic module loading, meaning each tenant gets a different set of services. It's in its early stages but it's doable. So even if @davidfowl says it's not a priority for the team and I agree with that, this is mine ;) So I am definitely interested in finding out solutions to all of the related issues.

@ses4j
Copy link

ses4j commented Nov 12, 2015

@sebastienros I am browsing through Brochard, looks very interesting. Can you possibly point out where the dynamic module loading code is and/or a little about how it works?

@sebastienros
Copy link
Member

Overall steps in Orchard 2 (as in Orchard 1)

1- Modules are set in well-known folders as .NET projects, with a manifest file to mark these as modules
2- Each tenant is represented in some state, with a list of allowed modules. All classes from these modules for each tenant are analyzed and listed as candidate services
3- A new DI container is created for each of them, with common Application Services added. Services are registered for each corresponding container, then referenced.
4- Routes are published, taking the tenant strategy into account.
5- On each request, a tenant resolution is applied and returns which tenant should serve it. Then the corresponding DI container and a scope are set in place of the default ones for the rest of the request.

@joeaudette
Copy link

I have implemented multi tenancy in this new project which aims to be a multi tenant web app foundation: https://github.com/joeaudette/cloudscribe

It is working now if anyone is interested in trying it out. It uses a single database and a single set of tables for all tenants with data identified by a SiteId. I know there are different conceptions of multi-tenancy so some may not like this solution. For my needs any multi tenant installations will be per customer, so all tenants in a given installation belong to 1 customer. I prefer not to mingle different customer data together so I use an installation per customer rather than tenant per customer. My implementation supports tenants defined either by host name or by the first folder segment of the url as mutually exclusive config options, ie you can use one approach or the other in an installation but not both at once. I also have an option to allow sharing the same users and roles across tenants or separate users and roles per tenant. The project currently has a working UI for managing sites, users, and roles.

For the cookie middleware and social auth middleware the problem was that options are defined once at startup and then kept as a singleton for later use. My solution was to modify the middleware to use an TenantOptionsResolver class so that the options can be resolved per request. Basically the resolver resolves the current tenant from the request and takes the original options and modifies them if needed for the tenant. So I copied and modified the open source implementations from Microsoft and will have to keep track of changes in the originals to keep my modified versions up to date, but I don't think this is a big problem, it is not a lot of code and I'm trying to keep minimal differences vs the reference implementations. Currently I'm keeping the social auth clientid and secrets in the db, they can be set from the UI. I'm thinking I need a way to encrypt and decrypt that data in the db instead of storing it in clear text but have not implemented a solution for that yet.

I specifically wanted to avoid doing different dependency registration per tenant in startup because I want to make sure there is no need to iterate through a list of tenants at startup time for performance. So I am not doing anything to differentiate DI resolution per tenant and not doing anything special for routing other than using a route contraint if folder tenants are enabled:

routes.MapRoute(
            name: "folderdefault",
            template: "{sitefolder}/{controller}/{action}/{id?}",
            defaults: new { controller = "Home", action = "Index" },
            constraints: new { name = new SiteFolderRouteConstraint() }
            );

I'd welcome feedback on this project.

@benfoster
Copy link

I recently released SaasKit for ASP.NET Core which enables multi-tenancy using lightweight middleware. It makes the current tenant instance injectable so most parts of ASP.NET Core can be partitioned per tenant.

One area it seems that is not very multi-tenant-friendly is the authentication middleware. I've put my thoughts on this related issue. I can't claim to know the security libs that well but if we were able to make AuthenticationOptions injectable and resolved per request, it would solve most of the problems for us.

@Eneuman
Copy link

Eneuman commented Mar 4, 2016

Take a look at @Identity Server. It solves many of the problems with muli-tenancy authentication/authorization and has the latest security protocols in place. It's a very sofisticated piece of software. They just released a beta for ASP.Net Core.
https://github.com/IdentityServer/IdentityServer4

@sebastienros
Copy link
Member

@Alxandr do you think the recent update that @benfoster did on SaasKit would solve all of these. @joeaudette was also able to implement multi-tenant security pipeline too with these changes. And Ben also created some samples explaining how to build what you are trying to achieve.

@alexsandro-xpt
Copy link

So, what we have about it today? Some news feature for muli-tenancy?

@Mystere
Copy link

Mystere commented May 29, 2017

Apparently, it's not going to happen for v2 either, probably not for 2.x as well, v3? v4? v10? Who knows?

The longer the ASP.NET team waits to start this, the bigger the job becomes, IMO. And the more unlikely it is to happen.

@Eilon
Copy link
Member

Eilon commented Aug 1, 2017

Hi folks, MVC does not have support for this today and we have no immediate plans to add these kinds of features. I suggest that you take a look at the Orchard Core Framework, a lightweight module system which builds on top of MVC.

You can find a basic sample at https://github.com/OrchardCMS/OrchardCore.Samples

@alexsandro-xpt
Copy link

@Eilon Realy nice demo!!! I will test it.. Thank you!

@valeriob
Copy link

valeriob commented Aug 25, 2017

Hi,
i have a multi tenant application in owin/katana. In the pipeline i have a step after the container scope has been instantiated where i can inject an additional service based on the url (or whatever on the request), the tenants are not known at compile time ofc.
This approach works very well, but i'm not able to replicate it in AspNetCore.
I tried a pipeline step where i replaced the container like this (i'm using autofac) :

        Task RegisterTenantDatabase(HttpContext context, Func<Task> next)
        {
            var tenantId = context.GetTenantId();
            var scope = (ILifetimeScope)context.RequestServices.GetService(typeof(ILifetimeScope));
            using (var requestScope = scope.BeginLifetimeScope())
            {
                // add my unit of work to the request Scope
                context.RequestServices = new AutofacServiceProvider(requestScope);
            }
            return next.Invoke();
        }

but without luck, if i look at components inside, i guess it misses the whole pipeline because i land in a white page 😄

Ho do you do that in aspnetcore ? Why the sample above does not work ?

Anyway i look forward to have some guide on this. this is a very old and it does not seem to have a clear answer, looks like an hack. Google only answers with this blog post, but it does not go into why and how it works, so it's impossible to reason about or diagnose http://benfoster.io/blog/asp-net-core-dependency-injection-multi-tenant
Thanks

UPDATE :
I guess that line return next.Invoke(); should go inside the using 😄
It works, i've mixed feelings about it, but it works 👍

@sebastienros
Copy link
Member

sebastienros commented Aug 25, 2017

@valeriob

You might also want to store the previous context.RequestServices value before swapping it, or it won't be disposed when the request is done, but only the one you added.

How do you share services across tenants?

@valeriob
Copy link

valeriob commented Aug 25, 2017

Thanks @sebastienros good point! Where should i store it ?
Something like next.Invoke().ContinueWith(r=> { context.RequestServices = old instance }); ?

At the moment all the tenants have the same services available to them, they just have different persistence.

@davidfowl
Copy link
Member

davidfowl commented Aug 26, 2017

@valeriob use async await, it's more efficient than what you're doing and easier 😄 .

I guess that line return next.Invoke(); should go inside the using 😄

This is why you should use async await. The using is also broken when you do continue with.

It works, i've mixed feelings about it, but it works 👍

Not sure why, DI enabling the stack allowed you to do this in the first place. If you want to ignore the built in container and go back to what you were doing in the katana days then don't replace RequestServices. Create your own ControllerFactory and whatever other composition roots and use your own scoped container to resolve services.

@valeriob
Copy link

Thanks @davidfowl, you are right !
There is no benefit on doing my own controllerfactory, i like using the framework and i see the improvement from mvc5!
I guess the only thing that i do not like much is the abuse of service locator pattern :
https://github.com/aspnet/Mvc/search?utf8=%E2%9C%93&q=RequestServices&type=
but that's without context or deep analysis.

@davidfowl
Copy link
Member

I guess the only thing that i do not like much is the abuse of service locator pattern :

The service locator pattern is required at composition roots. Whenever you see us calling GetService, it should be because we're trying to active a dependency graph. It should never be used in places that could otherwise be solved with proper ctor injection.

The other reason it's used is to do resolve services lazily. We don't have a primitive for lazily resolving services (like Func<T> or Lazy<T>).

@valeriob
Copy link

Thanks for the explanation !
A lot of the search result are indeed from test code 👍

@aspnet-hello
Copy link

This issue is being closed because it has not been updated in 3 months.

We apologize if this causes any inconvenience. We ask that if you are still encountering this issue, please log a new issue with updated information and we will investigate.

@papyr
Copy link

papyr commented Nov 2, 2018

Why is this closed, its not been resovled!

@davidfowl
Copy link
Member

Because we don't have any plans to implement anything first class here and the issue is super old.

@ghost ghost locked as resolved and limited conversation to collaborators Dec 4, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests