ASP.NET Boilerplate v0.9 and ASP.NET Zero v1.10 Have Been Released

ASP.NET Boilerplate v0.9 and ASP.NET Zero v1.10 Have Been Released

We are glad to announce that AspNet Boilerplate v0.9 and AspNet Zero v1.10 Have Been Released!

First of all, as you probably know, we were planning to release v0.9 with AspNet Core support. But, since AspNet Core’s RC2 date is not clear ye and we’re expecting many breaking changes from RC1, we decided to wait for RC2 and focus on new features for ASP.NET Boilerplate framework. So, AspNet Core version probably will be v0.10.

ASP.NET Boilerplate v0.9

Multi Tenancy

This release mostly focused on database per tenant architecture for multi tenant applications. ASP.NET Boilerplate is designed to work well for multi-tenant applications from beginning. But it focused on multi tenant single database architecture. Now, it supports and provides infrastructure for any type of database architecture;

  • Multi tenant – single database: Host data and all data of all tenants are stored in a single database.
  • Multi tenant – multi databases: One database for the host (master) and separated databases for each tenant.
  • Multi tenant – hybrid database: Some tenants are in host database, some tenants have their dedicated databases, some tenants are grouped in a seperated database and so on…
  • Single tenant – single database: No multi-tenancy at all.

Beside that, you can have more than one DbContext (for EF) combined with any scenario to create more complex architectures. We made a huge amount of work to support database per tenant architecture. Actually, it supports hybrid architecture. So, database per tenant is just a special case, you can store a tenant’s data in any database you want.

If you create a new template (included module-zero) then your project will be configured and ready for hybrid architecture. For existing applications, you may need to do some changes.

Supporting database per tenant architecture is not something like to change connection string dynamically based on current tenant. It’s not that simple. Let’s see what’s changed and how your application can work in hybrid style…

SetTenantId

As you know, ABP uses automatic data filters while querying a tenant data from database. It’s a good system when multiple tenants are stored in single database. Thus, you can query for only current (or desired) tenant without explicitly specifying TenantId in the Where condition of your query.

ABP uses TenantId in the session as default for the filtering. We were using SetFilterParameter to change current TenantId when needed. Now, you can use SetTenantId() method to switch to a desired tenant. Example:

public class ProductService : ITransientDependency
{
    private readonly IRepository<Product> _productRepository;
    private readonly IUnitOfWorkManager _unitOfWorkManager;

    public ProductService(IRepository<Product> productRepository, IUnitOfWorkManager unitOfWorkManager)
    {
        _productRepository = productRepository;
        _unitOfWorkManager = unitOfWorkManager;
    }

    [UnitOfWork]
    public virtual List<Product> GetProducts(int tenantId)
    {
        using (_unitOfWorkManager.Current.SetTenantId(tenantId))
        {
            return _productRepository.GetAllList();
        }
    }
}

SetTenantId have some benefits over primitive SetFilterParameter:

  • It internally changes both of tenant filters at once (MayHaveTenant and MustHaveTenant).
  • SetTenantId works for hybrid databases (or database per tenant) and can switch to tenant’s dedicated database if needed. SetFilterParameter only changes filter for current database.

So, always use SetTenantId, instead of SetFilterParameter to switch to desired tenantid programmaticaly. Note that; you can use SetTenantId(null) to switch to the host. See multitenancy document for more information.

Disabling Filters

We were using DisableFilter method to disable multi-tenancy filters. Thus, we were able to query all datas of all tenants in single query. While this method still can be usable, it will obviously can not work for database per tenant approach. So, use it only if you work single database for all tenants. If you are creating an application or a reusable library that can be used in a database per tenant (or hybrid) architecture, then do not disable multitenancy filters to query all tenants data since it will not be possible.

One related thing is TenantId in where conditions of your LINQ (in case of disabling multitenancy filters). If you will use database per tenant then you should not use TenantId in your where conditions since it’s meaningless. Just use SetTenantId if you need, as described above.

UserIdentifier

UserId (in session, setting management…) is 8-byte integer (long) in ABP and assumed to be generated by database. So, if you will use different databases for different tenants, then UserId will not be unique in your application. A unique id for a user will be a combination of TenantId and UserId. That’s why we introduced UserIdentifier class (value type). You will see it in some APIs (setting management, notifications, authorization and so on…) those only use UserId before. So, some API methods are marked as obsolete. It’s required to change your code if you want to use different databases for different tenants.

Actually, UserId is just a special case. If your entities are distributed to separated databased for different tenants, their Ids will not be unique anymore. For example, you may have two Products with same Ids in different databases (if Product’s primary key is an auto increment integer value). So, comparing Ids will not be enough to understand if two Products are same, you also need to compare TenantIds. Fortunately, ABP’s base Entity class overrides Equals method and == operator, and uses TenantId of the entity in this comparison, if given entity implements IMayHaveTenant or IMustHaveTenant interfaces. So, you can safely compare two entities to understand if they are same entities.

Additional info: You can create a UserIdentifier object from current session using IAbpSession.ToUserIdentifier() extension method.

TenantId Foreign Key

We can no longer use TenantId of a table as foreign key to Tenants table. Because, Tenants table will be in a different database and cross-database foreign key is not possible.

MSDTC

ABP uses transaction for each unit of work as default. If our UOW includes more than one database operation, then it becomes a distributed transaction. As you know, this may require MSDTC (Microsoft Distributed Transaction Coordinator) be enabled and running on the server.

DbContext(es)

In EntityFramework, we can have different DbContext structures for host and tenants.

Single DbContext

We can have a single DbContext class which includes both host and tenant entities. This approach has some pros and cons:

  • Pros
    • Easy database migration. We will have only one database migration code for all databases. This makes it easy in single db and hybrid architectures.
  • Cons
    • All tables are in all databases. That means we will have host tables (say, Tenants and Editions) in all tenant databases even they are empty.
    • When you use IRepository<YourEntity>, it always work in current tenant’s (or host’s if active TenantId is null) database even actually it’s a host side entity. Example: In Module-zero, Edition entity normally should be stored in the host database. If current TenantId is 42 and we use IRepository<Edition>.Insert(…) method, then new Edition is inserted into Tenant 42’s database. We probably don’t want that. To overcome this issue, we have two options:
      • Use SetTenantId(null) as defined above to switch to the host side. ABP selects the database when you use the repository, not when you inject it.
      • Add [MultiTenancySide(MultiTenancySide.Host)] attribute to the Entitity that should be only in the host database. ABP then can know to work in host database for this entity. In module zero; Tenant, Edition, Feature and UserAccount entities are marked as Host. For those entities, it will not be a problem.
Host DbContext vs Tenant DbContext

We can have a HostDbContext (contains only host side entities) and a TenantDbContext (contains only tenant side entities). We can also have an abstract CommonDbContext which contains common entities. Assume that HostDbContext and TenantDbContext are derived from CommonDbContext. This approach has also some pros and cons:

  • Pros
    • Host database will not contain tenant tables. Also, tenant database will not contain host tables. So, data and schema will be completely separated.
    • If YourEntity is only included in one dbcontext, then ABP automatically selects that dbcontext when you use IRepository<YourEntity>.
  • Cons
    • Database migration problem: First of all, we will have 2 dbcontexes. So, we will have 2 different migrations. This is not a problem if host database and tenant databases are completely separated. But if you use single database or hybrid architecture, then both migrations will be applied in the same database. And if you have a CommonDbContext (or somehow use same table names), you will have common migration code in your migrations. This will be a problem when running migrations. There will be conflicts. This is a common EntityFramework problem, beside ABP framework. To solve this issues, you can delete common migration code manually from host migration and apply host+tenant migration to the host database. But, even you do it once, it will be needed whenever you change CommonDbContext. So, be careful about that. We hadn’t find a better way yet.
    • Entities in common dbcontext: For example; if you have a User entity in CommonDbContext, then actually User entitiy is included both in HostDbContext and TenantDbContext. So, when you use IRepository<User>, then ABP should know which DbContext you want to use. To solve this issue:
      • Add [MultiTenancySide(MultiTenancySide.Host)] attribute to the HostDbContext and add [MultiTenancySide(MultiTenancySide.Tenant)] attribute to the TenantDbContext. Then ABP dynamically choice the right DbContext by using current TenantId. If TenantId is null, then HostDbContext is used, otherwise TenantDbContext is used. Remember that you can change current TenantId using SetTenantId method.
Creating Tenant Databases

In a db per tenant approach, we need to create/delete a databases dynamically when a tenant is created/deleted. Also, we need to apply initial migrations (and probably a seed data) just after creating the database. Module Zero startup template contains needed code to create database when a Tenant is created. So, your application should have privilege to create databases on the fly. If not, you may need to seperately create database for new tenants.

There can be alternative options. For example, you may use Azure Elastic Database Pool API to create databases. Then you should change the template code for that.

Migrating Databases

Database migrations one of the biggest problems of multi database approach. Since we will have a single deployment of the application, all databases should be updated (migrated) when an new application version installed. It may take a long time to update all in once since you may have thousands of tenant databases. Module Zero startup template contains a console application (Migrator.exe) to update them all in once. If you need a different approach, you can take it as a startup point.

Last Notes For Database Architecture

We suggest you to create your application independent from database architecture. That means, you should develop it based on hybrid architecture. Always use SetTenantId to explicitly change current tenant. Never disable tenancy filters since it does not works for multi database approach.

Good news; You mostly do nothing about multi-tenancy. Most of your code (probably more than 90%) will be unaware of multitenancy, thanks to dynamic filtering and dynamic database switch features of ABP. Your code mostly works for current user’s Tenant in the session. Multi-tenancy code (SetTenantId… etc.) comes into place especially for account operations (login, register) and tenant operations (create, update tenant specific data from a host user).

TimeZone Management

With the v0.9 release, we have made many improvements to better support to store and process datetimes in different time zones. To enable timezone features, we should switch to UTC, instead of local time. You can write this code to most beginning of your application (for example, as first line in Application_Start in Global.asax):

Clock.Provider = new UtcClockProvider();

Update: This is changed in v0.11 and should be like that:

Clock.Provider = ClockProviders.Utc;

Then ABP enables multi time zone features:

  • Uses DateTime.UtcNow instead of DateTime.Now when you use Clock.Now. Since it always use Clock class internally, all new datetimes will be UTC.
  • Sets Kinds of DateTimes to UTC while getting from database (for EntityFramework and NHibernate).
  • Sets Kinds of DateTimes to UTC and converting to UTC (if needed) while getting data from clients (via MVC and Web API actions).
  • Serializes all DateTimes as UTC for JSON serialization (for example, when returning data to clients).
  • Added a setting, named “Abp.Timing.TimeZone” to store timezone preference of application, tenant and user. This setting can store Windows TimeZone Name. ITimeZoneConverter service can convert given time to user’s, tenant’s or application’s time zone.

Notification System

Notification system has effected from multitenancy changes. We introduced a new entity, TenantNotificationInfo, to distribute notifications to tenant users. Also changed existing entities a little to support separated database approach. See Module Zero section below for details on database migrations.

Module Zero v0.9

As you probably know, ABP framework itself does not force any database schema and provides abstractions for data access requirements. And module zero (Abp.Zero.* packages) implement those abstractions and adds user, role, tenant, organization unit… management. So, we made a few changes in module zero to support database per tenant and hybrid architecture. Also, there are important breaking changes, but you can fix them easily by following this guide.

Tenant, Role and User Entities

Tenant, Role and User entities are derived from AbpTenant, AbpRole and AbpUser. These base classes are generic classes and their generic parameters are changed:

  • AbpUser<TTenant, TUser> become AbpUser<TUser>
  • AbpRole<TTenant, TUser> become AbpRole<TUser>
  • AbpTenant<TTenant, TUser> become AbpTenant<TUser>

As you see, we removed TTenant parameters since in database per tenant architecture, we can not define navigation properties from tenant users to tenant entity, they may be in different databases.

As similar, we removed TTenant parameter from AbpRoleManager, AbpRoleStore, UserStore base classes. Their constructors have been changed.

Also, we changed constructors of AbpEditionManager, FeatureValueStore and TenantManager. You can easily check base constructor and add/remove/replace injected dependencies. You can create a new template and see the latest versions if you can not do it for your existing application.

Tenant.ConnectionString Property

We added a ConnectionString property to Tenant entity. This allows us to know tenant’s database. ConnectionString property can be null if a tenant use host database to store data (in single database or hybrid approach). We added this property to also startup template to allow to use different database for new tenants.

ConnectionString property is encrypted using SimpleStringCipher class. If you need to change ConnectionString property of a tenant, always decrypt/encrypt it using SimpleStringCipher.Instance.Encrypt and SimpleStringCipher.Instance.Decrypt methods.

New TenantId Fields

There were not TenantId properties for PermissionSetting, UserRole, UserLogin, UserNotifications entities. Also, Setting entity was not IMayHaveTenant. We added these missing TenantId columns to support multi database architecture.

After upgrading Abp.Zero.* packages, you should add a new Migration for EF. You will see these changes in that migration. You should also add the following SQL code into your migration to update new TenantId columns and fix your current database:

//Update current AbpUserRoles.TenantId values

Sql(@"UPDATE AbpUserRoles
SET TenantId = AbpUsers.TenantId
FROM AbpUsers
WHERE AbpUserRoles.UserId = AbpUsers.Id");

//Update current AbpUserLogins.TenantId values

Sql(@"UPDATE AbpUserLogins
SET TenantId = AbpUsers.TenantId
FROM AbpUsers
WHERE AbpUserLogins.UserId = AbpUsers.Id");

//Update current AbpPermissions.TenantId values

Sql(@"UPDATE AbpPermissions
SET TenantId = AbpUsers.TenantId
FROM AbpUsers
WHERE AbpPermissions.UserId = AbpUsers.Id");

Sql(@"UPDATE AbpPermissions
SET TenantId = AbpRoles.TenantId
FROM AbpRoles
WHERE AbpPermissions.RoleId = AbpRoles.Id");

//Update current AbpUserNotifications.TenantId values

Sql(@"UPDATE AbpUserNotifications
SET TenantId = AbpUsers.TenantId
FROM AbpUsers
WHERE AbpUserNotifications.UserId = AbpUsers.Id");

//Update current AbpSettings.TenantId values

Sql(@"UPDATE AbpSettings
SET TenantId = AbpUsers.TenantId
FROM AbpUsers
WHERE AbpSettings.UserId = AbpUsers.Id");

Your seed code should also be changed to set new properties.

Note: EF migration will drop some TenantId indexes. Remove those DropIndex code indexing by TenantId is important for performance.

UserAccount Entity

Since users of tenants may be stored in their own database, it can be hard to query users of all tenants in a separated database approach. Therefore, we introduced a new Entity: UserAccount. UserAccount entities are stored in AbpUserAccounts table (by default) and contains all users of all tenants (including host users). This table is automatically updated and maintained (via handling entity change events of ABP). But, for existing applications, we need to fill it once. You can add this SQL to your database migration code:

INSERT INTO AbpUserAccounts(TenantId, UserId, UserName, EmailAddress, LastLoginTime, CreationTime, IsDeleted) 
SELECT TenantId, Id AS UserId, UserName, EmailAddress, LastLoginTime, CreationTime, IsDeleted FROM AbpUsers

Notifications

When you add migration, you will see a new AbpTenantNotifications table added. Also, AbpUserNotifications table has a rename: NotificationId foreign key changed to TenantNotificationId. You need to manually remove DropColumn for NotificationId and remove AddColumn for TenantNotificationId and add the rename code:

RenameColumn("dbo.AbpUserNotifications", "NotificationId", "TenantNotificationId");

New Users Page

We added a simple user list page and a user creation modal to startup template.

ASP.NET Zero v1.10

In this release, we focused on adapting ABP changes on multiple database and multiple timezones. Now, we can specify a connection string while creating a new tenant:

zero-dp-per-tenant

If we don’t specify it, host database is used to store tenant data. Thus, it supports hybrid architecture as defined above. Note that; if you create tenant in host database, there is no automatic way of moving all tenant data to a separated database later.

Also, we added Timezone settings for application, tenant and user.

Beside these new features and ABP upgrade, we upgraded Metronic theme to v4.5.6 and made some fixes and improvements.

9 Comments
  • Justin
    Reply
    Posted at 18:45, June 8, 2016

    Is there a Repository project in github for ABP, i am looking to replace EF with Dapper

  • Justin
    Reply
    Posted at 17:03, June 9, 2016

    How does abp handle Single Deployment – Hybrid Databases ? does that mean dedicated db also needs tenantId in each shared table? or only shared db needs tenantId in each table.

  • Justin
    Reply
    Posted at 17:14, June 9, 2016

    Great Framework and a great documentation for a Open Source project.

  • Justin
    Reply
    Posted at 20:57, June 9, 2016

    I went through lot of the documentation but did not see any examples for Singleton object By tenant not host. A Object that has instance per tenant.

  • Justin
    Reply
    Posted at 21:59, June 9, 2016

    Also are there any recommended tweaks for production performance deployment for ABP?

Post a Comment

Comment
Name
Email
Website