Orchard Core, a multipurpose open-source platform built upon ASP.NET Core, offers strong support for multi-tenancy. This article explores the installation and configuration of Orchard Core for multitenancy .NET applications, as well as the setup of multitenancy through its admin dashboard and framework.

Additionally, we’ll look into the integration of features and modules to extend our application’s functionalities and dynamically create new tenants using the Orchard Core framework.

To download the source code for this article, you can visit our GitHub repository.

What is a Multitenancy Application?

Multitenant applications are software that efficiently shares resources and databases among multiple tenants, which allows an instance of the software and its infrastructure to serve multiple customers simultaneously. For more on multitenant applications, see our article on How to Build a Multitenant Application with ASP.NET Core

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

What is Orchard Core?

As described on the Orchard Core GitHub page, Orchard Core is designed to be modular and support multi-tenancy, making it adaptable to various web application needs.

There are two primary targets in Orchard Core.

Orchard Core Framework

This aspect of Orchard Core functions as an application framework, which provides us with a way to build modular and multi-tenant applications on ASP.NET Core. Hence, it offers a flexible architecture that enables us to create solutions to fit specific project requirements. 

Orchard Core CMS

Orchard Core CMS provides out-of-the-box content management capabilities. It is a Web Content Management System (CMS) that sits on the Orchard Core Framework as its base. It offers powerful content management features, allowing us to create, edit, and publish content easily.

Installing and Configuring Orchard Core Application

We can install Orchard Core through two primary methods: manual installation via NuGet packages or code generation using dotnet new templates. While both approaches have their merits, we’ll focus on the manual method in this guide to gain a deeper understanding of the installation process.

Alright, let’s begin by launching a command prompt, and create a new ASP.NET Core Web App (Razor Pages) project, and name it MyOrchardCoreCMS with this command:

dotnet new webapp -o MyOrchardCoreCMS

Also, let’s add the Orchard Core CMS NuGet package to the project:

dotnet add package OrchardCore.Application.Cms.Targets

Alternatively, if we prefer a minimal setup with only TheAdmin theme included, we can use OrchardCore.Application.Cms.Core.Targets instead. For this demo, we’ll go with the former.

Let’s make the necessary adjustments to the Program.cs class to integrate Orchard Core into our application:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOrchardCms();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseStaticFiles();
app.UseOrchardCore();

app.Run();

First, the AddOrchardCms() method adds all the necessary services, modules, and configurations to enable Orchard Core CMS into our application. This includes services for content management, authentication, authorization, media library, themes, and more.

Likewise, UseOrchardCore() method sets up the middleware and modules that Orchard Core needs to handle incoming HTTP requests like routing, authentication, authorization, and other functionalities managed by Orchard Core CMS.

Launching and Setting Up Orchard Core CMS

Let us build and run the project to launch our Orchard Core CMS website. Upon launching, the setup screen will display for us to fill in the necessary information:

Orchard Core CMS tenant setup

After filling in the required information and clicking the ‘Finish Setup’ button to finalize the configuration, our website will be created and ready for us to start multitenant websites. Let’s see that.

Configuring Multitenancy With Orchard Core Admin Dashboard

Orchard Core makes it easy to create multitenant websites through its admin dashboard. Let’s see how we can get into configuring multitenancy:

We’ll begin by navigating to /admin of the CMS website base address. This will redirect us to the login page. We’ll land on the admin dashboard after successful authentication with our login details.

To add a new tenant, let’s expand the ‘Multi-Tenancy’ menu and select ‘Tenants’. Currently, we have only the default tenant.

Let’s go ahead and add another tenant by clicking the ‘Add Tenant’ button to start the creation process:

Tenants page

Let’s fill in some of the fields to define the new tenant and click the ‘Create Tenant’ button to complete this stage of the tenant creation process using the Create Tenant page:

Create Tenant page in the Orchard Core

However, note that the new tenant isn’t yet publicly accessible. We need to complete the setup before Orchard Core starts running the tenant.

Finally, let’s finish the setup by clicking the ‘Setup’ button for our new Blog tenant. Here, let’s fill in the required information and submit the form:

Tenant list with setup button

 

Orchard Core tenant setup page:

Orchard Core tenant setup

Upon completion, Orchard Core will redirect us to the new tenant’s homepage.

Configuring Multitenancy with Orchard Core Framework

As we have seen, the Orchard Core CMS framework offers a solid solution for developing applications that serve multiple tenants.

However, what if we want to implement multitenancy in an ASP.NET Core application without the overhead of integrating the entire Orchard Core CMS framework? Fortunately, the Orchard Core framework provides a multitenancy feature that can be seamlessly integrated into ASP.NET Core applications without using the CMS.

Setting up Multitenancy

To demonstrate how to configure multitenancy with Orchard Core in an ASP.NET Core application, let’s create a new project ASP.NET Core Web App with Razor Pages with the name MultiTenantApp.

Additionally, we need to add the Orchard Core MVC NuGet package to our project:

dotnet add package OrchardCore.Application.Mvc.Targets 

This package provides the necessary functionality for integrating Orchard Core into our ASP.NET Core application.

Again, let’s modify the Program.cs class with the necessary codes to integrate Orchard Core multitenancy functionalities into our application:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddOrchardCore().AddMvc().WithTenants();

var app = builder.Build();

if (!app.Environment.IsDevelopment())
{
    app.UseExceptionHandler("/Error");
    app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseOrchardCore();

app.Run();

We call the AddOrchardCore() method to register the core Orchard Core services required by Orchard Core to our application’s dependency injection container.

Furthermore, we also use the WithTenants() method to configure our application to support multiple tenancy functionalities. Finally, the UseOrchardCore() method adds Orchard Core middleware to our application’s middleware pipeline for handling requests.

Configuring Tenants and Tenant-Specific Settings

Now that our application is set up to use Orchard Core’s multitenancy feature, let’s configure it in the appSettings.json file:

"OrchardCore": {
    "Default": {
      "State": "Running",
      "RequestUrlHost": null,
      "RequestUrlPrefix": null,
      "CustomTitle": "Default Tenant",
      "CustomSetting": "Custom setting for Default tenant"
    },
    "CustomerA": {
      "State": "Running",
      "RequestUrlHost": null,
      "RequestUrlPrefix": "customer-a",
      "CustomTitle": "Customer A",
      "CustomSetting": "Custom setting for Customer A"
    },
    "CustomerB": {
      "State": "Running",
      "RequestUrlHost": null,
      "RequestUrlPrefix": "customer-b",
      "CustomTitle": "Customer B",
      "CustomSetting": "Custom setting for Customer B"
    }
}

Orchard Core extends ASP.NET Core IConfiguration through IShellConfiguration, enabling configuration specific to individual tenants in the global application configuration. Essentially, Orchard Core automatically accesses the OrchardCore section in appSettings.json, eliminating the need for us to write custom code for this purpose.

Here is what is happening in this configuration:

  • OrchardCore: This top-level section contains all Orchard Core-related settings.
  • Default, CustomerA, and CustomerB: These represent different tenants of the application, each with its configuration settings.
  • State: Indicates the state of each tenant, with all tenants set to “Running” in this example.
  • RequestUrlHost: Specifies the host part of the URL for each tenant. It’s set to null, meaning it will inherit from the parent host.
  • RequestUrlPrefix: Specifies a prefix to use in the URL for each tenant. For example, “customer-a” and “customer-b” prefixes are defined for CustomerA and CustomerB tenants respectively.
  • CustomTitle: Custom title for each tenant, which can be displayed in the application UI or used for identification purposes.
  • CustomSetting: Additional custom settings specific to each tenant, allowing for fine-grained configuration

Understanding Features and Modules of Orchard Core

In Orchard Core, features and modules serve as foundational concepts that enable modular and scalable application development. Features represent units of functionality within Orchard Core, bundling together related components or functionalities that can be enabled or disabled independently.

Specifically, each feature typically contributes a unique aspect or capability to the application, which contains various components that furnish different functionalities to the system. Orchard Core simplifies feature management by providing administrative dashboard controls within the Orchard Core CMS as we’ve seen, alongside configuration options in appSettings.json or programmatically through the Orchard Core API.

On the other hand, Modules act as the base elements of an Orchard Core application. These are Class Library applications containing code, templates, assets, and other necessary resources to expand the application’s capabilities.

For example, modules can define one or more features and provide implementations for those features, thereby encapsulating specific functionalities or reusable components that integrate with an Orchard Core application.

Integrating Features and Modules in Tenants

Currently, our application has three tenants, Default, CustomerA and CustomerB. Let’s assume we want to extend our application with the capability of sending email and SMS. That’s where Orchard Core modules come in.

However, these features are not going to be available for all customers. CustomerA will have only the email feature enabled, while CustomerB will have both the email and SMS features available.

To begin, let’s create two separate Orchard Core modules: one for handling email functionality and another for managing SMS capabilities.

Let’s create a new Class Library project, and name it EmailModule. Next, let’s add the OrchardCore Module NuGet package to the EmailModule project:

dotnet add package OrchardCore.Module.Targets

The OrchardCore.Module.Targets NuGet package serves as a way to indicate to Orchard Core that this library functions as an Orchard Core module.

Next, we’ll create a class and name it Startup.cs in the EmailModule project. Orchard Core will read this class during the initialization of the module:

public void Configure(IEndpointRouteBuilder endpoints)
{
    endpoints.MapGet("/Email", async context => { await context.Response.WriteAsync("Welcome to Email module"); });
}

We set up an endpoint route to handle HTTP GET requests to the “/Email” path of the email module. 

Similarly, we’ll repeat our process to create the EmailModule for the SMS module. Let’s create a new Class Library project and name it SmsModule and then, implement the Startup.cs class:
public void Configure(IEndpointRouteBuilder endpoints)
{
    endpoints.MapGet("/Sms", async context => { await context.Response.WriteAsync("Welcome to SMS module"); });
}

Next, let’s open and update the appSettings.json in the MultiTenantApp project:

"CustomerA": {
  "State": "Running",
  "RequestUrlHost": null,
  "RequestUrlPrefix": "customer-a",
  "Features": [ "EmailModule" ],
  "CustomTitle": "Customer A",
  "CustomSetting": "Custom setting for Customer A"
},
"CustomerB": {
  "State": "Running",
  "RequestUrlHost": null,
  "RequestUrlPrefix": "customer-b",
  "Features": [ "SmsModule", "EmailModule" ],
  "CustomTitle": "Customer B",
  "CustomSetting": "Custom setting for Customer B"
}

The Features section lists the specific modules enabled for each tenant. For instance, CustomerA has the EmailModule enabled exclusively, whereas CustomerB enjoys both SmsModule and EmailModule functionalities enabled.

To access the email module for CustomerA we would send a request to localhost:7290/customer-a/Email, where the server responds with the message “Welcome to Email module”.

Alternatively, to access the Email module for CustomerB, we would replace customer-a in the URL with customer-b URL prefix.

However, if we try to access the SMS module from CustomerA tenant, the request will fail with an HTTP 404 error message.

Creating Tenant Dynamically

Orchard Core Framework supports dynamically adding tenants, which makes it easy to create and configure new tenants without manual intervention.

To begin, let’s create a new Class Library project and name it DynamicTenantModule. Within this project, we’ll add OrchardCore.Module.Targets:

dotnet add package OrchardCore.Module.Targets

Also, we need to add OrchardCore.Tenants:

dotnet add package OrchardCore.Tenants

OrchardCore.Tenants supports dynamic additions of tenants, allowing for on-the-fly creation and configuration of new tenants without requiring manual intervention.

Additionally, within the DynamicTenantModule project, let’s create a class with the name DynamicTenantSetup. This class will be responsible for the actual creation of new tenants:

public class DynamicTenantSetup(IShellHost shellHost, IShellSettingsManager shellSettingsManager)
{
    public async Task CreateTenant(string tenantName, string urlPrefix)
    {
        var shellSettings = new ShellSettings
        {
            Name = tenantName,
            RequestUrlHost = null,
            RequestUrlPrefix = urlPrefix,
            State = TenantState.Uninitialized,
        };
        
        shellSettings["customProperty"] = $"Custom settings for '{tenantName}'";
        
        await shellSettingsManager.SaveSettingsAsync(shellSettings);

        shellSettings.State = TenantState.Running;

        await shellHost.UpdateShellSettingsAsync(shellSettings);
    }
}

In the CreateTenant() method, we initialize a ShellSettings instance to configure settings for the new tenant. Key properties such as Name, RequestUrlPrefix, and custom properties are set from the tenantName and urlPrefix parameters, accordingly.

Finally, we call shellHost.UpdateShellSettingsAsync() to update the shell settings with the newly configured settings for the tenant.

ShellSettings is a class that holds the configuration settings for a tenant. In Orchard Core, each tenant is represented by a ShellSettings instance.

IShellHost is responsible for managing tenants. It acts as a gateway to a specific tenant’s resources. Also, it provides ways to interact with an individual tenant’s services and manage its lifecycle.

Next, let’s add the Startup.cs class for this module:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddScoped<DynamicTenantSetup>();
    }
}

Additionally, let’s create a TenantsController class to handle HTTP requests for creating new tenants dynamically:

[ApiController, Route("api/tenants")]
public class TenantsController(DynamicTenantSetup tenantSetup) : Controller
{
    public IActionResult Index()
    {
        return Ok("Tenant controller home");
    }
    
    [HttpPost("create")]
    [IgnoreAntiforgeryToken]
    public async Task<IActionResult> Create(string tenantName, string urlPrefix)
    {
        await tenantSetup.CreateTenant(tenantName, urlPrefix);
        
        return Ok($"Tenant '{tenantName}' created");
    }
}

To make the DynamicTenantModule available to the Default tenant, we’ll add it to the Default tenant’s section by modifying the appSettings.json in the MultiTenantApp project:

"Default": {
    "State": "Running",
    "RequestUrlHost": null,
    "RequestUrlPrefix": null,
    "Features": ["DynamicTenantModule"],
    "CustomTitle": "Default Tenant",
    "CustomSetting": "Custom setting for Default tenant"
}

Finally, to ensure the module is available to our application, we reference the DynamicTenantModule in the MultiTenantApp project.

Ultimately, our application is now able to create new tenants dynamically. For example, to dynamically create a new tenant, we’ll send a POST request to https://localhost:7290/api/tenants/create, providing the tenantName and urlPrefix arguments, respectively.

Conclusion

Multitenancy applications provide efficient solutions for sharing resources among multiple tenants, particularly in Software-as-a-Service (SaaS) delivery. Conversely, Orchard Core is a versatile platform that supports application framework and content management system (CMS) functionalities, built upon ASP.NET Core.

In other words, through its modular architecture and support for multitenancy, Orchard Core enables us to develop scalable and customizable multi-tenant applications.

Moreover, Orchard Core simplifies the integration of features and modules, enabling us to easily extend our application capabilities dynamically. By leveraging features and modules, we can enhance application functionalities using the inbuilt modular application capabilities Orchard Core provides us.

Additionally, the Orchard Core framework offers support for dynamically creating tenants, which eliminates the need for manual intervention in setting up new tenants. Through the DynamicTenantModule, we saw how we can automate the process of tenant creation.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!