diff --git a/docs/Best-Practices/Entity-Framework-Core-Integration.md b/docs/Best-Practices/Entity-Framework-Core-Integration.md new file mode 100644 index 0000000000..2e404e975b --- /dev/null +++ b/docs/Best-Practices/Entity-Framework-Core-Integration.md @@ -0,0 +1,209 @@ +## Entity Framework Core Integration Best Practices + +> See [Entity Framework Core Integration document](../Entity-Framework-Core.md) for the basics of the EF Core integration. + +- Do define a separated `DbContext` interface and class for each module. + +### DbContext Interface + +- **Do** define an **interface** for the `DbContext` that inherits from `IEfCoreDbContext`. +- **Do** add a `ConnectionStringName` **attribute** to the `DbContext` interface. +- **Do** add `DbSet` **properties** to the `DbContext` interface for only aggregate roots. Example: + +````C# +[ConnectionStringName("AbpIdentity")] +public interface IIdentityDbContext : IEfCoreDbContext +{ + DbSet Users { get; set; } + DbSet Roles { get; set; } +} +```` + +### DbContext class + +* **Do** inherit the `DbContext` from the `AbpDbContext` class. +* **Do** add a `ConnectionStringName` attribute to the `DbContext` class. +* **Do** implement the corresponding `interface` for the `DbContext` class. Example: + +````C# +[ConnectionStringName("AbpIdentity")] +public class IdentityDbContext : AbpDbContext, IIdentityDbContext +{ + public DbSet Users { get; set; } + public DbSet Roles { get; set; } + + public IdentityDbContext(DbContextOptions options) + : base(options) + { + + } + + //code omitted for brevity +} +```` + +### Table Prefix and Schema + +- **Do** add static `TablePrefix` and `Schema` **properties** to the `DbContext` class. Set default value from a constant. Example: + +````C# +public static string TablePrefix { get; set; } = AbpIdentityConsts.DefaultDbTablePrefix; +public static string Schema { get; set; } = AbpIdentityConsts.DefaultDbSchema; +```` + + - **Do** always use a short `TablePrefix` value for a module to create **unique table names** in a shared database. `Abp` table prefix is reserved for ABP core modules. + - **Do** set `Schema` to `null` as default. + +### Model Mapping + +- **Do** explicitly **configure all entities** by overriding the `OnModelCreating` method of the `DbContext`. Example: + +````C# +protected override void OnModelCreating(ModelBuilder builder) +{ + base.OnModelCreating(builder); + + builder.ConfigureIdentity(options => + { + options.TablePrefix = TablePrefix; + options.Schema = Schema; + }); +} +```` + +- **Do not** configure model directly in the `OnModelCreating` method. Instead, create an **extension method** for `ModelBuilder`. Use Configure*ModuleName* as the method name. Example: + +````C# +public static class IdentityDbContextModelBuilderExtensions +{ + public static void ConfigureIdentity( + [NotNull] this ModelBuilder builder, + Action optionsAction = null) + { + Check.NotNull(builder, nameof(builder)); + + var options = new IdentityModelBuilderConfigurationOptions(); + optionsAction?.Invoke(options); + + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "Users", options.Schema); + //code omitted for brevity + }); + + builder.Entity(b => + { + b.ToTable(options.TablePrefix + "UserClaims", options.Schema); + //code omitted for brevity + }); + + //code omitted for brevity + } +} +```` + +* **Do** create a **configuration options** class by inheriting from the `ModelBuilderConfigurationOptions`. Example: + +````C# +public class IdentityModelBuilderConfigurationOptions : ModelBuilderConfigurationOptions +{ + public IdentityModelBuilderConfigurationOptions() + : base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema) + { + } +} +```` + +### Repository Implementation + +- **Do** **inherit** the repository from the `EfCoreRepository` class and implement the corresponding repository interface. Example: + +````C# +public class EfCoreIdentityUserRepository + : EfCoreRepository, IIdentityUserRepository +{ + public EfCoreIdentityUserRepository( + IDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } +} +```` + +* **Do** use the `DbContext` interface as the generic parameter, not the class. +* **Do** pass the `cancellationToken` to EF Core using the `GetCancellationToken` helper method. Example: + +````C# +public virtual async Task FindByNormalizedUserNameAsync( + string normalizedUserName, + bool includeDetails = true, + CancellationToken cancellationToken = default) +{ + return await DbSet + .IncludeDetails(includeDetails) + .FirstOrDefaultAsync( + u => u.NormalizedUserName == normalizedUserName, + GetCancellationToken(cancellationToken) + ); +} +```` + +`GetCancellationToken` fallbacks to the `ICancellationTokenProvider.Token` to obtain the cancellation token if it is not provided by the caller code. + +- **Do** create a `IncludeDetails` **extension method** for the `IQueryable` for each aggregate root which has **sub collections**. Example: + +````C# +public static IQueryable IncludeDetails( + this IQueryable queryable, + bool include = true) +{ + if (!include) + { + return queryable; + } + + return queryable + .Include(x => x.Roles) + .Include(x => x.Logins) + .Include(x => x.Claims) + .Include(x => x.Tokens); +} +```` + +* **Do** use the `IncludeDetails` extension method in the repository methods just like used in the example code above (see FindByNormalizedUserNameAsync). + +- **Do** override `IncludeDetails` method of the repository for aggregates root which have **sub collections**. Example: + +````C# +protected override IQueryable IncludeDetails(IQueryable queryable) +{ + return queryable.IncludeDetails(); //uses the extension method defined above +} +```` + +### Module Class + +- **Do** define a module class for the Entity Framework Core integration package. +- **Do** add `DbContext` to the `IServiceCollection` using the `AddAbpDbContext` method. +- **Do** add implemented repositories to the options for the `AddAbpDbContext` method. Example: + +````C# +[DependsOn( + typeof(AbpIdentityDomainModule), + typeof(AbpEntityFrameworkCoreModule) + )] +public class AbpIdentityEntityFrameworkCoreModule : AbpModule +{ + public override void ConfigureServices(IServiceCollection services) + { + services.AddAbpDbContext(options => + { + options.AddRepository(); + options.AddRepository(); + }); + + services.AddAssemblyOf(); + } +} +```` + diff --git a/docs/Best-Practices/Index.md b/docs/Best-Practices/Index.md new file mode 100644 index 0000000000..38df76919f --- /dev/null +++ b/docs/Best-Practices/Index.md @@ -0,0 +1,25 @@ +## Module Development Best Practices & Conventions + +### Introduction + +This document describes the **best practices** and **conventions** for who want to develop **modules** that satisfies the following specifications: + +* Develop the module that applies the **Domain Driven Design** patterns & best practices. +* Develop the module as **DBMS and ORM independent**. +* Develop the module that can be used as a **remote service / microservice** as well as can be integrated to a **monolithic** application. + +Also, this guide is mostly usable for the **application development**. + +### Guides + +* Domain Layer + * Entities + * [Repositories](Repositories.md) + * Domain Services +* Application Layer + * Application Services + * Data Transfer Objects +* Data Access + * [Entity Framework Core Integration](Entity-Framework-Core-Integration.md) + * [MongoDB Integration](MongoDB-Integration.md) + diff --git a/docs/Best-Practices/MongoDB-Integration.md b/docs/Best-Practices/MongoDB-Integration.md new file mode 100644 index 0000000000..b344d35cb7 --- /dev/null +++ b/docs/Best-Practices/MongoDB-Integration.md @@ -0,0 +1,203 @@ +## MongoDB Integration + +* Do define a separated `DbContext` interface and class for each module. + +### MongoDbContext Interface + +- **Do** define an **interface** for the `MongoDbContext` that inherits from `IAbpMongoDbContext`. +- **Do** add a `ConnectionStringName` **attribute** to the `MongoDbContext` interface. +- **Do** add `IMongoCollection` **properties** to the `MongoDbContext` interface for only aggregate roots. Example: + +````C# +[ConnectionStringName("AbpIdentity")] +public interface IAbpIdentityMongoDbContext : IAbpMongoDbContext +{ + IMongoCollection Users { get; } + IMongoCollection Roles { get; } +} +```` + +### MongoDbContext class + +- **Do** inherit the `MongoDbContext` from the `AbpMongoDbContext` class. +- **Do** add a `ConnectionStringName` attribute to the `MongoDbContext` class. +- **Do** implement the corresponding `interface` for the `MongoDbContext` class. Example: + +```c# +[ConnectionStringName("AbpIdentity")] +public class AbpIdentityMongoDbContext : AbpMongoDbContext, IAbpIdentityMongoDbContext +{ + public IMongoCollection Users => Collection(); + public IMongoCollection Roles => Collection(); + + //code omitted for brevity +} +``` + +### Collection Prefix + +- **Do** add static `CollectionPrefix` **property** to the `DbContext` class. Set default value from a constant. Example: + +```c# +public static string CollectionPrefix { get; set; } = AbpIdentityConsts.DefaultDbTablePrefix; +``` + +Used the same constant defined for the EF Core integration table prefix in this example. + +- **Do** always use a short `CollectionPrefix` value for a module to create **unique collection names** in a shared database. `Abp` collection prefix is reserved for ABP core modules. + +### Collection Mapping + +- **Do** explicitly **configure all entities** by overriding the `CreateModel` method of the `MongoDbContext`. Example: + +```c# +protected override void CreateModel(IMongoModelBuilder modelBuilder) +{ + base.CreateModel(modelBuilder); + + modelBuilder.ConfigureIdentity(options => + { + options.CollectionPrefix = CollectionPrefix; + }); +} +``` + +- **Do not** configure model directly in the `CreateModel` method. Instead, create an **extension method** for `IMongoModelBuilder`. Use Configure*ModuleName* as the method name. Example: + +```c# +public static class AbpIdentityMongoDbContextExtensions +{ + public static void ConfigureIdentity( + this IMongoModelBuilder builder, + Action optionsAction = null) + { + Check.NotNull(builder, nameof(builder)); + + var options = new IdentityMongoModelBuilderConfigurationOptions(); + + optionsAction?.Invoke(options); + + builder.Entity(b => + { + b.CollectionName = options.CollectionPrefix + "Users"; + }); + + builder.Entity(b => + { + b.CollectionName = options.CollectionPrefix + "Roles"; + }); + } +} +``` + +- **Do** create a **configuration options** class by inheriting from the `MongoModelBuilderConfigurationOptions`. Example: + +```c# +public class IdentityMongoModelBuilderConfigurationOptions + : MongoModelBuilderConfigurationOptions +{ + public IdentityMongoModelBuilderConfigurationOptions() + : base(AbpIdentityConsts.DefaultDbTablePrefix) + { + } +} +``` + +* **Do** explicitly configure `BsonClassMap` for all entities. Create a static method for this purpose. Example: + +````C# +public static class AbpIdentityBsonClassMap +{ + private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); + + public static void Configure() + { + OneTimeRunner.Run(() => + { + BsonClassMap.RegisterClassMap(map => + { + map.AutoMap(); + map.ConfigureExtraProperties(); + }); + + BsonClassMap.RegisterClassMap(map => + { + map.AutoMap(); + }); + }); + } +} +```` + +`BsonClassMap` works with static methods. So, it is only needed to configure entities once in an application. `OneTimeRunner` guarantees it in a thread safe manner. Such a mapping above ensures that unit test properly run. This code will be called by the **module class** below. + +### Repository Implementation + +- **Do** **inherit** the repository from the `MongoDbRepository` class and implement the corresponding repository interface. Example: + +```c# +public class MongoIdentityUserRepository + : MongoDbRepository, + IIdentityUserRepository +{ + public MongoIdentityUserRepository( + IMongoDbContextProvider dbContextProvider) + : base(dbContextProvider) + { + } +} +``` + +- **Do** pass the `cancellationToken` to the MongoDB Driver using the `GetCancellationToken` helper method. Example: + +```c# +public async Task FindByNormalizedUserNameAsync( + string normalizedUserName, + bool includeDetails = true, + CancellationToken cancellationToken = default) +{ + return await GetMongoQueryable() + .FirstOrDefaultAsync( + u => u.NormalizedUserName == normalizedUserName, + GetCancellationToken(cancellationToken) + ); +} +``` + +`GetCancellationToken` fallbacks to the `ICancellationTokenProvider.Token` to obtain the cancellation token if it is not provided by the caller code. + +* **Do** ignore the `includeDetails` parameters for the repository implementation since MongoDB loads the aggregate root as a whole (including sub collections) by default. +* **Do** use the `GetMongoQueryable()` method to obtain an `IQueryable` to perform queries wherever possible. Because; + * `GetMongoQueryable()` method automatically uses the `ApplyDataFilters` method to filter the data based on the current data filters (like soft delete and multi-tenancy). + * Using `IQueryable` makes the code as much as similar to the EF Core repository implementation and easy to write and read. +* **Do** implement data filtering if the `GetMongoQueryable()` method is not possible to use. + +### Module Class + +- **Do** define a module class for the MongoDB integration package. +- **Do** add `MongoDbContext` to the `IServiceCollection` using the `AddMongoDbContext` method. +- **Do** add implemented repositories to the options for the `AddMongoDbContext` method. Example: + +```c# +[DependsOn( + typeof(AbpIdentityDomainModule), + typeof(AbpUsersMongoDbModule) + )] +public class AbpIdentityMongoDbModule : AbpModule +{ + public override void ConfigureServices(IServiceCollection services) + { + AbpIdentityBsonClassMap.Configure(); + + services.AddMongoDbContext(options => + { + options.AddRepository(); + options.AddRepository(); + }); + + services.AddAssemblyOf(); + } +} +``` + +Notice that this module class also calls the static `BsonClassMap` configuration method defined above. \ No newline at end of file diff --git a/docs/Best-Practices/Repositories.md b/docs/Best-Practices/Repositories.md new file mode 100644 index 0000000000..295c832c39 --- /dev/null +++ b/docs/Best-Practices/Repositories.md @@ -0,0 +1,92 @@ +## Repository Best Practices & Conventions + +### Repository Interfaces + +* **Do** define a repository interface (like `IIdentityUserRepository`) and create its corresponding implementations for **each aggregate root**. + * **Do** always use the created repository interface from the application code. + * **Do not** use generic repository interfaces (like `IRepository`) from the application code. + * **Do not** use `IQueryable` features in the application code (domain, application... layers). + +For the example aggregate root: + +````C# +public class IdentityUser : AggregateRoot +{ + //... +} +```` + +Define the repository interface as below: + +````C# +public interface IIdentityUserRepository : IBasicRepository +{ + //... +} +```` + +* **Do** define repository interfaces in the **domain layer**. +* **Do not** inherit the repository interface from the `IRepository` interface. Because it inherits the `IQueryable` and the repository should not expose `IQueryable` to the application. +* **Do** inherit the repository interface from `IBasicRepository` (as normally) or a lower-featured interface, like `IReadOnlyRepository` (if it's needed). +* **Do not** define repositories for entities those are **not aggregate roots**. + +### Repository Methods + +* **Do** define all repository methods as **asynchronous**. +* **Do** add an **optional** `cancellationToken` parameter to every method of the repository. Example: + +````C# +Task FindByNormalizedUserNameAsync( + [NotNull] string normalizedUserName, + CancellationToken cancellationToken = default +); +```` + +* **Do** create a **synchronous extension** method for each asynchronous repository method. Example: + +````C# +public static class IdentityUserRepositoryExtensions +{ + public static IdentityUser FindByNormalizedUserName( + this IIdentityUserRepository repository, + [NotNull] string normalizedUserName) + { + return AsyncHelper.RunSync( + () => repository.FindByNormalizedUserNameAsync(normalizedUserName) + ); + } +} +```` + +This will allow synchronous code to use the repository methods easier. + +* **Do** add an optional `bool includeDetails = true` parameter (default value is `true`) for every repository method which returns a **single entity**. Example: + +````C# +Task FindByNormalizedUserNameAsync( + [NotNull] string normalizedUserName, + bool includeDetails = true, + CancellationToken cancellationToken = default +); +```` + +This parameter will be implemented for ORMs to eager load sub collections of the entity. + +* **Do** add an optional `bool includeDetails = false` parameter (default value is `false`) for every repository method which returns a **list of entities**. Example: + +````C# +Task> GetListByNormalizedRoleNameAsync( + string normalizedRoleName, + bool includeDetails = false, + CancellationToken cancellationToken = default +); +```` + +* **Do not** create composite classes to combine entities to get from repository with a single method call. Examples: *UserWithRoles*, *UserWithTokens*, *UserWithRolesAndTokens*. Instead, properly use `includeDetails` option to add all details of the entity when needed. +* **Avoid** to create projection classes for entities to get less property of an entity from the repository. Example: Avoid to create BasicUserView class to select a few properties needed for the use case needs. Instead, directly use the aggregate root class. However, there may be some exceptions for this rule, where: + * Performance is so critical for the use case and getting the whole aggregate root highly impacts the performance. + +### See Also + +* [Entity Framework Core Integration](Entity-Framework-Core-Integration.md) +* [MongoDB Integration](MongoDB-Integration.md) diff --git a/docs/Entity-Framework-Core.md b/docs/Entity-Framework-Core.md index c486d4b5b8..7203258e6c 100644 --- a/docs/Entity-Framework-Core.md +++ b/docs/Entity-Framework-Core.md @@ -100,9 +100,9 @@ TODO: Example TODO... -### Advanced Topics +### Best Practices -We will cover advanced techniques especially used to develop reusable modules and modular applications. +See [best practices](Best-Practices/) for EF Core integration. #### Set Base DbContext Class or Interface for Default Repositories diff --git a/docs/Index.md b/docs/Index.md index 0a8d8ea6c3..bb3e7098f5 100644 --- a/docs/Index.md +++ b/docs/Index.md @@ -11,7 +11,7 @@ * Module Development * Basics * Plug-In Modules - * Best Practices + * [Best Practices](Best-Practices/Index.md) * Domain Driven Design * Domain Layer * [Entities & Aggregate Roots](Entities.md) diff --git a/docs/Module-Development-Best-Practices.md b/docs/Module-Development-Best-Practices.md deleted file mode 100644 index 07eb9b7751..0000000000 --- a/docs/Module-Development-Best-Practices.md +++ /dev/null @@ -1,513 +0,0 @@ -## Module Development - -### Introduction - -This document describes the best practices for who want to develop modules that satisfies the following specifications: - -* Develop the module that applies the **Domain Driven Design** patterns & best practices. -* Develop the module as DBMS and **ORM independent**. -* Develop the module that can be used as a remote service / **microservice** as well as can be integrated to a **monolithic** application. - -This guide is also usable for application development best practices, mostly. - -### Data Access Layer - -The module should be completely independent of any DBMS and and ORM. - -- **Do not** use `IQueryable` features in the application code (domain, application... layers) except the data access layer. -- **Do** always use the specifically created repository interface (like `IIdentityUserRepository`) from the application code (as developed specified below). **Do not** use generic repository interfaces (like `IRepository`). - -#### Repositories - -* **Do** define a repository interface (and create its corresponding implementations) for each aggregate root. - -For the example aggregate root: - -````C# -public class IdentityUser : AggregateRoot -{ - //... -} -```` - -Define the repository interface as below: - -````C# -public interface IIdentityUserRepository : IBasicRepository -{ - //... -} -```` - -* **Do** define repository interfaces in the **domain layer**. -* **Do not** inherit the repository interface from `IRepository` interfaces. Because it inherits `IQueryable` and the repository should not expose `IQueryable` to the application. -* **Do** inherit the repository interface from `IBasicRepository` (as normally) or a lower-featured interface, like `IReadOnlyRepository` (if it's needed). -* **Do not** define repositories for entities those are **not aggregate roots**. -* **Do** define all repository methods as **asynchronous**. -* **Do** add an **optional** `cancellationToken` parameter to every method of the repository. Example: - -````C# -Task FindByNormalizedUserNameAsync( - [NotNull] string normalizedUserName, - CancellationToken cancellationToken = default -); -```` - -* **Do** create a **synchronous extension** method for each asynchronous repository method. Example: - -````C# -public static class IdentityUserRepositoryExtensions -{ - public static IdentityUser FindByNormalizedUserName( - this IIdentityUserRepository repository, - [NotNull] string normalizedUserName) - { - return AsyncHelper.RunSync( - () => repository.FindByNormalizedUserNameAsync(normalizedUserName) - ); - } -} -```` - -This will allow synchronous code to use the repository methods easier. - -* **Do** add an optional `bool includeDetails = true` parameter (default value is `true`) for every repository method which returns a **single entity**. Example: - -````C# -Task FindByNormalizedUserNameAsync( - [NotNull] string normalizedUserName, - bool includeDetails = true, - CancellationToken cancellationToken = default -); -```` - -This parameter will be implemented for ORMs to eager load sub collections of the entity. - -* **Do** add an optional `bool includeDetails = false` parameter (default value is `false`) for every repository method which returns a **list of entities**. Example: - -````C# -Task> GetListByNormalizedRoleNameAsync( - string normalizedRoleName, - bool includeDetails = false, - CancellationToken cancellationToken = default -); -```` - -* **Do not** create composite classes to combine entities to get from repository with a single method call. Examples: *UserWithRoles*, *UserWithTokens*, *UserWithRolesAndTokens*. Instead, properly use `includeDetails` option to add all details of the entity when needed. -* **Avoid** to create projection classes for entities to get less property of an entity from the repository. Example: Avoid to create BasicUserView class to select a few properties needed for the use case needs. Instead, directly use the aggregate root class. However, there may be some exceptions for this rule, where: - * Performance is so critical for the use case and getting the whole aggregate root highly impacts the performance. - -#### Integrations - -* **Do** create separated package/module for each ORM integration (like *Company.Module.EntityFrameworkCore* and *Company.Module.MongoDB*). - -##### Entity Framework Core - -- Do define a separated `DbContext` interface and class for each module. - -###### DbContext Interface - -- **Do** define an **interface** for the `DbContext` that inherits from `IEfCoreDbContext`. -- **Do** add a `ConnectionStringName` **attribute** to the `DbContext` interface. -- **Do** add `DbSet` **properties** to the `DbContext` interface for only aggregate roots. Example: - -````C# -[ConnectionStringName("AbpIdentity")] -public interface IIdentityDbContext : IEfCoreDbContext -{ - DbSet Users { get; set; } - DbSet Roles { get; set; } -} -```` - -###### DbContext class - -* **Do** inherit the `DbContext` from the `AbpDbContext` class. -* **Do** add a `ConnectionStringName` attribute to the `DbContext` class. -* **Do** implement the corresponding `interface` for the `DbContext` class. Example: - -````C# -[ConnectionStringName("AbpIdentity")] -public class IdentityDbContext : AbpDbContext, IIdentityDbContext -{ - public DbSet Users { get; set; } - public DbSet Roles { get; set; } - - public IdentityDbContext(DbContextOptions options) - : base(options) - { - - } - - //code omitted for brevity -} -```` - -###### Table Prefix and Schema - -- **Do** add static `TablePrefix` and `Schema` **properties** to the `DbContext` class. Set default value from a constant. Example: - -````C# -public static string TablePrefix { get; set; } = AbpIdentityConsts.DefaultDbTablePrefix; -public static string Schema { get; set; } = AbpIdentityConsts.DefaultDbSchema; -```` - - - **Do** always use a short `TablePrefix` value for a module to create **unique table names** in a shared database. `Abp` table prefix is reserved for ABP core modules. - - **Do** set `Schema` to `null` as default. - -###### Model Mapping - -- **Do** explicitly **configure all entities** by overriding the `OnModelCreating` method of the `DbContext`. Example: - -````C# -protected override void OnModelCreating(ModelBuilder builder) -{ - base.OnModelCreating(builder); - - builder.ConfigureIdentity(options => - { - options.TablePrefix = TablePrefix; - options.Schema = Schema; - }); -} -```` - -- **Do not** configure model directly in the `OnModelCreating` method. Instead, create an **extension method** for `ModelBuilder`. Use Configure*ModuleName* as the method name. Example: - -````C# -public static class IdentityDbContextModelBuilderExtensions -{ - public static void ConfigureIdentity( - [NotNull] this ModelBuilder builder, - Action optionsAction = null) - { - Check.NotNull(builder, nameof(builder)); - - var options = new IdentityModelBuilderConfigurationOptions(); - optionsAction?.Invoke(options); - - builder.Entity(b => - { - b.ToTable(options.TablePrefix + "Users", options.Schema); - //code omitted for brevity - }); - - builder.Entity(b => - { - b.ToTable(options.TablePrefix + "UserClaims", options.Schema); - //code omitted for brevity - }); - - //code omitted for brevity - } -} -```` - -* **Do** create a **configuration options** class by inheriting from the `ModelBuilderConfigurationOptions`. Example: - -````C# -public class IdentityModelBuilderConfigurationOptions : ModelBuilderConfigurationOptions -{ - public IdentityModelBuilderConfigurationOptions() - : base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema) - { - } -} -```` - -###### Repository Implementation - -- **Do** **inherit** the repository from the `EfCoreRepository` class and implement the corresponding repository interface. Example: - -````C# -public class EfCoreIdentityUserRepository - : EfCoreRepository, IIdentityUserRepository -{ - public EfCoreIdentityUserRepository( - IDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} -```` - -* **Do** use the `DbContext` interface as the generic parameter, not the class. -* **Do** pass the `cancellationToken` to EF Core using the `GetCancellationToken` helper method. Example: - -````C# -public virtual async Task FindByNormalizedUserNameAsync( - string normalizedUserName, - bool includeDetails = true, - CancellationToken cancellationToken = default) -{ - return await DbSet - .IncludeDetails(includeDetails) - .FirstOrDefaultAsync( - u => u.NormalizedUserName == normalizedUserName, - GetCancellationToken(cancellationToken) - ); -} -```` - -`GetCancellationToken` fallbacks to the `ICancellationTokenProvider.Token` to obtain the cancellation token if it is not provided by the caller code. - -- **Do** create a `IncludeDetails` **extension method** for the `IQueryable` for each aggregate root which has **sub collections**. Example: - -````C# -public static IQueryable IncludeDetails( - this IQueryable queryable, - bool include = true) -{ - if (!include) - { - return queryable; - } - - return queryable - .Include(x => x.Roles) - .Include(x => x.Logins) - .Include(x => x.Claims) - .Include(x => x.Tokens); -} -```` - -* **Do** use the `IncludeDetails` extension method in the repository methods just like used in the example code above (see FindByNormalizedUserNameAsync). - -- **Do** override `IncludeDetails` method of the repository for aggregates root which have **sub collections**. Example: - -````C# -protected override IQueryable IncludeDetails(IQueryable queryable) -{ - return queryable.IncludeDetails(); //uses the extension method defined above -} -```` - -###### Module Class - -- **Do** define a module class for the Entity Framework Core integration package. -- **Do** add `DbContext` to the `IServiceCollection` using the `AddAbpDbContext` method. -- **Do** add implemented repositories to the options for the `AddAbpDbContext` method. Example: - -````C# -[DependsOn( - typeof(AbpIdentityDomainModule), - typeof(AbpEntityFrameworkCoreModule) - )] -public class AbpIdentityEntityFrameworkCoreModule : AbpModule -{ - public override void ConfigureServices(IServiceCollection services) - { - services.AddAbpDbContext(options => - { - options.AddRepository(); - options.AddRepository(); - }); - - services.AddAssemblyOf(); - } -} -```` - -##### MongoDB - -* Do define a separated `DbContext` interface and class for each module. - -###### MongoDbContext Interface - -- **Do** define an **interface** for the `MongoDbContext` that inherits from `IAbpMongoDbContext`. -- **Do** add a `ConnectionStringName` **attribute** to the `MongoDbContext` interface. -- **Do** add `IMongoCollection` **properties** to the `MongoDbContext` interface for only aggregate roots. Example: - -````C# -[ConnectionStringName("AbpIdentity")] -public interface IAbpIdentityMongoDbContext : IAbpMongoDbContext -{ - IMongoCollection Users { get; } - IMongoCollection Roles { get; } -} -```` - -###### MongoDbContext class - -- **Do** inherit the `MongoDbContext` from the `AbpMongoDbContext` class. -- **Do** add a `ConnectionStringName` attribute to the `MongoDbContext` class. -- **Do** implement the corresponding `interface` for the `MongoDbContext` class. Example: - -```c# -[ConnectionStringName("AbpIdentity")] -public class AbpIdentityMongoDbContext : AbpMongoDbContext, IAbpIdentityMongoDbContext -{ - public IMongoCollection Users => Collection(); - public IMongoCollection Roles => Collection(); - - //code omitted for brevity -} -``` - -###### Collection Prefix - -- **Do** add static `CollectionPrefix` **property** to the `DbContext` class. Set default value from a constant. Example: - -```c# -public static string CollectionPrefix { get; set; } = AbpIdentityConsts.DefaultDbTablePrefix; -``` - -Used the same constant defined for the EF Core integration table prefix in this example. - -- **Do** always use a short `CollectionPrefix` value for a module to create **unique collection names** in a shared database. `Abp` collection prefix is reserved for ABP core modules. - -###### Collection Mapping - -- **Do** explicitly **configure all entities** by overriding the `CreateModel` method of the `MongoDbContext`. Example: - -```c# -protected override void CreateModel(IMongoModelBuilder modelBuilder) -{ - base.CreateModel(modelBuilder); - - modelBuilder.ConfigureIdentity(options => - { - options.CollectionPrefix = CollectionPrefix; - }); -} -``` - -- **Do not** configure model directly in the `CreateModel` method. Instead, create an **extension method** for `IMongoModelBuilder`. Use Configure*ModuleName* as the method name. Example: - -```c# -public static class AbpIdentityMongoDbContextExtensions -{ - public static void ConfigureIdentity( - this IMongoModelBuilder builder, - Action optionsAction = null) - { - Check.NotNull(builder, nameof(builder)); - - var options = new IdentityMongoModelBuilderConfigurationOptions(); - - optionsAction?.Invoke(options); - - builder.Entity(b => - { - b.CollectionName = options.CollectionPrefix + "Users"; - }); - - builder.Entity(b => - { - b.CollectionName = options.CollectionPrefix + "Roles"; - }); - } -} -``` - -- **Do** create a **configuration options** class by inheriting from the `MongoModelBuilderConfigurationOptions`. Example: - -```c# -public class IdentityMongoModelBuilderConfigurationOptions - : MongoModelBuilderConfigurationOptions -{ - public IdentityMongoModelBuilderConfigurationOptions() - : base(AbpIdentityConsts.DefaultDbTablePrefix) - { - } -} -``` - -* **Do** explicitly configure `BsonClassMap` for all entities. Create a static method for this purpose. Example: - -````C# -public static class AbpIdentityBsonClassMap -{ - private static readonly OneTimeRunner OneTimeRunner = new OneTimeRunner(); - - public static void Configure() - { - OneTimeRunner.Run(() => - { - BsonClassMap.RegisterClassMap(map => - { - map.AutoMap(); - map.ConfigureExtraProperties(); - }); - - BsonClassMap.RegisterClassMap(map => - { - map.AutoMap(); - }); - }); - } -} -```` - -`BsonClassMap` works with static methods. So, it is only needed to configure entities once in an application. `OneTimeRunner` guarantees it in a thread safe manner. Such a mapping above ensures that unit test properly run. This code will be called by the **module class** below. - -###### Repository Implementation - -- **Do** **inherit** the repository from the `MongoDbRepository` class and implement the corresponding repository interface. Example: - -```c# -public class MongoIdentityUserRepository - : MongoDbRepository, - IIdentityUserRepository -{ - public MongoIdentityUserRepository( - IMongoDbContextProvider dbContextProvider) - : base(dbContextProvider) - { - } -} -``` - -- **Do** pass the `cancellationToken` to the MongoDB Driver using the `GetCancellationToken` helper method. Example: - -```c# -public async Task FindByNormalizedUserNameAsync( - string normalizedUserName, - bool includeDetails = true, - CancellationToken cancellationToken = default) -{ - return await GetMongoQueryable() - .FirstOrDefaultAsync( - u => u.NormalizedUserName == normalizedUserName, - GetCancellationToken(cancellationToken) - ); -} -``` - -`GetCancellationToken` fallbacks to the `ICancellationTokenProvider.Token` to obtain the cancellation token if it is not provided by the caller code. - -* **Do** ignore the `includeDetails` parameters for the repository implementation since MongoDB loads the aggregate root as a whole (including sub collections) by default. -* **Do** use the `GetMongoQueryable()` method to obtain an `IQueryable` to perform queries wherever possible. Because; - * `GetMongoQueryable()` method automatically uses the `ApplyDataFilters` method to filter the data based on the current data filters (like soft delete and multi-tenancy). - * Using `IQueryable` makes the code as much as similar to the EF Core repository implementation and easy to write and read. -* **Do** implement data filtering if the `GetMongoQueryable()` method is not possible to use. - -###### Module Class - -- **Do** define a module class for the MongoDB integration package. -- **Do** add `MongoDbContext` to the `IServiceCollection` using the `AddMongoDbContext` method. -- **Do** add implemented repositories to the options for the `AddMongoDbContext` method. Example: - -```c# -[DependsOn( - typeof(AbpIdentityDomainModule), - typeof(AbpUsersMongoDbModule) - )] -public class AbpIdentityMongoDbModule : AbpModule -{ - public override void ConfigureServices(IServiceCollection services) - { - AbpIdentityBsonClassMap.Configure(); - - services.AddMongoDbContext(options => - { - options.AddRepository(); - options.AddRepository(); - }); - - services.AddAssemblyOf(); - } -} -``` - -Notice that this module class also calls the static `BsonClassMap` configuration method defined above. \ No newline at end of file