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**.
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) the multi-tenancy as like that:
@ -10,8 +10,8 @@ Wikipedia [defines](https://en.wikipedia.org/wiki/Multitenancy) the multi-tenanc
There are two main side of a typical SaaS / Multi-tenant application:
* 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.
- 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.
The Host and the Tenant terms will be used for that purpose in the rest of the document.
ABP Framework supports all the following approaches to store the tenant data in the database;
***Single Database**: All tenants are stored in a single database.
***Database per Tenant**: Every tenant has a separate, dedicated database to store the data related to that tenant.
***Hybrid**: Some tenants share a single databases while some tenants may have their own databases.
-**Single Database**: All tenants are stored in a single database.
-**Database per Tenant**: Every tenant has a separate, dedicated database to store the data related to that tenant.
-**Hybrid**: Some tenants share a single database while some tenants may have their own databases.
[Tenant management module](Modules/Tenant-Management.md) (which comes pre-installed with the startup projects) allows you to set a connection string for any tenant (as optional), so you can achieve any of the approaches.
@ -48,11 +48,11 @@ Multi-tenancy system is designed to **work seamlessly** and make your applicatio
### IMultiTenant
You should implement the `IMultiTenant` interface for your [entities](Entities.md) to make them **multi-tenancy ready**.
You should implement the `IMultiTenant` interface for your [entities](Entities.md) to make them **multi-tenancy ready**.
`IMultiTenant` interface just defines a `TenantId` property. 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.
@ -96,9 +96,9 @@ If you set the `TenantId` value for a specific entity object, it will override t
`ICurrentTenant` defines the following properties;
*`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`.
-`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`.
#### Change the Current Tenant
@ -108,7 +108,7 @@ ABP Framework automatically filters the resources (database, cache...) based on
**Example: Get product count of a specific tenant**
*`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.
* Use `CurrentTenant.Change(null)` to change scope to the host context.
-`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.
- Use `CurrentTenant.Change(null)` to change scope to the host context.
> Always use the `Change` method with a `using` statement like done in this example.
@ -148,7 +148,7 @@ As mentioned before, ABP Framework handles data isolation between tenants using
**Example: Get count of products in the database, including all the products of all the tenants.**
See the [Data Filtering document](Data-Filtering.md) for more.
@ -201,15 +201,15 @@ ABP Framework provides an extensible **Tenant Resolving** system for that purpos
The following resolvers are provided and configured by default;
*`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.
*`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.
-`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.
-`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.
###### Problems with the NGINX
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:
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:
> 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.
@ -277,30 +279,30 @@ In a real application, most of times you will want to determine the current tena
> There is an [example](https://github.com/abpframework/abp-samples/tree/master/DomainTenantResolver) that uses the subdomain to determine the current tenant.
> There is an [example](https://github.com/abpframework/abp-samples/tree/master/DomainTenantResolver) that uses the subdomain to determine the current tenant.
If you use a sepereted Auth server, you must install `[Owl.TokenWildcardIssuerValidator](https://www.nuget.org/packages/Owl.TokenWildcardIssuerValidator)` on the `HTTPApi.Host` project
You can add implement your custom tenant resolver and configure the `AbpTenantResolveOptions` in your module's `ConfigureServices` method as like below:
* 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.
- 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
@ -384,9 +386,9 @@ Multi-Tenancy middleware is an ASP.NET Core request pipeline [middleware](https:
Multi-Tenancy middleware is typically placed just under the [authentication](https://docs.microsoft.com/en-us/aspnet/core/security/authentication) middleware (`app.UseAuthentication()`):
````csharp
```csharp
app.UseMultiTenancy();
````
```
> This middleware is already configured in the startup templates, so you normally don't need to manually add it.
@ -406,7 +408,7 @@ The [tenant management module](Modules/Tenant-Management) is **included in the s
**Example: Define tenants in appsettings.json**
````json
```json
"Tenants": [
{
"Id": "446a5211-3d72-4339-9adc-845151f8ada0",
@ -422,7 +424,7 @@ The [tenant management module](Modules/Tenant-Management) is **included in the s
}
}
]
````
```
> 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.
@ -440,4 +442,4 @@ The [Tenant Management module](Modules/Tenant-Management.md) provides a basic UI
Note: You should remove the old theme styles from "angular.json" if you are switching from "ThemeBasic" or "Lepton."
Look at the [Theme Configurations](../../UI/Angular/Theme-Configurations) list of styles. Depending on your theme, you can alter your styles in angular.json.
- Finally, remove `ThemeBasicModule` from `app.module.ts`, and import the related modules in `app.module.ts`
- Finally, remove `ThemeBasicModule`, `provideThemeBasicConfig` from `app.module.ts`, and import the related modules in `app.module.ts`
```js
import { ThemeLeptonXModule } from "@abp/ng.theme.lepton-x";
import { SideMenuLayoutModule } from "@abp/ng.theme.lepton-x/layouts";
@NgModule({
imports: [
// ...
// do not forget to remove ThemeBasicModule or other old theme module
// ThemeBasicModule.forRoot(),
ThemeLeptonXModule.forRoot(),
SideMenuLayoutModule.forRoot(),
// ThemeBasicModule
ThemeLeptonXModule.forRoot()
],
providers: [
// do not forget to remove provideThemeBasicConfig or other old theme providers
@ -7,7 +7,6 @@ If you add the account module to your project;
- "My account" link in the current user dropdown on the top bar will redirect the user to a page in the account module.
- You can switch the authentication flow to the resource owner password flow.
### Account Module Implementation
Install the `@abp/ng.account` NPM package by running the below command:
@ -18,18 +17,18 @@ npm install @abp/ng.account
> Make sure v4.3 or higher version is installed.
Open the `app.module.ts` and add `AccountConfigModule.forRoot()` to the imports array as shown below:
Open the `app.module.ts` and add `provideAccountConfig()` to the providers array as shown below:
```js
// app.module.ts
import { AccountConfigModule } from '@abp/ng.account/config';
import { provideAccountConfig } from "@abp/ng.account/config";
//...
@NgModule({
imports: [
providers: [
//...
AccountConfigModule.forRoot()
provideAccountConfig(),
],
//...
})
@ -57,19 +56,19 @@ The pro startup template comes with `@volo/abp.ng.account` package. You should u
```bash
npm install @volo/abp.ng.account
```
> Make sure v4.3 or higher version is installed.
Open the `app.module.ts` and add `AccountPublicConfigModule.forRoot()` to the imports array as shown below:
> Ensure that the `Account Layout Module` has been added if you are using the Lepton X theme. If you miss the step, you will get an error message that says `Account layout not found. Please check your configuration. If you are using LeptonX, please make sure you have added "AccountLayoutModule.forRoot()" to your app.module configuration.` when you try to access the account pages. Otherwise, you can skip adding the `AccountLayoutModule` step.
```js
// app.module.ts
import { AccountPublicConfigModule } from '@volo/abp.ng.account/public/config';
import { AccountPublicConfigModule } from "@volo/abp.ng.account/public/config";
// if you are using or want to use Lepton X, you should add AccountLayoutModule
// import { AccountLayoutModule } from '@volosoft/abp.ng.theme.lepton-x/account'
// import { AccountLayoutModule } from '@volosoft/abp.ng.theme.lepton-x/account'
//...
@ -104,9 +103,9 @@ Before v4.3, the "My account" link in the current user dropdown on the top bar r
### Personal Info Page Confirm Message
When the user changes their own data on the personal settings tab in My Account, The data can not update the CurrentUser key of Application-Configuration. The information of the user is stored in claims. The only way to apply this information to the CurrentUser of Application-Configuration is user should log out and log in. When the Refresh-Token feature is implemented, it will be fixed. So We've added a confirmation alert.
When the user changes their own data on the personal settings tab in My Account, The data can not update the CurrentUser key of Application-Configuration. The information of the user is stored in claims. The only way to apply this information to the CurrentUser of Application-Configuration is user should log out and log in. When the Refresh-Token feature is implemented, it will be fixed. So We've added a confirmation alert.
If you want to disable these warning, You should set `isPersonalSettingsChangedConfirmationActive` false
If you want to disable these warning, You should set `isPersonalSettingsChangedConfirmationActive` false
@ -11,7 +11,7 @@ The Basic Theme is a theme implementation for the Angular UI. It is a minimalist
If you need to manually this theme, follow the steps below:
* Install the [@abp/ng.theme.basic](https://www.npmjs.com/package/@abp/ng.theme.basic) NPM package to your Angular project.
* Open the `src/app/app.module.ts` file, import `ThemeBasicModule` (it can be imported from `@abp/ng.theme.basic` package), and add `ThemeBasicModule.forRoot()` to the `imports` array.
* Open the `src/app/app.module.ts` file, import `ThemeBasicModule`,`provideThemeBasicConfig` (it can be imported from `@abp/ng.theme.basic` package), and add `ThemeBasicModule` to the `imports` array and provide `provideThemeBasicConfig()` to the providers array.
* Open the `src/app/shared/shared.module` file, import `ThemeBasicModule` (it can be imported from `@abp/ng.theme.basic` package), and add `ThemeBasicModule` to the `imports` and `exports` array.
The `ThemeBasicModule` is registered own layouts (`ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`) to a service which is exposed by `@abp/ng.core` package on application initialization.
Each ABP theme module has 3 layouts named `ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`. These layouts can be replaced the same way.
@ -88,11 +87,13 @@ This component should have a 'router-outlet' for dynamic content loading. You ca
```bash
ng generate component new-layout
```
This command will create a new component named `new-layout`. Now, open the new-layout.component.html file and add a `router-outlet` to it:
```html
<router-outlet></router-outlet>
<router-outlet></router-outlet>
```
This 'router-outlet' will act as a placeholder that Angular dynamically fills based on the current router state.
note: (don't forget: you should add the app in the app.module.ts file)
@ -103,10 +104,11 @@ Although this step is optional, it can be useful if you're going to use the layo
```javascript
export const eCustomLayout = {
key: 'CustomLayout',
component: 'CustomLayoutComponent',
key: "CustomLayout",
component: "CustomLayoutComponent",
};
```
In this variable, `key` is a unique identifier for the layout component, and `component` is the name of the layout component.
You can use this variable when you need to refer to the layout component.
In this code, `configureLayoutFn` is a factory function that adds the new layout component to the `ReplaceableComponentsService`. The `APP_INITIALIZER` provider runs this function when the application starts.
note: (don't forget: you should add the CUSTOM_LAYOUT_PROVIDERS in the app.module.ts file)
#### Step 5: Pass the Dynamic Layouts to the CoreModule
The final step is to pass the dynamic layouts to the `CoreModule` using the `forRoot` method. This method allows you to configure the module with a static method.
The final step is to pass the dynamic layouts to the `provideAbpCore` using the `withOptions` method. This method allows you to configure the module with a static method.
In this code, `myDynamicLayouts` is the map of dynamic layouts you defined earlier. We pass this map to the `CoreModule` using the `forRoot` method.
In this code, `myDynamicLayouts` is the map of dynamic layouts you defined earlier. We pass this map to the `provideAbpCore` using the `withOptions` method.
Now that you have defined the new layout, you can use it in the router definition. You do this by adding a new route that uses the new layout.
Here's how you can do it:
Here's how you can do it:
```javascript
// route.provider.ts
@ -221,14 +225,13 @@ Run the following command in `angular` folder to create a new component called `
yarn ng generate component logo --inlineTemplate --inlineStyle
```
Open the generated `logo.component.ts` in `src/app/logo` folder and replace its content with the following:
```js
import { Component } from '@angular/core';
import { Component } from "@angular/core";
@Component({
selector: 'app-logo',
selector: "app-logo",
template: `
<aclass="navbar-brand"routerLink="/">
<!-- Change the img src -->
@ -284,14 +287,14 @@ yarn ng generate component routes
Open the generated `routes.component.ts` in `src/app/routes` folder and replace its content with the following:
```js
import { Component, HostBinding } from '@angular/core';
import { Component, HostBinding } from "@angular/core";
@Component({
selector: 'app-routes',
templateUrl: 'routes.component.html',
selector: "app-routes",
templateUrl: "routes.component.html",
})
export class RoutesComponent {
@HostBinding('class.mx-auto')
@HostBinding("class.mx-auto")
marginAuto = true;
get smallScreen() {
@ -321,11 +324,14 @@ Open the generated `routes.component.html` in `src/app/routes` folder and replac
You can change icons with the token of "confirmationIcons" in ThemeSharedModule in the app.module.ts. The changes will affect all confirmation popup in the project.
```js
...
ThemeSharedModule.forRoot({
confirmationIcons: {
info: 'fa fa-info-circle',
success: 'fa fa-check-circle',
warning: 'fa fa-exclamation-triangle',
error: 'fa fa-times-circle',
default: 'fa fa-question-circle',
},
}),
...
You can change icons with the `withConfirmationIcon()` method of `provideAbpThemeShared` function in the app.module.ts. The changes will affect all confirmation popup in the project.
```ts
import { provideAbpThemeShared, withConfirmationIcon } from '@abp/ng.theme.shared';
As of ABP v3.0, every lazy-loaded module has a config module available via a secondary entry point on the same package. Importing them in your root module looks like this:
```js
import { IdentityConfigModule } from "@abp/ng.identity/config";
```ts
import { provideIdentityConfig } from "@abp/ng.identity/config";
@ -6,28 +6,20 @@ Reactive forms in ABP Angular UI are validated by [ngx-validate](https://www.npm
## How to Add New Error Messages
You can add a new error message by passing validation options to the `ThemeSharedModule` in your root module.
You can add a new error message by passing validation options to the `withValidationBluePrint` method of `provideAbpThemeShared` function in your root module.
```js
import { VALIDATION_BLUEPRINTS } from "@ngx-validate/core";
import { DEFAULT_VALIDATION_BLUEPRINTS } from "@abp/ng.theme.shared";
```ts
import { provideAbpThemeShared, withValidationBluePrint } from '@abp/ng.theme.shared';
To get a localized text as [_Observable_](https://rxjs.dev/guide/observable) use `get` method instead of `instant`:
```js
this.localizationService.get('Resource::Key');
this.localizationService.get("Resource::Key");
// with fallback value
this.localizationService.get({
key: 'Resource::Key',
defaultValue: 'Default Value',
key: "Resource::Key",
defaultValue: "Default Value",
});
```
@ -118,41 +119,46 @@ Localizations can be determined on backend side. Angular UI gets the localizatio
See an example:
```ts
// app.module.ts
import { provideAbpCore, withOptions } from '@abp/ng.core';
@NgModule({
imports: [
//...other imports
CoreModule.forRoot({
localizations: [
{
culture: 'en',
resources: [
{
resourceName: 'MyProjectName',
texts: {
Administration: 'Administration',
HomePage: 'Home',
providers: [
// ...
provideAbpCore(
withOptions({
...,
localizations: [
{
culture: 'en',
resources: [
{
resourceName: 'MyProjectName',
texts: {
Administration: 'Administration',
HomePage: 'Home',
},
},
},
],
},
{
culture: 'de',
resources: [
{
resourceName: 'MyProjectName',
texts: {
Administration: 'Verwaltung',
HomePage: 'Startseite',
],
},
{
culture: 'de',
resources: [
{
resourceName: 'MyProjectName',
texts: {
Administration: 'Verwaltung',
HomePage: 'Startseite',
},
},
},
],
},
],
}),
]
],
},
],
}),
),
...
],
})
export class AppModule {}
```
...or, you can determine the localizations in a feature module:
@ -256,10 +262,10 @@ Find [styles configuration in angular.json](https://angular.io/guide/workspace-c
If you have created and injected chunks for Fontawesome as seen above, you no longer need the lazy loading in the `AppComponent` which was implemented before v2.9. Simply remove them. The `AppComponent` in the template of the new version looks like this:
```js
import { Component } from '@angular/core';
import { Component } from "@angular/core";
@Component({
selector: 'app-root',
selector: "app-root",
template: `
<abp-loader-bar></abp-loader-bar>
<router-outlet></router-outlet>
@ -274,33 +280,32 @@ Since ABP has more than one language, Angular locale files loads lazily using [W
### registerLocaleFn
`registerLocale` function that exported from `@abp/ng.core/locale` package is a higher order function that accepts `cultureNameLocaleFileMap` object and `errorHandlerFn` function as params and returns Webpack `import` function. A `registerLocale` function must be passed to the `forRoot` of the `CoreModule` as shown below:
```js
// app.module.ts
`registerLocale` function that exported from `@abp/ng.core/locale` package is a higher order function that accepts `cultureNameLocaleFileMap` object and `errorHandlerFn` function as params and returns Webpack `import` function. A `registerLocale` function must be passed to the `withOptions` of the `provideAbpCore` as shown below:
```ts
import { provideAbpCore, withOptions } from '@abp/ng.core';
import { registerLocale } from '@abp/ng.core/locale';
// if you have commercial license and the language management module, add the below import
// import { registerLocale } from '@volo/abp.ng.language-management/locale';
@NgModule({
imports: [
// ...
CoreModule.forRoot({
// ...other options,
registerLocaleFn: registerLocale(
// you can pass the cultureNameLocaleFileMap and errorHandlerFn as optionally
The authentication functionality has been moved from @abp/ng.core to @abp/ng.ouath since v7.0.
If your app is version 7.0 or higher, you should include "AbpOAuthModule.forRoot()" in your app.module.ts as an import after "CoreModule.forRoot(...)".
If your app is version 8.3 or higher, you should include "provideAbpOAuth()" in your app.module.ts as an providers after "provideAbpCore()
".
Those abstractions can be found in the @abp/ng-core packages.
- `AuthService` (the class that implements the IAuthService interface).
- `NAVIGATE_TO_MANAGE_PROFILE` Inject token.
- `ApiInterceptor` (the class that implements the IApiInterceptor interface).
@ -11,9 +14,9 @@ Those abstractions can be found in the @abp/ng-core packages.
Those base classes are overridden by the "AbpOAuthModule" for oAuth. There are also three functions provided with AbpOAuthModule.
- `PIPE_TO_LOGIN_FN_KEY` a provide that calls a function when the user is not authenticated. The function should be PipeToLoginFn type.
- `SET_TOKEN_RESPONSE_TO_STORAGE_FN_KEY` a provide that calls a function when the user is authenticated. The function should be SetTokenResponseToStorageFn type.
- `SET_TOKEN_RESPONSE_TO_STORAGE_FN_KEY` a provide that calls a function when the user is authenticated. The function should be SetTokenResponseToStorageFn type.
- `CHECK_AUTHENTICATION_STATE_FN_KEY` a provide that calls a function when the user is authenticated and stores the auth state. The function should be CheckAuthenticationStateFn type.
The tokens and interfaces are in the `@abp/ng.core` package but the implementation of these interfaces is in the `@abp/ng.oauth` package.
The tokens and interfaces are in the `@abp/ng.core` package but the implementation of these interfaces is in the `@abp/ng.oauth` package.
If you want to make your own authentication system, you must also change these 'abstract' classes.