From cf8c96342274f82e0bef228f541e0f66dfe9783c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Halil=20=C4=B0brahim=20Kalkan?= Date: Wed, 11 Apr 2018 09:42:30 +0300 Subject: [PATCH] Added data access layer section. --- docs/Module-Development-Best-Practices.md | 184 +++++++++++++++++++++- 1 file changed, 180 insertions(+), 4 deletions(-) diff --git a/docs/Module-Development-Best-Practices.md b/docs/Module-Development-Best-Practices.md index 097b00dee1..7116b31cad 100644 --- a/docs/Module-Development-Best-Practices.md +++ b/docs/Module-Development-Best-Practices.md @@ -8,12 +8,13 @@ This document describes the best practices for who want to develop modules that * 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** create separated packages (projects/assemblies/libraries) for each ORM integration (like *Company.Module.EntityFrameworkCore* and *Company.Module.MongoDB*). - **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 @@ -43,12 +44,187 @@ public interface IIdentityUserRepository : IBasicRepository * **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 a `cancellationToken` parameter to every method of the repository. -* **Do** create a **synchronous extension** method for each asynchronous repository method. -* ... +* **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 repository `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 an 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) + { + + } +} +```` + +###### Module Class + +Define + +###### Repository Implementation + +- ​ + ##### MongoDB \ No newline at end of file