Revisit the EF Core migrations document for ABP 4.4.

pull/9515/head
Halil İbrahim Kalkan 4 years ago
parent 23a99557cc
commit 0f5ccf41c6

@ -61,9 +61,9 @@ ABP Framework's [connection string](Connection-Strings.md) system allows you to
} }
```` ````
The example configuration about tells to the ABP Framework to use the second connection string for the [Audit Logging module](Modules/Audit-Logging.md). The example configuration tells to the ABP Framework to use the second connection string for the [Audit Logging module](Modules/Audit-Logging.md) (if you don't specify connection string for a module, it uses the `Default` connection string).
**However, this is just the beginning**. You also need to create the second database, create audit log tables inside it and maintain the database tables using the code first migrations approach. One of the main purposes of this document is to guide you on such **database separation** scenarios. **However, this can work only if the audit log database with the given connection string is available**. So, you need to create the second database, create audit log tables inside it and maintain the database tables. No problem if you manually do all these. However, the recommended approach is the code first migrations. One of the main purposes of this document is to guide you on such **database separation** scenarios.
#### Module Tables #### Module Tables
@ -85,23 +85,24 @@ This code changes the prefix of the [Identity Server](Modules/IdentityServer.md)
> Every module also defines `DbSchema` property (near to `DbTablePrefix`), so you can set it for the databases support the schema usage. > Every module also defines `DbSchema` property (near to `DbTablePrefix`), so you can set it for the databases support the schema usage.
### The Projects ### .EntityFrameworkCore Project
From the database point of view, there is one important project that will be explained in the next sections. The solution contains a project, which's name ends with `.EntityFrameworkCore`. This project has the `DbContext` class (`BookStoreDbContext` for this sample) of your application.
#### .EntityFrameworkCore Project **Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your [repositories](Repositories.md) if you follow the best practices and hide your data access code behind the repositories). It is almost an empty `DbContext` since your application don't have any entities at the beginning:
This project has the `DbContext` class (`BookStoreDbContext` for this sample) of your application.
**Every module uses its own `DbContext` class** to access to the database. Likewise, your application has its own `DbContext`. You typically use this `DbContext` in your application code (in your [repositories](Repositories.md) if you follow the best practices). It is almost an empty `DbContext` since your application don't have any entities at the beginning, except the pre-defined `AppUser` entity:
````csharp ````csharp
[ReplaceDbContext(typeof(IIdentityDbContext))]
[ReplaceDbContext(typeof(ITenantManagementDbContext))]
[ConnectionStringName("Default")] [ConnectionStringName("Default")]
public class BookStoreDbContext : AbpDbContext<BookStoreDbContext> public class BookStoreDbContext :
AbpDbContext<BookStoreDbContext>,
IIdentityDbContext,
ITenantManagementDbContext
{ {
public DbSet<AppUser> Users { get; set; }
/* Add DbSet properties for your Aggregate Roots / Entities here. */ /* Add DbSet properties for your Aggregate Roots / Entities here. */
/* DbSet for entities from the replaced DbContexts */
public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options) public BookStoreDbContext(DbContextOptions<BookStoreDbContext> options)
: base(options) : base(options)
@ -112,457 +113,188 @@ public class BookStoreDbContext : AbpDbContext<BookStoreDbContext>
protected override void OnModelCreating(ModelBuilder builder) protected override void OnModelCreating(ModelBuilder builder)
{ {
base.OnModelCreating(builder); base.OnModelCreating(builder);
/* Configure the shared tables (with included modules) here */ /* Include modules to your migration db context */
builder.ConfigurePermissionManagement();
builder.Entity<AppUser>(b => builder.ConfigureSettingManagement();
{ builder.ConfigureBackgroundJobs();
//Sharing the same Users table with the IdentityUser builder.ConfigureAuditLogging();
b.ToTable(AbpIdentityDbProperties.DbTablePrefix + "Users"); builder.ConfigureIdentity();
builder.ConfigureIdentityServer();
b.ConfigureByConvention(); builder.ConfigureFeatureManagement();
b.ConfigureAbpUser(); builder.ConfigureTenantManagement();
/* Configure mappings for your additional properties /* Configure your own tables/entities here. Example: */
* Also see the MyProjectNameEntityExtensions class //builder.Entity<YourEntity>(b =>
*/ //{
}); // b.ToTable("YourEntities");
// b.ConfigureByConvention(); //auto configure for the base properties
/* Configure your own tables/entities inside the ConfigureBookStore method */ // //...
builder.ConfigureBookStore(); //});
} }
} }
```` ````
This simple `DbContext` class still needs some explanations: This `DbContext` class needs some explanations:
* It defines `[ReplaceDbContext]` attributes for `IIdentityDbContext` and `ITenantManagementDbContext` those replaces Identity and Tenant Management module's `DbContext`s by your `DbContext` on runtime. This allows us to easily perform LINQ queries by joining your entities with the entities (over the repositories) coming from those modules.
* It defines a `[ConnectionStringName]` attribute which tells ABP to always use the `Default` connection string for this `Dbcontext`. * It defines a `[ConnectionStringName]` attribute which tells ABP to always use the `Default` connection string for this `Dbcontext`.
* It inherits from the `AbpDbContext<T>` instead of the standard `DbContext` class. You can see the [EF Core integration](Entity-Framework-Core.md) document for more. For now, know that the `AbpDbContext<T>` base class implements some conventions of the ABP Framework to automate some common tasks for you. * It inherits from the `AbpDbContext<T>` instead of the standard `DbContext` class. You can see the [EF Core integration](Entity-Framework-Core.md) document for more. For now, know that the `AbpDbContext<T>` base class implements some conventions of the ABP Framework to automate some common tasks for you.
* It declares a `DbSet` property for the `AppUser` entity. `AppUser` shares the same table (named `AbpUsers` by default) with the `IdentityUser` entity of the [Identity module](Modules/Identity.md). The startup template provides this entity inside the application since we think that the User entity is generally needs to be customized in your application. * It declares `DbSet` properties for entities from the replaced `DbContext`s (by implementing the corresponding interfaces). These `DbSet` properties are not shown above (for the sake of brevity), but you can find in your application's code in a `region`.
* The constructor takes a `DbContextOptions<T>` instance. * The constructor takes a `DbContextOptions<T>` instance.
* It overrides the `OnModelCreating` method to define the EF Core mappings. * It overrides the `OnModelCreating` method to define the EF Core mappings.
* It first calls the the `base.OnModelCreating` method to let the ABP Framework to implement the base mappings for us. * It first calls the the `base.OnModelCreating` method to let the ABP Framework to implement the base mappings for us.
* It then configures the mapping for the `AppUser` entity. There is a special case for this entity (it shares a table with the Identity module), which will be explained in the next sections. * It then calls some `builder.ConfigureXXX()` methods for the used modules. This makes possible to add database mappings for these modules to this `DbContext`, so it creates the database tables of the modules when we add a new EF Core database migration.
* It finally calls the `builder.ConfigureBookStore()` extension method to configure other entities of your application. * You can configure the mappings for your own entities as commented in the example code. At this point, you can also change mappings for the modules you are using.
This design will be explained in more details after introducing the other database related projects.
##### Sharing the Mapping Code
First problem is that: A module uses its own `DbContext` which needs to the database mappings. The `ApplicationDbContext` also needs to the same mapping in order to create the database tables for this module. We definitely **don't want to duplicate** the mapping code. ### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path
The solution is to define an **extension method** (on the `ModelBuilder`) that can be called by both `DbContext` classes. So, all modules define such extension methods. As mentioned before, in the `.EntityFrameworkCore` project, we merge all the database mappings of all the modules (plus your application's mappings) to create a unified migration path.
For example, the `builder.ConfigureBackgroundJobs()` method call configures the database tables for the [Background Jobs module](Modules/Background-Jobs.md). The definition of this extension method is something like that: An alternative approach would be to allow each module to have its own migrations to maintain its database tables. While it seems more modular in the beginning, it has some drawbacks in practical:
````csharp * **EF Core migration system depends on the DBMS provider**. For example, if a module has created migrations for SQL Server, then you can not use this migration code for MySQL. It is not practical for a module to maintain migrations for all available DBMS providers. Leaving the migration to the application code (as explained in this document) allows you to **choose the DBMS in the application** code. If you can depend on a specific DBMS in your module, that's not an issue for you, however all pre-built ABP modules are DBMS agnostic.
public static class BackgroundJobsDbContextModelCreatingExtensions * It would be harder to **customize/enhance** the mapping and the resulting migration code, in the final application.
{ * It would be harder to track and **apply changes** to database when you use multiple modules.
public static void ConfigureBackgroundJobs(
this ModelBuilder builder,
Action<BackgroundJobsModelBuilderConfigurationOptions> optionsAction = null)
{
var options = new BackgroundJobsModelBuilderConfigurationOptions(
BackgroundJobsDbProperties.DbTablePrefix,
BackgroundJobsDbProperties.DbSchema
);
optionsAction?.Invoke(options); ## Using Multiple Databases
builder.Entity<BackgroundJobRecord>(b =>
{
b.ToTable(options.TablePrefix + "BackgroundJobs", options.Schema);
b.ConfigureCreationTime(); The default startup template is organized to use a **single database** used by all the modules and by your application. However, the ABP Framework and all the pre-built modules are designed so that **they can use multiple databases**. Each module can use its own database or you can group modules into a few databases.
b.ConfigureExtraProperties();
b.Property(x => x.JobName) This section will explain how to move Audit Logging, Setting Management and Permission Management module tables to a **second database** while the remaining modules continue to use the main ("Default") database.
.IsRequired()
.HasMaxLength(BackgroundJobRecordConsts.MaxJobNameLength);
//...
});
}
}
````
This extension method also gets options to change the database table prefix and schema for this module, but it is not important here.
The final application calls the extension methods inside the `ApplicationDbContext` class, so it can decide which modules are included in the database maintained by this `ApplicationDbContext`. If you want to create a second database and move some module tables to the second database, then you need to have a second `ApplicationDbContext` class which only calls the extension methods of the related modules. This topic will be detailed in the next sections. The resulting structure will be like the figure below:
The same `ConfigureBackgroundJobs` method is also called in the `DbContext` of the Background Jobs module: ![single-database-usage](images/multiple-database-usage.png)
````csharp ### Change the Connection Strings Section
[ConnectionStringName(BackgroundJobsDbProperties.ConnectionStringName)]
public class BackgroundJobsDbContext
: AbpDbContext<BackgroundJobsDbContext>, IBackgroundJobsDbContext
{
public DbSet<BackgroundJobRecord> BackgroundJobs { get; set; }
public BackgroundJobsDbContext(DbContextOptions<BackgroundJobsDbContext> options) First step is to change the connection string section inside all the `appsettings.json` files. Initially, it is like that:
: base(options)
{
} ````json
"ConnectionStrings": {
"Default": "Server=(LocalDb)\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True"
}
````
protected override void OnModelCreating(ModelBuilder builder) Change it as shown below:
{
base.OnModelCreating(builder);
//Reuse the same extension method! ````json
builder.ConfigureBackgroundJobs(); "ConnectionStrings": {
} "Default": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore;Trusted_Connection=True",
"AbpPermissionManagement": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpSettingManagement": "Server=(LocalDb)\\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpAuditLogging": "Server=(LocalDb)\MSSQLLocalDB;Database=BookStore_SecondDb;Trusted_Connection=True"
} }
```` ````
In this way, the mapping configuration of a module can be shared between `DbContext` classes. The code above is inside the related module NuGet package, so you don't care about it. Added **three more connection strings** for the related module to target the `BookStore_SecondDb` database (they are all the same). For example, `AbpPermissionManagement` is the connection string name used by the permission management module.
##### Reusing a Table of a Module
You may want to **reuse a table** of a depended module in your application. In this case, you have two options: The `AbpPermissionManagement` is a constant [defined](https://github.com/abpframework/abp/blob/97eaa6ff5a044f503465455c86332e5a277b077a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDbProperties.cs#L11) by the permission management module. ABP Framework [connection string selection system](Connection-Strings.md) selects this connection string for the permission management module if you define. If you don't define, it fallbacks to the `Default` connection string.
1. You can **directly use the entity** defined by the module (you can still [extend the entity](Customizing-Application-Modules-Extending-Entities.md) in some level). ### Create a Second DbContext
2. You can **create a new entity** mapping to the same database table.
###### Use the Entity Defined by a Module Defining the connection strings as explained above is enough **on runtime**. However, `BookStore_SecondDb` database doesn't exist yet. You need to create the database and the tables for the related modules.
Using an entity defined a module is pretty easy and standard. For example, Identity module defines the `IdentityUser` entity. You can inject the [repository](Repositories.md) for the `IdentityUser` and perform the standard repository operations for this entity. Example: Just like the main database, we want to use the EF Core Code First migration system to create and maintain the second database. So, create a new `DbContext` class inside the `.EntityFrameworkCore` project:
````csharp ````csharp
using System; using Microsoft.EntityFrameworkCore;
using System.Threading.Tasks; using Volo.Abp.AuditLogging.EntityFrameworkCore;
using Volo.Abp.DependencyInjection; using Volo.Abp.Data;
using Volo.Abp.Domain.Repositories; using Volo.Abp.EntityFrameworkCore;
using Volo.Abp.Identity; using Volo.Abp.PermissionManagement.EntityFrameworkCore;
using Volo.Abp.SettingManagement.EntityFrameworkCore;
namespace Acme.BookStore
namespace BookStore.EntityFrameworkCore
{ {
public class MyService : ITransientDependency [ConnectionStringName("AbpPermissionManagement")]
public class BookStoreSecondDbContext :
AbpDbContext<BookStoreSecondDbContext>
{ {
private readonly IRepository<IdentityUser, Guid> _identityUserRepository; public BookStoreSecondDbContext(
DbContextOptions<BookStoreSecondDbContext> options)
public MyService(IRepository<IdentityUser, Guid> identityUserRepository) : base(options)
{ {
_identityUserRepository = identityUserRepository;
} }
public async Task DoItAsync() protected override void OnModelCreating(ModelBuilder builder)
{ {
//Get all users base.OnModelCreating(builder);
var users = await _identityUserRepository.GetListAsync();
}
}
}
````
This example injects the `IRepository<IdentityUser, Guid>` (default repository) which defines the standard repository methods and implements the `IQueryable` interface. /* Include modules to your migration db context */
builder.ConfigurePermissionManagement();
> In addition, Identity module defines the `IIdentityUserRepository` (custom repository) that can also be injected and used by your application. `IIdentityUserRepository` provides additional custom methods for the `IdentityUser` entity while it does not implement the `IQueryable` interface. builder.ConfigureSettingManagement();
builder.ConfigureAuditLogging();
###### Create a New Entity
Working with an entity of a module is easy if you want to use the entity as is. However, you may want to define your own entity class and map to the same database table in the following cases;
* You want to **add a new field** to the table and map it to a property in the entity. You can't use the module's entity since it doesn't have the related property.
* You want to **use a subset of the table fields**. You don't want to access to all properties of the entity and hide the unrelated properties (from a security perspective or just by design).
* You don't want to directly **depend on** a module entity class.
In any case, the progress is same. Assume that you want to create an entity, named `AppRole`, mapped to the same table of the `IdentityRole` entity of the [Identity module](Modules/Identity.md).
Here, we will show the implementation, then **will discuss the limitations** of this approach.
First, create a new `AppRole` class in your `.Domain` project:
````csharp
using System;
using Volo.Abp.Domain.Entities;
using Volo.Abp.MultiTenancy;
namespace Acme.BookStore.Roles
{
public class AppRole : AggregateRoot<Guid>, IMultiTenant
{
// Properties shared with the IdentityRole class
public Guid? TenantId { get; private set; }
public string Name { get; private set; }
//Additional properties
public string Title { get; set; }
private AppRole()
{
} }
} }
} }
```` ````
* It's inherited from [the `AggregateRoot<Guid>` class](Entities.md) and implements [the `IMultiTenant` interface](Multi-Tenancy.md) because the `IdentityRole` also does the same. > `[ConnectionStringName(...)]` attribute is important here and tells to the ABP Framework which connection string should be used for this `DbContext`. We've used `AbpPermissionManagement`, but all are the same.
* You can add any properties defined by the `IdentityRole` entity. This examples add only the `TenantId` and `Name` properties since we only need them here. You can make the setters private (like in this example) to prevent changing Identity module's properties accidently.
* You can add custom (additional) properties. This example adds the `Title` property.
* The **constructor is private**, so it is not allowed to directly create a new `AppRole` entity. Creating a role is a responsibility of the Identity module. You can query roles, set/update your custom properties, but you should not create or delete a role in your code, as a best practice (while there is nothing restricts you).
Now, it is time to define the EF Core mappings. Open the `DbContext` of your application (`BookStoreDbContext` in this sample) and add the following property:
````csharp
public DbSet<AppRole> Roles { get; set; }
````
Then configure the mapping inside the `OnModelCreating` method (after calling the `base.OnModelCreating(builder)`): We need to register this `BookStoreSecondDbContext` class to the dependency injection system. Open the `BookStoreEntityFrameworkCoreModule` class in the `BookStore.EntityFrameworkCore` project and add the following line into the `ConfigureServices` method:
````csharp ````csharp
protected override void OnModelCreating(ModelBuilder builder) context.Services.AddAbpDbContext<BookStoreSecondDbContext>();
{
base.OnModelCreating(builder);
/* Configure the shared tables (with included modules) here */
//CONFIGURE THE AppRole ENTITY
builder.Entity<AppRole>(b =>
{
b.ToTable("AbpRoles");
b.ConfigureByConvention();
b.Property(x => x.Title).HasMaxLength(128);
});
...
/* Configure your own tables/entities inside the ConfigureBookStore method */
builder.ConfigureBookStore();
}
```` ````
We added the following lines: We should also create a **Design Time Db Factory** class, that is used by the EF Core tooling (by `Add-Migration` and `Update-Database` PCM commands for example):
````csharp ````csharp
builder.Entity<AppRole>(b => using System.IO;
{ using Microsoft.EntityFrameworkCore;
b.ToTable("AbpRoles"); using Microsoft.EntityFrameworkCore.Design;
b.ConfigureByConvention(); using Microsoft.Extensions.Configuration;
b.Property(x => x.Title).HasMaxLength(128);
});
````
* It maps to the same `AbpRoles` table shared with the `IdentityRole` entity. namespace BookStore.EntityFrameworkCore
* `ConfigureByConvention()` configures the standard/base properties (like `TenantId`) and recommended to always call it.
You've configured the custom property for your `DbContext` that is used by your application on the runtime. We also need to configure the `MigrationsDbContext`.
Instead of directly changing the `MigrationsDbContext`, we **should** use the entity extension system of the ABP Framework. Find the `YourProjectNameEfCoreEntityExtensionMappings` class in the `.EntityFrameworkCore` project of your solution (`BookStoreEfCoreEntityExtensionMappings` for this example) and change it as shown below:
````csharp
public static class MyProjectNameEntityExtensions
{ {
private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); /* This class is needed for EF Core console commands
* (like Add-Migration and Update-Database commands) */
public static void Configure() public class BookStoreSecondDbContextFactory
: IDesignTimeDbContextFactory<BookStoreSecondDbContext>
{ {
OneTimeRunner.Run(() => public BookStoreSecondDbContext CreateDbContext(string[] args)
{ {
ObjectExtensionManager.Instance var configuration = BuildConfiguration();
.MapEfCoreProperty<IdentityRole, string>( var builder = new DbContextOptionsBuilder<BookStoreSecondDbContext>()
"Title", .UseSqlServer(configuration.GetConnectionString("AbpPermissionManagement"));
(entityBuilder, propertyBuilder) => return new BookStoreSecondDbContext(builder.Options);
{ }
propertyBuilder.HasMaxLength(128);
}
);
});
}
}
````
> Instead of hard-coded "Title" string, we suggest to use `nameof(AppRole.Title)` or use a constant string.
`ObjectExtensionManager` is used to add properties to existing entities. Since `ObjectExtensionManager.Instance` is a static instance (singleton), we should call it once. `OneTimeRunner` is a simple utility class defined by the ABP Framework.
See the [EF Core integration documentation](Entity-Framework-Core.md) for more about the entity extension system.
> We've repeated a similar database mapping code, like `HasMaxLength(128)`, in both classes.
Now, you can add a new EF Core database migration using the standard `Add-Migration` command in the Package Manager Console (remember to select `.EntityFrameworkCore` as the Default Project in the PMC and make sure that the `.Web` project is still the startup project):
![pmc-add-migration-role-title](images/pmc-add-migration-role-title.png)
This command will create a new code first migration class as shown below:
````csharp
public partial class Added_Title_To_Roles : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Title",
table: "AbpRoles",
maxLength: 128,
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Title",
table: "AbpRoles");
}
}
````
All done! Just run the `Update-Database` command in the PMC or run the `.DbMigrator` project in your solution to apply changes to database.
Now, you can work with the `AppRole` entity just like any other entity of your application. An example [application service](Application-Services.md) that queries and updates roles:
````csharp
public class AppRoleAppService : ApplicationService, IAppRoleAppService
{
private readonly IRepository<AppRole, Guid> _appRoleRepository;
public AppRoleAppService(IRepository<AppRole, Guid> appRoleRepository)
{
_appRoleRepository = appRoleRepository;
}
public async Task<List<AppRoleDto>> GetListAsync() private static IConfigurationRoot BuildConfiguration()
{ {
var roles = await _appRoleRepository.GetListAsync(); var builder = new ConfigurationBuilder()
.SetBasePath(Path.Combine(Directory.GetCurrentDirectory(), "../BookStore.DbMigrator/"))
return roles .AddJsonFile("appsettings.json", optional: false);
.Select(r => new AppRoleDto
{
Id = r.Id,
Name = r.Name,
Title = r.Title
})
.ToList();
}
public async Task UpdateTitleAsync(Guid id, string title) return builder.Build();
{ }
var role = await _appRoleRepository.GetAsync(id);
role.Title = title;
await _appRoleRepository.UpdateAsync(role);
} }
} }
```` ````
There are some **limitations** of creating a new entity and mapping it to a table of a depended module: Now, you can open the Package Manager Console, select the `.EntityFrameworkCore` project as the default project (make sure the `.Web` project is still the startup project) and run the following command:
* Your **custom properties must be nullable**. For example, `AppRole.Title` was nullable here. Otherwise, Identity module throws exception because it doesn't know and can not fill the Title when it inserts a new role to the database.
* As a good practice, you should not update the **properties defined by the module**, especially if it requires a business logic. You typically want to manage your own properties.
##### Alternative Approaches
Instead of creating a new entity class to add a custom property, you can use the following approaches.
###### Using the ExtraProperties
All entities derived from the `AggregateRoot ` class can store name-value pairs in their `ExtraProperties` property (because they implement the `IHasExtraProperties` interface), which is a `Dictionary<string, object>` serialized to JSON in the database table. So, you can add values to this dictionary and query again without changing the entity.
For example, you can store query the title Property inside an `IdentityRole` instead of creating a new entity. Example:
````csharp ````bash
public class IdentityRoleExtendingService : ITransientDependency Add-Migration "Initial" -OutputDir "SecondDbMigrations" -Context BookStoreSecondDbContext
{
private readonly IIdentityRoleRepository _identityRoleRepository;
public IdentityRoleExtendingService(IIdentityRoleRepository identityRoleRepository)
{
_identityRoleRepository = identityRoleRepository;
}
public async Task<string> GetTitleAsync(Guid id)
{
var role = await _identityRoleRepository.GetAsync(id);
return role.GetProperty<string>("Title");
}
public async Task SetTitleAsync(Guid id, string newTitle)
{
var role = await _identityRoleRepository.GetAsync(id);
role.SetProperty("Title", newTitle);
await _identityRoleRepository.UpdateAsync(role);
}
}
```` ````
* `GetProperty` and `SetProperty` methods are shortcuts to get and set a value in the `role.ExtraProperties` dictionary and they are the recommended way to work with the extra properties. This will add a `SecondDbMigrations` folder in the `.EntityFrameworkCore` project and a migration class inside it. `OutputDir` and `Context` parameters are required since we currently have two `DbContext` class and two migrations folder in the same project.
In this way, you can easily attach any type of value to an entity of a depended module. However, there are some drawbacks of this usage:
* All the extra properties are stored as **a single JSON object** in the database. They are not stored as new table fields, as you may expect. Creating database table indexes and using SQL queries against these properties will be harder compared to simple table fields.
* Property names are strings, so they are **not type safe**. It is recommended to define constants for these kind of properties to prevent typo errors.
###### Using the Entity Extensions System
Entity extension system solves the main problem of the extra properties: It can store an extra property in a **standard table field** in the database.
All you need to do is to use the `ObjectExtensionManager` to define the extra property as explained above, in the `AppRole` example. Then you can continue to use the same `GetProperty` and `SetProperty` methods defined above to get/set the related property on the entity, but this time stored as a separate field in the database.
See the [entity extension system](Customizing-Application-Modules-Extending-Entities.md) for details.
###### Creating a New Table
Instead of creating a new entity and mapping to the same table, you can also create **your own table** to store your properties. You typically duplicate some values of the original entity. For example, you can add `Name` field to your own table which is a duplication of the `Name` field in the original table.
In this case, you don't deal with migration problems, however you need to deal with the problems of data duplication. When the duplicated value changes, you should reflect the same change in your table. You can use local or distributed [event bus](Event-Bus.md) to subscribe to the change events for the original entity. This is the recommended way of depending on a microservice's data from another microservice, especially if they have separate physical databases (you can search on the web on data sharing on a microservice design, it is a wide topic to cover here).
> See the "[extending entities](Customizing-Application-Modules-Extending-Entities.md)" guide for more details on extending entities, including data duplication and synchronization tips.
#### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path You can now run the following command to create the database and the tables inside it:
As mentioned before, `.EntityFrameworkCore` merges all the database mappings of all the modules (plus your application's mappings) to create a unified migration path. ````bash
Update-Database -Context BookStoreSecondDbContext
An alternative approach would be to allow each module to have its own migrations to maintain its database tables. While it seems more module in the beginning, it has some important drawbacks:
* **EF Core migration system depends on the DBMS provider**. For example, if a module has created migrations for SQL Server, then you can not use this migration code for MySQL. It is not practical for a module to maintain migrations for all available DBMS providers. Leaving the migration to the application code (as explained in this document) allows you to **choose the DBMS in the application** code.
* It would be harder or impossible to **share a table** between modules or **re-use a table** of a module in your application. Because EF Core migration system can not handle it and will throw exceptions like "Table XXX is already exists in the database".
* It would be harder to **customize/enhance** the mapping and the resulting migration code.
* It would be harder to track and **apply changes** to database when you use multiple modules.
## Using Multiple Databases
The default startup template is organized to use a single database used by all the modules and by your application. However, the ABP Framework and all the pre-built modules are designed so that **they can use multiple databases**. Each module can use its own database or you can group modules into a few databases.
This section will explain how to move Audit Logging, Setting Management and Permission Management module tables to a **second database** while the remaining modules continue to use the main ("Default") database.
The resulting structure will be like the figure below:
![single-database-usage](images/multiple-database-usage.png)
### Change the Connection Strings Section
First step is to change the connection string section inside all the `appsettings.json` files. Initially, it is like that:
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True"
}
```` ````
Change it as shown below: A new database, named `BookStore_SecondDb` should be created.
````json
"ConnectionStrings": {
"Default": "Server=localhost;Database=BookStore;Trusted_Connection=True",
"AbpPermissionManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpSettingManagement": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True",
"AbpAuditLogging": "Server=localhost;Database=BookStore_SecondDb;Trusted_Connection=True"
}
````
Added **three more connection strings** for the related module to target the `BookStore_SecondDb` database (they are all same). For example, `AbpPermissionManagement` is the connection string for the permission management module.
The `AbpPermissionManagement` is a constant [defined](https://github.com/abpframework/abp/blob/97eaa6ff5a044f503465455c86332e5a277b077a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain/Volo/Abp/PermissionManagement/AbpPermissionManagementDbProperties.cs#L11) by the permission management module. ABP Framework [connection string selection system](Connection-Strings.md) selects this connection string for the permission management module if you define. If you don't define, it fallbacks to the `Default` connection string.
### Remove Modules from the Main Database ### Remove Modules from the Main Database
We've **created a second database** contains tables for the Audit Logging, Permission Management and Setting Management modules. So, we should **delete these tables from the main database**. It is pretty easy. We've **created a second database** that contains tables for the Audit Logging, Permission Management and Setting Management modules. So, we should **delete these tables from the main database**. It is pretty easy.
First, remove the following lines from the `MigrationsDbContext` class (`BookStoreMigrationsDbContext` for this example): First, remove the following lines from the `BookStoreDbContext` class:
````csharp ````csharp
builder.ConfigurePermissionManagement(); builder.ConfigurePermissionManagement();
@ -573,7 +305,7 @@ builder.ConfigureAuditLogging();
Open the Package Manager Console, select the `.EntityFrameworkCore` as the Default project (make sure that the `.Web` project is still the startup project) and run the following command: Open the Package Manager Console, select the `.EntityFrameworkCore` as the Default project (make sure that the `.Web` project is still the startup project) and run the following command:
```` ````
Add-Migration "Removed_Audit_Setting_Permission_Modules" Add-Migration "Removed_Audit_Setting_Permission_Modules" -Context BookStoreDbContext
```` ````
This command will create a new migration class as shown below: This command will create a new migration class as shown below:
@ -611,88 +343,148 @@ Be careful in this step:
* If you have a **live system**, then you should care about the **data loss**. You need to move the table contents to the second database before deleting the tables. * If you have a **live system**, then you should care about the **data loss**. You need to move the table contents to the second database before deleting the tables.
* If you **haven't started** your project yet, you can consider to **remove all the migrations** and re-create the initial one to have a cleaner migration history. * If you **haven't started** your project yet, you can consider to **remove all the migrations** and re-create the initial one to have a cleaner migration history.
Run the `Update-Database` command to delete the tables from your main database. Run the following command to delete the tables from your main database:
````bash
Update-Database -Context BookStoreDbContext
````
Notice that you've also **deleted some initial seed data** (for example, permission grants for the admin role) if you haven't copied it to the new database. If you run the application, you may not login anymore. The solution is simple: **Re-run the `.DbMigrator` console application** in your solution, it will seed the new database. Notice that you've also **deleted some initial seed data** (for example, permission grants for the admin role) if you haven't copied it to the new database. If you run the application, you may not login anymore. The solution is simple: **Re-run the `.DbMigrator` console application** in your solution, it will seed the new database.
### Automate the Second Database Schema Migration ### Automate the Second Database Schema Migration
`.DbMigrator` console application can run the database seed code across multiple databases, without any additional configuration. However, it can not run the EF Core Code First Migrations inside the second database migration project. Now, you will see how to configure the console migration application to handle both databases. `.DbMigrator` console application can run the database seed code across multiple databases, without any additional configuration. However, it can not apply the EF Core Code First Migrations for the database of the `BookStoreSecondDbContext`. Now, you will see how to configure the console migration application to handle both databases.
#### Implementing the IBookStoreDbSchemaMigrator
`EntityFrameworkCoreBookStoreDbSchemaMigrator` class inside the `Acme.BookStore.EntityFrameworkCore` project is responsible to migrate the database schema for the `BookStoreMigrationsDbContext`. It should be like that: `EntityFrameworkCoreBookStoreDbSchemaMigrator` class inside the `Acme.BookStore.EntityFrameworkCore` project is responsible to migrate the database schema for the `BookStoreMigrationsDbContext`. It should be like that:
````csharp ````csharp
[Dependency(ReplaceServices = true)] using System;
public class EntityFrameworkCoreBookStoreDbSchemaMigrator using System.Threading.Tasks;
: IBookStoreDbSchemaMigrator, ITransientDependency using Microsoft.EntityFrameworkCore;
{ using Microsoft.Extensions.DependencyInjection;
private readonly IServiceProvider _serviceProvider; using BookStore.Data;
using Volo.Abp.DependencyInjection;
public EntityFrameworkCoreBookStoreDbSchemaMigrator( namespace BookStore.EntityFrameworkCore
IServiceProvider serviceProvider) {
public class EntityFrameworkCoreBookStoreDbSchemaMigrator
: IBookStoreDbSchemaMigrator, ITransientDependency
{ {
_serviceProvider = serviceProvider; private readonly IServiceProvider _serviceProvider;
}
public async Task MigrateAsync() public EntityFrameworkCoreBookStoreDbSchemaMigrator(
{ IServiceProvider serviceProvider)
/* We are intentionally resolving the BookStoreMigrationsDbContext {
* from IServiceProvider (instead of directly injecting it) _serviceProvider = serviceProvider;
* to properly get the connection string of the current tenant in the }
* current scope.
*/ public async Task MigrateAsync()
{
await _serviceProvider /* We intentionally resolving the BookStoreDbContext
.GetRequiredService<BookStoreMigrationsDbContext>() * from IServiceProvider (instead of directly injecting it)
.Database * to properly get the connection string of the current tenant in the
.MigrateAsync(); * current scope.
*/
await _serviceProvider
.GetRequiredService<BookStoreDbContext>()
.Database
.MigrateAsync();
}
} }
} }
```` ````
It implements the `IBookStoreDbSchemaMigrator` and **replaces existing services** (see the first line). Add the following code inside the `MigrateAsync` method:
Remove the `[Dependency(ReplaceServices = true)]` line, because we will have two implementations of this interface and we want to use both. We don't want to replace one of them. ````csharp
await _serviceProvider
.GetRequiredService<BookStoreSecondDbContext>()
.Database
.MigrateAsync();
````
Create a copy of this class inside the new migration project (`Acme.BookStore.EntityFrameworkCore.ForSecondDb`), but use the `BookStoreSecondMigrationsDbContext`. Example implementation: So, the `MigrateAsync` method should look like the following:
````csharp ````csharp
public class EntityFrameworkCoreSecondBookStoreDbSchemaMigrator public async Task MigrateAsync()
: IBookStoreDbSchemaMigrator, ITransientDependency
{ {
private readonly IServiceProvider _serviceProvider; /* We intentionally resolving the BookStoreDbContext
* from IServiceProvider (instead of directly injecting it)
* to properly get the connection string of the current tenant in the
* current scope.
*/
await _serviceProvider
.GetRequiredService<BookStoreDbContext>()
.Database
.MigrateAsync();
await _serviceProvider
.GetRequiredService<BookStoreSecondDbContext>()
.Database
.MigrateAsync();
}
````
public EntityFrameworkCoreSecondBookStoreDbSchemaMigrator( That's all. You can now run the `.DbMigrator` application to migrate & seed the databases. To test, you can delete both databases and run the `.DbMigrator` application again to see if it creates both of the databases.
IServiceProvider serviceProvider)
{ ### Fixing the Tests
_serviceProvider = serviceProvider;
} Creating a new DbContext will break the integration tests. It is easy to fix. Open the `BookStoreEntityFrameworkCoreTestModule` class in the `BookStore.EntityFrameworkCore.Tests` project, find the `CreateDatabaseAndGetConnection` method. It should be like that:
````csharp
private static SqliteConnection CreateDatabaseAndGetConnection()
{
var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<BookStoreDbContext>()
.UseSqlite(connection)
.Options;
public async Task MigrateAsync() using (var context = new BookStoreDbContext(options))
{ {
/* We are intentionally resolving the BookStoreSecondMigrationsDbContext context.GetService<IRelationalDatabaseCreator>().CreateTables();
* from IServiceProvider (instead of directly injecting it)
* to properly get the connection string of the current tenant in the
* current scope.
*/
await _serviceProvider
.GetRequiredService<BookStoreSecondMigrationsDbContext>()
.Database
.MigrateAsync();
} }
return connection;
} }
```` ````
> Name of this class is important for [dependency injection](Dependency-Injection.md). It should end with `BookStoreDbSchemaMigrator` to be injectable by `IBookStoreDbSchemaMigrator` reference. Change it as the following:
````csharp
private static SqliteConnection CreateDatabaseAndGetConnection()
{
var connection = new SqliteConnection("Data Source=:memory:");
connection.Open();
var options = new DbContextOptionsBuilder<BookStoreDbContext>()
.UseSqlite(connection)
.Options;
using (var context = new BookStoreDbContext(options))
{
context.GetService<IRelationalDatabaseCreator>().CreateTables();
}
// Add the following code --------------
var optionsForSecondDb = new DbContextOptionsBuilder<BookStoreSecondDbContext>()
.UseSqlite(connection)
.Options;
We, now, have two implementations of the `IBookStoreDbSchemaMigrator` interface, each one responsible to migrate the related database schema. using (var context = new BookStoreSecondDbContext(optionsForSecondDb))
{
context.GetService<IRelationalDatabaseCreator>().CreateTables();
}
//--------------------------------------
#### Run the Database Migrator! return connection;
}
````
You can run the `.DbMigrator` application to migrate & seed the databases. To test, you can delete both databases and run the `.DbMigrator` application again to see if it creates both of the databases. Integration tests now will work. I've used the same database in the tests to keep it simple.
## Separating Host & Tenant Database Schemas ## Separating Host & Tenant Database Schemas
@ -700,7 +492,7 @@ In a multi-tenant solution, you may want to separate your database schemas, so h
Some pre-built ABP modules are related only with the host side, like the [Tenant Management](Modules/Tenant-Management.md) module. So, in the tenant `DbContext` class you don't call `modelBuilder.ConfigureTenantManagement()` and that's all. Some pre-built ABP modules are related only with the host side, like the [Tenant Management](Modules/Tenant-Management.md) module. So, in the tenant `DbContext` class you don't call `modelBuilder.ConfigureTenantManagement()` and that's all.
However, some modules, like the [Identity](Modules/Identity.md) module, is both used in host and tenant sides. It stores tenant users in the tenant database and host users in the host database. However, it stores some entities, like `IdentityClaimType`, only in the host side. In this case, you don't want to add these tables in the tenant database, even if they are not used and will always be empty. Some modules, like the [Identity](Modules/Identity.md) module, is both used in host and tenant sides. It stores tenant users in the tenant database and host users in the host database. However, it stores some entities, like `IdentityClaimType`, only in the host side. In this case, you don't want to add these tables in the tenant database, even if they are not used and will always be empty for tenants.
ABP provides a simple way to set the multi-tenancy side for a `DbContext`, so the modules can check it and decide to map tables to the database, or not. ABP provides a simple way to set the multi-tenancy side for a `DbContext`, so the modules can check it and decide to map tables to the database, or not.
@ -712,6 +504,7 @@ public class MyTenantDbContext : AbpDbContext<MyTenantDbContext>
modelBuilder.SetMultiTenancySide(MultiTenancySides.Tenant); modelBuilder.SetMultiTenancySide(MultiTenancySides.Tenant);
base.OnModelCreating(modelBuilder); base.OnModelCreating(modelBuilder);
modelBuilder.ConfigureIdentity(); modelBuilder.ConfigureIdentity();
modelBuilder.ConfigureFeatureManagement(); modelBuilder.ConfigureFeatureManagement();
modelBuilder.ConfigureAuditLogging(); modelBuilder.ConfigureAuditLogging();
@ -761,4 +554,4 @@ This document explains how to split your databases and manage your database migr
## Source Code ## Source Code
You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo). However, you need to read and understand this document in order to understand the example project's source code. You can find the source code of the example project referenced by this document [here](https://github.com/abpframework/abp-samples/tree/master/EfCoreMigrationDemo). You can also find the changes explained in this document as a [single commit](https://github.com/abpframework/abp-samples/pull/95/commits/c2ffd76175e0a6fdfcf6477bbaea23dc2793fedd).
Loading…
Cancel
Save