diff --git a/docs/Module-Development-Best-Practices.md b/docs/Module-Development-Best-Practices.md index b1e3414e7d..07eb9b7751 100644 --- a/docs/Module-Development-Best-Practices.md +++ b/docs/Module-Development-Best-Practices.md @@ -109,14 +109,13 @@ Task> GetListByNormalizedRoleNameAsync( - **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: +- **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; } } ```` @@ -125,14 +124,13 @@ public interface IIdentityDbContext : IEfCoreDbContext * **Do** inherit the `DbContext` from the `AbpDbContext` class. * **Do** add a `ConnectionStringName` attribute to the `DbContext` class. -* **Do** implement the repository `interface` for the `DbContext` class. Example: +* **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) @@ -147,15 +145,14 @@ public class IdentityDbContext : AbpDbContext, IIdentityDbCon ###### Table Prefix and Schema -- **Do** add static `TablePrefix` and `Schema` properties to the `DbContext` class. Set default value from an constant. Example: +- **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** 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 @@ -214,7 +211,6 @@ public class IdentityModelBuilderConfigurationOptions : ModelBuilderConfiguratio public IdentityModelBuilderConfigurationOptions() : base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema) { - } } ```` @@ -235,6 +231,7 @@ public class EfCoreIdentityUserRepository } ```` +* **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# @@ -292,7 +289,10 @@ protected override IQueryable IncludeDetails(IQueryable` method. Example: ````C# -[DependsOn(typeof(AbpIdentityDomainModule))] +[DependsOn( + typeof(AbpIdentityDomainModule), + typeof(AbpEntityFrameworkCoreModule) + )] public class AbpIdentityEntityFrameworkCoreModule : AbpModule { public override void ConfigureServices(IServiceCollection services) @@ -308,4 +308,206 @@ public class AbpIdentityEntityFrameworkCoreModule : AbpModule } ```` -##### MongoDB \ No newline at end of file +##### 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