mirror of https://github.com/abpframework/abp
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
7.9 KiB
7.9 KiB
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<TEntity>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 (likeIRepository<IdentityUser, Guid>).
Repositories
- Do define a repository interface (and create its corresponding implementations) for each aggregate root.
For the example aggregate root:
public class IdentityUser : AggregateRoot<Guid>
{
//...
}
Define the repository interface as below:
public interface IIdentityUserRepository : IBasicRepository<IdentityUser, Guid>
{
//...
}
- Do define repository interfaces in the domain layer.
- Do not inherit the repository interface from
IRepositoryinterfaces. Because it inheritsIQueryableand the repository should not exposeIQueryableto the application. - Do inherit the repository interface from
IBasicRepository<TEntity, TKey>(as normally) or a lower-featured interface, likeIReadOnlyRepository<TEntity, TKey>(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
cancellationTokenparameter to every method of the repository. Example:
Task<IdentityUser> FindByNormalizedUserNameAsync(
[NotNull] string normalizedUserName,
CancellationToken cancellationToken = default
);
- Do create a synchronous extension method for each asynchronous repository method. Example:
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 = trueparameter (default value istrue) for every repository method which returns a single entity. Example:
Task<IdentityUser> 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 = falseparameter (default value isfalse) for every repository method which returns a list of entities. Example:
Task<List<IdentityUser>> 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
includeDetailsoption 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
DbContextinterface and class for each module.
DbContext Interface
- Do define an interface for the
DbContextthat inherits fromIEfCoreDbContext. - Do add a
ConnectionStringNameattribute to theDbContextinterface. - Do add
DbSet<T>properties to theDbContextinterface for only aggregate roots. Example:
[ConnectionStringName("AbpIdentity")]
public interface IIdentityDbContext : IEfCoreDbContext
{
DbSet<IdentityUser> Users { get; set; }
DbSet<IdentityRole> Roles { get; set; }
}
DbContext class
- Do inherit the
DbContextfrom theAbpDbContext<TDbContext>class. - Do add a
ConnectionStringNameattribute to theDbContextclass. - Do implement the repository
interfacefor theDbContextclass. Example:
[ConnectionStringName("AbpIdentity")]
public class IdentityDbContext : AbpDbContext<IdentityDbContext>, IIdentityDbContext
{
public DbSet<IdentityUser> Users { get; set; }
public DbSet<IdentityRole> Roles { get; set; }
public IdentityDbContext(DbContextOptions<IdentityDbContext> options)
: base(options)
{
}
//code omitted for brevity
}
Table Prefix and Schema
- Do add static
TablePrefixandSchemaproperties to theDbContextclass. Set default value from an constant. Example:
public static string TablePrefix { get; set; } = AbpIdentityConsts.DefaultDbTablePrefix;
public static string Schema { get; set; } = AbpIdentityConsts.DefaultDbSchema;
- Do always use a short
TablePrefixvalue for a module to create unique table names in a shared database.Abptable prefix is reserved for ABP core modules. - Do set
Schematonullas default.
Model Mapping
- Do explicitly configure all entities by overriding the
OnModelCreatingmethod of theDbContext. Example:
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
OnModelCreatingmethod. Instead, create an extension method forModelBuilder. Use ConfigureModuleName as the method name. Example:
public static class IdentityDbContextModelBuilderExtensions
{
public static void ConfigureIdentity(
[NotNull] this ModelBuilder builder,
Action<IdentityModelBuilderConfigurationOptions> optionsAction = null)
{
Check.NotNull(builder, nameof(builder));
var options = new IdentityModelBuilderConfigurationOptions();
optionsAction?.Invoke(options);
builder.Entity<IdentityUser>(b =>
{
b.ToTable(options.TablePrefix + "Users", options.Schema);
//code omitted for brevity
});
builder.Entity<IdentityUserClaim>(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:
public class IdentityModelBuilderConfigurationOptions : ModelBuilderConfigurationOptions
{
public IdentityModelBuilderConfigurationOptions()
: base(AbpIdentityConsts.DefaultDbTablePrefix, AbpIdentityConsts.DefaultDbSchema)
{
}
}
Module Class
Define
Repository Implementation
-