Browse Source

Merge branch 'multi-tenancy-doc' into rel-3.2

pull/5585/head
Halil İbrahim Kalkan 6 years ago
parent
commit
7133f4a4f3
  1. 2
      docs/en/Modules/Identity.md
  2. 431
      docs/en/Multi-Tenancy.md

2
docs/en/Modules/Identity.md

@ -2,7 +2,7 @@
Identity module is used to manage [organization units](Organization-Units.md), roles, users and their permissions, based on the Microsoft Identity library.
See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon...
**See [the source code](https://github.com/abpframework/abp/tree/dev/modules/identity). Documentation will come soon...**
## Identity Security Log

431
docs/en/Multi-Tenancy.md

@ -1,53 +1,36 @@
## Multi-Tenancy
# Multi-Tenancy
ABP Multi-tenancy module provides base functionality to create multi tenant applications.
Multi-Tenancy is a widely used architecture to create **SaaS applications** where the hardware and software **resources are shared by the customers** (tenants). ABP Framework provides all the base functionalities to create **multi tenant applications**.
Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) multi-tenancy as like that:
Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) the multi-tenancy as like that:
> Software **Multi-tenancy** refers to a software **architecture** in which a **single instance** of software runs on a server and serves **multiple tenants**. A tenant is a group of users who share a common access with specific privileges to the software instance. With a multitenant architecture, a software application is designed to provide every tenant a **dedicated share of the instance including its data**, configuration, user management, tenant individual functionality and non-functional properties. Multi-tenancy contrasts with multi-instance architectures, where separate software instances operate on behalf of different tenants.
### Volo.Abp.MultiTenancy Package
## Terminology: Host vs Tenant
Volo.Abp.MultiTenancy package defines fundamental interfaces to make your code "multi-tenancy ready". So, install it to your project using the package manager console (PMC):
There are two main side of a typical SaaS / Multi-tenant application:
````
Install-Package Volo.Abp.MultiTenancy
````
* A **Tenant** is a customer of the SaaS application that pays money to use the service.
* **Host** is the company that owns the SaaS application and manages the system.
> This package is already installed by default with the startup template. So, most of the time, you don't need to install it manually.
We will use the Host and Tenant terms for that purpose in the rest of the document.
Then you can add **AbpMultiTenancyModule** dependency to your module:
## IMultiTenant
````C#
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
You should implement the `IMultiTenant` interface for your [entities](Entities.md) to make them **multi-tenancy ready**.
namespace MyCompany.MyProject
{
[DependsOn(typeof(AbpMultiTenancyModule))]
public class MyModule : AbpModule
{
//...
}
}
````
**Example: A multi-tenant *Product* entity**
> With the "Multi-tenancy ready" concept, we intent to develop our code to be compatible with multi-tenancy approach. Then it can be used in a multi-tenant application or not, depending on the requirements of the final application.
#### Define Entities
You can implement **IMultiTenant** interface for your entities to make them multi-tenancy ready. Example:
````C#
````csharp
using System;
using Volo.Abp.Domain.Entities;
using Volo.Abp.MultiTenancy;
namespace MyCompany.MyProject
namespace MultiTenancyDemo.Products
{
public class Product : AggregateRoot, IMultiTenant
public class Product : AggregateRoot<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; } //IMultiTenant defines TenantId property
public Guid? TenantId { get; set; } //Defined by the IMultiTenant interface
public string Name { get; set; }
@ -56,320 +39,278 @@ namespace MyCompany.MyProject
}
````
IMultiTenant requires to define a **TenantId** property in the implementing entity (See [entity documentation](Entities.md) for more about entities).
* `IMultiTenant` interface just defines a `TenantId` property.
#### Obtain Current Tenant's Id
When you implement this interface, ABP Framework **automatically** [filters](Data-Filtering.md) entities for the current tenant when you query from database. So, you don't need to manually add `TenantId` condition while performing queries. A tenant can not access to data of another tenant by default.
Your code may require to get current tenant's id (regardless of how it's retrieved actually). You can [inject](Dependency-Injection.md) and use **ICurrentTenant** interface for such cases. Example:
### Why the TenantId Property is Nullable?
````C#
using Volo.Abp.DependencyInjection;
`IMultiTenant.TenantId` is **nullable**. When it is null that means the entity is owned by the **Host** side and not owned by a tenant. It is useful when you create a functionality in your system that is both used by the tenant and the host sides.
For example, `IdentityUser` is an entity defined by the [Identity Module](Modules/Identity.md). The host and all the tenants have their own users. So, for the host side, users will have a `null` `TenantId` while tenant users will have their related `TenantId`.
> **Tip**: If your entity is tenant-specific and has no meaning in the host side, you can force to not set `null` for the `TenantId` in the constructor of your entity.
### When to set the TenantId?
ABP Framework doesn't set the `TenantId` for you (because of the cross tenant operations, ABP can not know the proper `TenantId` in some cases). So, you need to set it yourself **when you create a new multi-tenant entity**.
#### Best Practice
We suggest to set the `TenantId` in the constructor and never allow to change it again. So, the `Product` class can be re-written as below:
````csharp
using System;
using Volo.Abp.Domain.Entities;
using Volo.Abp.MultiTenancy;
namespace MyCompany.MyProject
namespace MultiTenancyDemo.Products
{
public class MyService : ITransientDependency
public class Product : AggregateRoot<Guid>, IMultiTenant
{
private readonly ICurrentTenant _currentTenant;
//Private setter prevents changing it later
public Guid? TenantId { get; private set; }
public string Name { get; set; }
public MyService(ICurrentTenant currentTenant)
public float Price { get; set; }
protected Product()
{
_currentTenant = currentTenant;
//This parameterless constructor is needed for ORMs
}
public void DoIt()
public Product(string name, float price, Guid? tenantId)
{
var tenantId = _currentTenant.Id;
//use tenantId in your code...
Name = name;
Price = price;
TenantId = tenantId; //Set in the constructor
}
}
}
````
#### Change Current Tenant
TODO: ...
#### Determining Current Tenant
> You can see the [entities document](Entities.md) for a more about entities and aggregate roots.
The first thing for a multi-tenant application is to determine the current tenant on the runtime. Volo.Abp.MultiTenancy package only provides abstractions (named as tenant resolver) for determining the current tenant, however it does not have any implementation out of the box.
You typically use the `ICurrentTenant` to set the `TenantId` while creating a new `Product`.
**Volo.Abp.AspNetCore.MultiTenancy** package has implementation to determine the current tenant from current web request (from subdomain, header, cookie, route... etc.). See Volo.Abp.AspNetCore.MultiTenancy Package section later in this document.
**Example: Creating a new product in a [Domain Service](Domain-Services.md)**
##### Custom Tenant Resolvers
You can add your custom tenant resolver to **AbpTenantResolveOptions** in your module's ConfigureServices method as like below:
````C#
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
````csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;
namespace MyCompany.MyProject
namespace MultiTenancyDemo.Products
{
[DependsOn(typeof(AbpMultiTenancyModule))]
public class MyModule : AbpModule
public class ProductManager : DomainService
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpTenantResolveOptions>(options =>
{
options.TenantResolvers.Add(new MyCustomTenantResolveContributor());
});
private readonly IRepository<Product, Guid> _productRepository;
//...
public ProductManager(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
}
}
````
`MyCustomTenantResolveContributor` must implement **ITenantResolveContributor** as shown below:
````C#
using Volo.Abp.MultiTenancy;
namespace MyCompany.MyProject
{
public class MyCustomTenantResolveContributor : ITenantResolveContributor
{
public void Resolve(ITenantResolveContext context)
public async Task<Product> CreateAsync(string name, float price)
{
context.TenantIdOrName = ... //find tenant id or tenant name from somewhere...
var product = new Product(name, price, CurrentTenant.Id);
return await _productRepository.InsertAsync(product);
}
}
}
````
A tenant resolver can set **TenantIdOrName** if it can determine it. If not, just leave it as is to allow next resolver to determine it.
* `DomainService` base class (and some common base classes in the ABP Framework) provides the `CurrentTenant`, so you directly use it. Otherwise, you need to [inject](Dependency-Injection.md) the `ICurrentTenant` service.
#### Tenant Store
## ICurrentTenant
Volo.Abp.MultiTenancy package defines **ITenantStore** to abstract data source from the framework. You can implement ITenantStore to work with any data source (like a relational database) that stores information of your tenants.
`ICurrentTenant` is the main service to interact with the multi-tenancy infrastructure. `ApplicationService`, `DomainService`, `AbpController` and some other base classes already has pre-injected `CurrentTenant` properties. For other type of classes, you can inject the `ICurrentTenant` into your service.
...
### Tenant Properties
##### Configuration Data Store
`ICurrentTenant` defines the following properties;
There is a built in (and default) tenant store, named ConfigurationTenantStore, that can be used to store tenants using standard [configuration system](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/) (with [Microsoft.Extensions.Configuration](https://www.nuget.org/packages/Microsoft.Extensions.Configuration) package). Thus, you can define tenants as hard coded or get from your appsettings.json file.
* `Id` (`Guid`): Id of the current tenant. Can be `null` if the current user is a host user or the tenant could not be determined from the request.
* `Name` (`string`): Name of the current tenant. Can be `null` if the current user is a host user or the tenant could not be determined from the request.
* `IsAvailable` (`bool`): Returns `true` if the `Id` is not `null`.
###### Example: Define tenants as hard-coded
````C#
using System;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Data;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
### Change the Current Tenant
namespace MyCompany.MyProject
{
[DependsOn(typeof(AbpMultiTenancyModule))]
public class MyModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpDefaultTenantStoreOptions>(options =>
{
options.Tenants = new[]
{
new TenantConfiguration(
Guid.Parse("446a5211-3d72-4339-9adc-845151f8ada0"), //Id
"tenant1" //Name
),
new TenantConfiguration(
Guid.Parse("25388015-ef1c-4355-9c18-f6b6ddbaf89d"), //Id
"tenant2" //Name
)
{
//tenant2 has a seperated database
ConnectionStrings =
{
{ConnectionStrings.DefaultConnectionStringName, "..."}
}
}
};
});
}
}
}
````
ABP Framework automatically filters the resources (database, cache...) based on the `ICurrentTenant.Id`. However, in some cases you may want to perform an operation on behalf of a specific tenant, generally when you are in the host context.
###### Example: Define tenants in appsettings.json
`ICurrentTenant.Change` method changes the current tenant for a limited scope, so you can safely perform operations for the tenant.
First create your configuration from your appsettings.json file as you always do.
**Example: Get product count of a specific tenant**
````C#
using System.IO;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.Modularity;
using Volo.Abp.MultiTenancy;
````csharp
using System;
using System.Threading.Tasks;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Domain.Services;
namespace MyCompany.MyProject
namespace MultiTenancyDemo.Products
{
[DependsOn(typeof(AbpMultiTenancyModule))]
public class MyModule : AbpModule
public class ProductManager : DomainService
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = BuildConfiguration();
private readonly IRepository<Product, Guid> _productRepository;
Configure<AbpDefaultTenantStoreOptions>(configuration);
public ProductManager(IRepository<Product, Guid> productRepository)
{
_productRepository = productRepository;
}
private static IConfigurationRoot BuildConfiguration()
public async Task<long> GetProductCountAsync(Guid? tenantId)
{
return new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
using (CurrentTenant.Change(tenantId))
{
return await _productRepository.GetCountAsync();
}
}
}
}
````
Then add a "**Tenants**" section to your appsettings.json:
* `Change` method can be used in a **nested way**. It restores the `CurrentTenant.Id` to the previous value after the `using` statement.
* When you use `CurrentTenant.Id` inside the `Change` scope, you get the `tenantId` provided to the `Change` method. So, the repository also get this `tenantId` and can filter the database query accordingly.
````json
"Tenants": [
{
"Id": "446a5211-3d72-4339-9adc-845151f8ada0",
"Name": "tenant1"
},
{
"Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d",
"Name": "tenant2",
"ConnectionStrings": {
"Default": "...write tenant2's db connection string here..."
}
}
]
````
> Always use the `Change` method with a `using` statement like done in this example.
##### Volo.Abp... Package (TODO)
## Determining the Current Tenant
TODO: This package implements ITenantStore using a real database...
The first thing for a multi-tenant application is to determine the current tenant on the runtime.
#### Tenant Information
ABP Framework provides an extensible **Tenant Resolving** system for that purpose. Tenant Resolving system then used in the **Multi-Tenancy Middleware** to determine the current tenant for the current HTTP request.
ITenantStore works with **TenantConfiguration** class that has several properties for a tenant:
### Tenant Resolvers
* **Id**: Unique Id of the tenant.
* **Name**: Unique name of the tenant.
* **ConnectionStrings**: If this tenant has dedicated database(s) to store it's data, then connection strings can provide database connection strings (it may have a default connection string and connection strings per modules - TODO: Add link to Abp.Data package document).
#### Default Tenant Resolvers
A multi-tenant application may require additional tenant properties, but these are the minimal requirements for the framework to work with multiple tenants.
The following resolvers are provided and configured by default;
#### Change Tenant By Code
* `CurrentUserTenantResolveContributor`: Gets the tenant id from claims of the current user, if the current user has logged in. **This should always be the first contributor for the security**.
* `QueryStringTenantResolveContributor`: Tries to find current tenant id from query string parameters. The parameter name is `__tenant` by default.
* `FormTenantResolveContributor`:Tries to find current tenant id from form parameters. The parameter name is `__tenant` by default.
* `RouteTenantResolveContributor`: Tries to find current tenant id from route (URL path). The variable name is `__tenant` by default. If you defined a route with this variable, then it can determine the current tenant from the route.
* `HeaderTenantResolveContributor`: Tries to find current tenant id from HTTP headers. The header name is `__tenant` by default.
* `CookieTenantResolveContributor`: Tries to find current tenant id from cookie values. The cookie name is `__tenant` by default.
TODO...
##### Problems with the NGINX
### Volo.Abp.AspNetCore.MultiTenancy Package
Volo.Abp.AspNetCore.MultiTenancy package integrate multi-tenancy to ASP.NET Core applications. To install it to your project, run the following command on PMC:
You may have problems with the `__tenant` in the HTTP Headers if you're using the [nginx](https://www.nginx.com/) as the reverse proxy server. Because it doesn't allow to use underscore and some other special characters in the HTTP headers and you may need to manually configure it. See the following documents please:
http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers
http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
````
Install-Package Volo.Abp.AspNetCore.MultiTenancy
````
##### AbpAspNetCoreMultiTenancyOptions
Then you can add **AbpAspNetCoreMultiTenancyModule** dependency to your module:
`__tenant` parameter name can be changed using `AbpAspNetCoreMultiTenancyOptions`.
````C#
using Volo.Abp.Modularity;
using Volo.Abp.AspNetCore.MultiTenancy;
**Example:**
namespace MyCompany.MyProject
````csharp
services.Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
{
[DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))]
public class MyModule : AbpModule
{
//...
}
}
options.TenantKey = "MyTenantKey";
});
````
#### Multi-Tenancy Middleware
> However, we don't suggest to change this value since some clients may assume the the `__tenant` as the parameter name and they might need to manually configure then.
Volo.Abp.AspNetCore.MultiTenancy package includes the multi-tenancy middleware...
#### Domain/Subdomain Tenant Resolver
````C#
app.UseMultiTenancy();
````
In a real application, most of times you will want to determine current tenant either by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). If so, you can configure the `AbpTenantResolveOptions` to add the domain tenant resolver.
TODO:...
**Example: Add a subdomain resolver**
#### Determining Current Tenant From Web Request
Volo.Abp.AspNetCore.MultiTenancy package adds following tenant resolvers to determine current tenant from current web request (ordered by priority). These resolvers are added and work out of the box:
* **CurrentUserTenantResolveContributor**: Gets the tenant id from claims of the current user, if the current user has logged in. **This should always be the first contributor for security**.
* **QueryStringTenantResolveContributor**: Tries to find current tenant id from query string parameter. Parameter name is "__tenant" by default.
* **FormTenantResolveContributor**: Tries to find current tenant id from form parameter. Parameter name is "__tenant" by default.
* **RouteTenantResolveContributor**: Tries to find current tenant id from route (URL path). Variable name is "__tenant" by default. So, if you defined a route with this variable, then it can determine the current tenant from the route.
* **HeaderTenantResolveContributor**: Tries to find current tenant id from HTTP header. Header name is "__tenant" by default.
* **CookieTenantResolveContributor**: Tries to find current tenant id from cookie values. Cookie name is "__tenant" by default.
````csharp
Configure<AbpTenantResolveOptions>(options =>
{
options.AddDomainTenantResolver("{0}.mydomain.com");
});
````
> If you use nginx as a reverse proxy server, please note that if `TenantKey` contains an underscore or other special characters, there may be a problem, please refer to:
http://nginx.org/en/docs/http/ngx_http_core_module.html#ignore_invalid_headers
http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
* `{0}` is the placeholder to determine current tenant's unique name.
* Add this code to the `ConfigureServices` method of your [module](Module-Development-Basics.md).
* This should be done in the *Web/API Layer* since the URL is a web related stuff.
#### Custom Tenant Resolvers
"__tenant" parameter name can be changed using AbpAspNetCoreMultiTenancyOptions. Example:
You can add implement your custom tenant resolver and configure the `AbpTenantResolveOptions` in your module's `ConfigureServices` method as like below:
````C#
services.Configure<AbpAspNetCoreMultiTenancyOptions>(options =>
````csharp
Configure<AbpTenantResolveOptions>(options =>
{
options.TenantKey = "MyTenantKey";
options.TenantResolvers.Add(new MyCustomTenantResolveContributor());
});
````
##### Domain Tenant Resolver
In a real application, most of times you will want to determine current tenant either by subdomain (like mytenant1.mydomain.com) or by the whole domain (like mytenant.com). If so, you can configure AbpTenantResolveOptions to add a domain tenant resolver.
`MyCustomTenantResolveContributor` must inherit from the `TenantResolveContributorBase` (or implement the `ITenantResolveContributor`) as shown below:
###### Example: Add a subdomain resolver
````C#
using Microsoft.Extensions.DependencyInjection;
using Volo.Abp.AspNetCore.MultiTenancy;
using Volo.Abp.Modularity;
````csharp
using Volo.Abp.MultiTenancy;
namespace MyCompany.MyProject
namespace MultiTenancyDemo.Web
{
[DependsOn(typeof(AbpAspNetCoreMultiTenancyModule))]
public class MyModule : AbpModule
public class MyCustomTenantResolveContributor : TenantResolveContributorBase
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpTenantResolveOptions>(options =>
{
//Subdomain format: {0}.mydomain.com
//Adding as the second highest priority resolver after 'CurrentUserTenantResolveContributor' to
//ensure the user cannot impersonate a different tenant.
options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com"));
});
public override string Name => "Custom";
//...
public override void Resolve(ITenantResolveContext context)
{
//TODO...
}
}
}
````
{0} is the placeholder to determine current tenant's unique name.
* A tenant resolver should set `context.TenantIdOrName` if it can determine it. If not, just leave it as is to allow the next resolver to determine it.
* `context.ServiceProvider` can be used if you need to additional services to resolve from the [dependency injection](Dependency-Injection.md) system.
### Multi-Tenancy Middleware
Instead of ``options.TenantResolvers.Insert(1, new DomainTenantResolveContributor("{0}.mydomain.com"));`` you can use this shortcut:
Multi-Tenancy middleware is an ASP.NET Core request pipeline [middleware](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware) that determines the current tenant from the HTTP request and sets the `ICurrentTenant` properties.
Multi-Tenancy middleware is typically placed just under the [authentication](https://docs.microsoft.com/en-us/aspnet/core/security/authentication) middleware (`app.UseAuthentication()`):
````C#
options.AddDomainTenantResolver("{0}.mydomain.com");
app.UseMultiTenancy();
````
###### Example: Add a domain resolver
> This middleware is already configured in the startup templates, so you normally don't need to manually add it.
````C#
options.AddDomainTenantResolver("{0}.com");
## Tenant Store
`ITenantStore` is used to get the tenant configuration from a data source.
### Tenant Management Module
The [tenant management module](Modules/Tenant-Management) is **included in the startup templates** and implements the `ITenantStore` interface to get the tenants and their configuration from a database. It also provides the necessary functionality and UI to manage the tenants and their connection strings.
### Configuration Data Store
**If you don't want to use the tenant management module**, the `DefaultTenantStore` is used as the `ITenantStore` implementation. It gets the tenant configurations from the [configuration system](Configuration.md) (`IConfiguration`). You can either configure the `AbpDefaultTenantStoreOptions` [options](Options.md) or set it in your `appsettings.json` file:
**Example: Define tenants in appsettings.json**
````json
"Tenants": [
{
"Id": "446a5211-3d72-4339-9adc-845151f8ada0",
"Name": "tenant1"
},
{
"Id": "25388015-ef1c-4355-9c18-f6b6ddbaf89d",
"Name": "tenant2",
"ConnectionStrings": {
"Default": "...tenant2's db connection string here..."
}
}
]
````
> It is recommended to **use the Tenant Management module**, which is already pre-configured when you create a new application with the ABP startup templates.
## See Also
* [Features](Features.md)
Loading…
Cancel
Save