Advanced Orchard: accessing other tenants' services

Zoltán Lehóczky's avatar
Tutorial, Multi-tenancy, Advanced

Many using Orchard's multi-tenancy feature sooner or later want to share data between tenants or generally, execute some operations on other tenants from one tenant. This is possible! Let's see how.

First some core Orchard fundamentals: Orchard creates so called shells for tenants. These shells describe that sub-application what a tenant is, among others the shell also has its own Autofac IoC container. This container deals with all the dependencies that are injected or otherwise requested. The interesting thing is that you can access the shell - the shell context - of all running tenants and through it also their IoC container. Now if you have the container you can make dependency resolution calls on it - what means that you can resolve services from that shell, i.e. that tenant.

public class TenantAccessDemo
{
    // ShellSettingsManager lets you access the shell settings of all the tenants.
    private readonly IShellSettingsManager _shellSettingsManager;
    // OrchardHost is the very core Orchard service running the environment.
    private readonly IOrchardHost _orchardHost;


    public TenantAccessDemo(IShellSettingsManager shellSettingsManager, IOrchardHost orchardHost)
    {
        _shellSettingsManager = shellSettingsManager;
        _orchardHost = orchardHost;
    }


    public void UseServicesFromTenant()
    {
        // Fetching the shell settings for a tenant. This is more efficient if you have many tenants with the Lombiq Hosting Suite, 
        // see below.
        var tenantShellSettings = _shellSettingsManager.LoadSettings().Where(settings => settings.Name == "TenantName").Single();
        var shellContext = _orchardHost.GetShellContext(tenantShellSettings);
        // Creating a new work context to run our code. Resolve() needs using Autofac;
        using (var wc = shellContext.LifetimeScope.Resolve<IWorkContextAccessor>().CreateWorkContextScope())
        {
            // You can resolve services from the tenant that you would normally inject through the constructor.
            var tenantSiteName = wc.Resolve<ISiteService>().GetSiteSettings().SiteName;
            // ...
        }
    }
}

If you're operating a multi-tenant environment you may also want to look at the Lombiq Hosting Suite: among others it has a Maintenance feature that you can use to run code in the context of tenants in a safe and scheduled way. Also it has an extended ShellSettingsManager that stores shell settings in the database: this means you can have any number of tenants, you can even fetch their settings by name or you can page when listing them and you don't have to take care about all the Settings.txt files in App_Data.

11 Comments

  • hihayden said Reply

    what's the difference between following two methods:
    using (var env = _orchardHost.CreateStandaloneEnvironment(shellSetting))
    using (var wc = shellContext.LifetimeScope.Resolve<IWorkContextAccessor>().CreateWorkContextScope())

  • harmony7 said Reply

    Very interesting. There is always more to learn about Orchard.

  • Zoltán Lehóczky said Reply

    @hihayden The second one creates a new work context scope from the given shell context. By wrapping pieces of code into new work contexts you can control the life time of dependencies, e.g. if you have two pieces of code wrapped into two work contexts then IDependency types will be instantiated twice as they live as long as their work context. A wc is similar to the scope of an http request.

    The first one on the other hand creates a new, standalone shell context, independent from the current shell context of the tenant. If your tenant is running then with this you'll have two shells running for the same tenant.

  • Zoltán Lehóczky said Reply

    And to add: creating a standalone shell context of course creates some overhead because it's starting a new shell, as opposed to using an already running shell.

  • Usquiano said Reply

    I just came up with this today, i have not tested yet!! one year ago, i built a multitenant website for a big company, and they had to share info btwn tenants.. it was a mess. Nobody on the orchardproject comunity could answer this. I had to use files to share data btwn tenants and write a complex portocol to sync data btwn tenants... This approach was exactly what i was looking for 1 year ago.. I will test it! coz i will need it on the future!
    Nice post!

  • Zoltán Lehóczky said Reply

    Thanks Usquiano, let us know how it worked out for you.

  • Anexander said Reply

    Can other tenant work context lifetime scope be extended to the end of the current request (or generally to the end of the current work context lifetime scope).

    I try to use this method inside a layout element and when I return some content items from a driver then I get an error when this content items are rendered.The error is "Instances cannot be resolved and nested lifetimes cannot be created from this LifetimeScope as it has already been disposed.". It happens when content item try to load some lazy fields. This problem is also relevant for many other situations. Example for passing delegates to somewhere.

  • Zoltán Lehóczky said Reply

    Since you manage the lifetime scope explicitly in this case it's up to you when you dispose it; and this should happen after any operation on that scope (including lazy field evaluations). So what you could do for example is to eagerly evaluate everything from the tenant scope and just keep the resulting values. Also you could, instead of embedding the expression in a using(), just create the WorkContextScope and explicitly dispose it at the end of the request.

  • Alexander said Reply

    Thanks Zoltán. The IWorkContextEvents service can be used for dispose a created IWorkContextScope.
    But there is another problem. When work context scope is being created it uses current http context that has url relevant to current tenant (and doesn't relevant to creating tenant). Because of that doesn't possible to use some part of orchard cms, Example urlHelper doesn't get correct content item urls. Can this problem be fixed? (The UrlHelper doesn't work correct because the HubRoute class retrieves routes collection by url from request that is relevant to current tenant).

  • Pedro said Reply

    Hi!
    Thanks for this information seems very nice. But I have other kind of problem. Is there a way to get data from another user? Like, I'm user A but I want to access to the data of user B?
    Thanks for the great work.

  • Zoltán Lehóczky said Reply

    Sure, you can do this from code as well, since users are also content items (of type "User"), so you can use IContentManager as usual. Or take a look at IUserService.

Add a Comment