From 31ac3aafaf88996ce35392a1d1b946e3be95af31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Fri, 28 Feb 2020 11:55:14 +0300 Subject: [PATCH] Added sections to Entity-Framework-Core-Migrations --- docs/en/Entity-Framework-Core-Migrations.md | 116 +++++++++++++++++- .../Roles/IdentityRoleExtendingService.cs | 21 ---- .../Roles/IdentityRoleExtendingService.cs | 34 +++++ .../IdentityRoleExtendingService_Tests.cs | 29 +++++ 4 files changed, 174 insertions(+), 26 deletions(-) delete mode 100644 samples/EfCoreMigrationDemo/src/Acme.BookStore.Application/Roles/IdentityRoleExtendingService.cs create mode 100644 samples/EfCoreMigrationDemo/src/Acme.BookStore.Domain/Roles/IdentityRoleExtendingService.cs create mode 100644 samples/EfCoreMigrationDemo/test/Acme.BookStore.Domain.Tests/Roles/IdentityRoleExtendingService_Tests.cs diff --git a/docs/en/Entity-Framework-Core-Migrations.md b/docs/en/Entity-Framework-Core-Migrations.md index 395db99504..d3dd077411 100644 --- a/docs/en/Entity-Framework-Core-Migrations.md +++ b/docs/en/Entity-Framework-Core-Migrations.md @@ -332,7 +332,7 @@ namespace Acme.BookStore.Roles // Properties shared with the IdentityRole class public Guid? TenantId { get; private set; } - public virtual string Name { get; protected internal set; } + public string Name { get; private set; } //Additional properties @@ -347,7 +347,7 @@ namespace Acme.BookStore.Roles ```` * It's inherited from [the `AggregateRoot` class](Entities.md) and implements [the `IMultiTenant` interface](Multi-Tenancy.md) because the `IdentityRole` also does 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 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 provide, 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). @@ -400,7 +400,7 @@ builder.Entity(b => * It maps to the same `AbpRoles` table shared with the `IdentityRole` entity. * `ConfigureByConvention()` configures the standard/base properties (like `TenantId`) and recommended to always call it. -`ConfigureCustomRoleProperties()` has not exists yet. Define it inside the `BookStoreDbContextModelCreatingExtensions` class (near to your `DbContext` in the `EntityFrameworkCore` project): +`ConfigureCustomRoleProperties()` has not exists yet. Define it inside the `BookStoreDbContextModelCreatingExtensions` class (near to your `DbContext` in the `.EntityFrameworkCore` project): ````csharp public static void ConfigureCustomRoleProperties(this EntityTypeBuilder b) @@ -413,7 +413,9 @@ public static void ConfigureCustomRoleProperties(this EntityTypeBuilder _appRoleRepository; + + public AppRoleAppService(IRepository appRoleRepository) + { + _appRoleRepository = appRoleRepository; + } + + public async Task> GetListAsync() + { + var roles = await _appRoleRepository.GetListAsync(); + + return roles + .Select(r => new AppRoleDto + { + Id = r.Id, + Name = r.Name, + Title = r.Title + }) + .ToList(); + } + + public async Task UpdateTitleAsync(Guid id, string title) + { + 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: + +* 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 manage your own properties. + +##### Alternative Approaches + +Instead of creating an entity 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, which is a `Dictionary` 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 +public class IdentityRoleExtendingService : ITransientDependency +{ + private readonly IIdentityRoleRepository _identityRoleRepository; + + public IdentityRoleExtendingService(IIdentityRoleRepository identityRoleRepository) + { + _identityRoleRepository = identityRoleRepository; + } + + public async Task GetTitleAsync(Guid id) + { + var role = await _identityRoleRepository.GetAsync(id); + + return role.GetProperty("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. + +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 can expect. Creating indexes and using SQL queries against this properties will be harder compared to simple table fields. +* Property names are string, so they are not type safe. It is recommended to define constants for these kind of properties to prevent typo errors. + +###### Creating a New Table + +Instead of creating a new entity and mapping to the same table, you can 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. + +#### Discussion of an Alternative Scenario: Every Module Manages Its Own Migration Path + +As mentioned before, `.EntityFrameworkCore.DbMigrations` merges all the database mappings of all the modules (plus the application mappings) to create a unified migration path. + +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 TODO \ No newline at end of file diff --git a/samples/EfCoreMigrationDemo/src/Acme.BookStore.Application/Roles/IdentityRoleExtendingService.cs b/samples/EfCoreMigrationDemo/src/Acme.BookStore.Application/Roles/IdentityRoleExtendingService.cs deleted file mode 100644 index 8b0f6aca69..0000000000 --- a/samples/EfCoreMigrationDemo/src/Acme.BookStore.Application/Roles/IdentityRoleExtendingService.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using System.Threading.Tasks; -using Volo.Abp.DependencyInjection; -using Volo.Abp.Identity; - -namespace Acme.BookStore.Roles -{ - public class IdentityRoleExtendingService : ITransientDependency - { - private readonly IIdentityRoleRepository _identityRoleRepository; - - public IdentityRoleExtendingService(IIdentityRoleRepository identityRoleRepository) - { - _identityRoleRepository = identityRoleRepository; - } - - - } -} diff --git a/samples/EfCoreMigrationDemo/src/Acme.BookStore.Domain/Roles/IdentityRoleExtendingService.cs b/samples/EfCoreMigrationDemo/src/Acme.BookStore.Domain/Roles/IdentityRoleExtendingService.cs new file mode 100644 index 0000000000..ea258ed39d --- /dev/null +++ b/samples/EfCoreMigrationDemo/src/Acme.BookStore.Domain/Roles/IdentityRoleExtendingService.cs @@ -0,0 +1,34 @@ +using System; +using System.Threading.Tasks; +using Volo.Abp.Data; +using Volo.Abp.DependencyInjection; +using Volo.Abp.Identity; + +namespace Acme.BookStore.Roles +{ + public class IdentityRoleExtendingService : ITransientDependency + { + private readonly IIdentityRoleRepository _identityRoleRepository; + + public IdentityRoleExtendingService(IIdentityRoleRepository identityRoleRepository) + { + _identityRoleRepository = identityRoleRepository; + } + + public async Task GetTitleAsync(Guid id) + { + var role = await _identityRoleRepository.GetAsync(id); + + return role.GetProperty("Title"); + } + + public async Task SetTitleAsync(Guid id, string newTitle) + { + var role = await _identityRoleRepository.GetAsync(id); + + role.SetProperty("Title", newTitle); + + await _identityRoleRepository.UpdateAsync(role); + } + } +} diff --git a/samples/EfCoreMigrationDemo/test/Acme.BookStore.Domain.Tests/Roles/IdentityRoleExtendingService_Tests.cs b/samples/EfCoreMigrationDemo/test/Acme.BookStore.Domain.Tests/Roles/IdentityRoleExtendingService_Tests.cs new file mode 100644 index 0000000000..704a731775 --- /dev/null +++ b/samples/EfCoreMigrationDemo/test/Acme.BookStore.Domain.Tests/Roles/IdentityRoleExtendingService_Tests.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Shouldly; +using Volo.Abp.Identity; +using Xunit; + +namespace Acme.BookStore.Roles +{ + public class IdentityRoleExtendingService_Tests : BookStoreDomainTestBase + { + private readonly IdentityRoleExtendingService _identityRoleExtendingService; + private readonly IIdentityRoleRepository _identityRoleRepository; + + public IdentityRoleExtendingService_Tests() + { + _identityRoleExtendingService = GetRequiredService(); + _identityRoleRepository = GetRequiredService(); + } + + [Fact] + public async Task Should_Set_And_Get_ExtraProperties() + { + var adminRole = await _identityRoleRepository.FindByNormalizedNameAsync("ADMIN"); + + await _identityRoleExtendingService.SetTitleAsync(adminRole.Id, "New Title!"); + + (await _identityRoleExtendingService.GetTitleAsync(adminRole.Id)).ShouldBe("New Title!"); + } + } +}