ABP Framework Patterns¶
This document describes the core patterns and conventions used by the ABP Framework in the Cargonerds application.
Overview¶
The ABP Framework is built on well-established software development principles and patterns. Understanding these patterns is crucial for working effectively with the Cargonerds codebase.
Core Patterns¶
1. Layered Architecture¶
ABP enforces a strict layered architecture where each layer has specific responsibilities and dependencies flow in one direction (from outer layers to inner layers).
Domain Layer (Core)¶
The innermost layer containing business logic and rules:
- Entities: Domain objects with identity
- Value Objects: Objects without identity
- Aggregate Roots: Entities that serve as entry points to aggregates
- Repository Interfaces: Abstraction for data access
- Domain Services: Business logic that doesn't naturally fit in entities
- Domain Events: Events raised by domain objects
// Example: Book entity from Cargonerds
public class Book : AuditedAggregateRoot<Guid>
{
public string Name { get; set; }
public BookType Type { get; set; }
public DateTime PublishDate { get; set; }
public float Price { get; set; }
}
Application Layer¶
Orchestrates domain objects to implement use cases:
- Application Services: Entry points for business operations
- DTOs (Data Transfer Objects): Data contracts for communication
- Application Service Interfaces: Contracts for app services
// Example: Application service interface
public interface IBookAppService : IApplicationService
{
Task<BookDto> GetAsync(Guid id);
Task<PagedResultDto<BookDto>> GetListAsync(PagedAndSortedResultRequestDto input);
Task<BookDto> CreateAsync(CreateUpdateBookDto input);
Task<BookDto> UpdateAsync(Guid id, CreateUpdateBookDto input);
Task DeleteAsync(Guid id);
}
Infrastructure Layer¶
Provides concrete implementations for infrastructure concerns:
- Repository Implementations: EF Core repositories
- DbContext: Database context
- External Service Integrations: Third-party APIs
Presentation Layer¶
User interface and API endpoints:
- Controllers: API endpoints
- Pages/Components: UI components
- View Models: UI-specific data models
2. Dependency Injection¶
ABP heavily uses dependency injection (DI) for loose coupling and testability.
Conventional Registration¶
ABP automatically registers services by convention:
// Services ending with these suffixes are auto-registered:
// - *AppService (as transient)
// - *Repository (as transient)
// - *Manager, *Service (domain services, transient)
// - *Controller (as transient)
public class BookAppService : ApplicationService, IBookAppService
{
private readonly IRepository<Book, Guid> _repository;
// Constructor injection
public BookAppService(IRepository<Book, Guid> repository)
{
_repository = repository;
}
}
Manual Registration¶
For custom registration needs:
public override void ConfigureServices(ServiceConfigurationContext context)
{
context.Services.AddTransient<IMyService, MyService>();
context.Services.AddSingleton<IMyCache, MyCache>();
}
3. Module System¶
ABP applications are composed of modules that can be independently developed and reused.
[DependsOn(
typeof(CargonerdsDomainSharedModule),
typeof(AbpIdentityApplicationModule),
typeof(AbpPermissionManagementApplicationModule)
)]
public class CargonerdsApplicationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
// Configure module services
}
}
Module Features: - Encapsulates related functionality - Declares dependencies on other modules - Can be reused across applications - Manages its own database schema
4. Repository Pattern¶
ABP provides generic repositories out of the box:
public class BookAppService : ApplicationService
{
private readonly IRepository<Book, Guid> _repository;
public async Task<BookDto> GetAsync(Guid id)
{
var book = await _repository.GetAsync(id);
return ObjectMapper.Map<Book, BookDto>(book);
}
public async Task<PagedResultDto<BookDto>> GetListAsync(
PagedAndSortedResultRequestDto input)
{
var queryable = await _repository.GetQueryableAsync();
var query = queryable
.OrderBy(input.Sorting ?? "Name")
.Skip(input.SkipCount)
.Take(input.MaxResultCount);
var books = await AsyncExecuter.ToListAsync(query);
var totalCount = await AsyncExecuter.CountAsync(queryable);
return new PagedResultDto<BookDto>(
totalCount,
ObjectMapper.Map<List<Book>, List<BookDto>>(books)
);
}
}
Repository Benefits: - Abstracts data access - Provides CRUD operations - Supports LINQ queries - Integrates with Unit of Work
5. Unit of Work (UOW)¶
ABP automatically manages database transactions:
[UnitOfWork]
public virtual async Task CreateBookWithAuthorAsync(
CreateBookDto bookDto,
CreateAuthorDto authorDto)
{
// Both operations are in the same transaction
var author = await _authorRepository.InsertAsync(
new Author { Name = authorDto.Name }
);
var book = await _bookRepository.InsertAsync(
new Book {
Name = bookDto.Name,
AuthorId = author.Id
}
);
// Auto-committed if no exception
}
UOW Features: - Automatic transaction management - Supports nested UOW scopes - Rollback on exceptions - Ambient transaction context
6. Object to Object Mapping¶
ABP uses AutoMapper for object mapping:
public class CargonerdsApplicationAutoMapperProfile : Profile
{
public CargonerdsApplicationAutoMapperProfile()
{
CreateMap<Book, BookDto>();
CreateMap<CreateUpdateBookDto, Book>();
}
}
// Usage in application service
public async Task<BookDto> GetAsync(Guid id)
{
var book = await _repository.GetAsync(id);
return ObjectMapper.Map<Book, BookDto>(book);
}
7. Authorization¶
ABP provides a permission-based authorization system:
// Define permissions
public static class CargonerdsPermissions
{
public const string GroupName = "Cargonerds";
public static class Books
{
public const string Default = GroupName + ".Books";
public const string Create = Default + ".Create";
public const string Edit = Default + ".Edit";
public const string Delete = Default + ".Delete";
}
}
// Use in application service
[Authorize(CargonerdsPermissions.Books.Default)]
public class BookAppService : ApplicationService, IBookAppService
{
[Authorize(CargonerdsPermissions.Books.Create)]
public async Task<BookDto> CreateAsync(CreateUpdateBookDto input)
{
// Only users with Create permission can execute
}
}
8. Validation¶
ABP automatically validates DTOs using data annotations:
public class CreateUpdateBookDto
{
[Required]
[StringLength(128)]
public string Name { get; set; }
[Required]
public BookType Type { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime PublishDate { get; set; }
[Required]
[Range(0, 999.99)]
public float Price { get; set; }
}
9. Auditing¶
ABP provides automatic auditing for entities:
// Entity with auditing
public class Book : AuditedAggregateRoot<Guid>
{
// CreationTime, CreatorId are automatically set
// LastModificationTime, LastModifierId are automatically tracked
}
// Full auditing includes soft delete
public class Order : FullAuditedAggregateRoot<Guid>
{
// Adds: IsDeleted, DeletionTime, DeleterId
}
10. Multi-Tenancy¶
ABP supports multi-tenancy for SaaS applications:
// Tenant-aware entity
public class Document : AuditedAggregateRoot<Guid>, IMultiTenant
{
public Guid? TenantId { get; set; }
// Data is automatically filtered by tenant
}
// Current tenant access
public class DocumentAppService : ApplicationService
{
public async Task<List<DocumentDto>> GetMyDocumentsAsync()
{
var tenantId = CurrentTenant.Id;
// Query automatically filtered by tenant
var documents = await _repository.GetListAsync();
return ObjectMapper.Map<List<Document>, List<DocumentDto>>(documents);
}
}
11. Localization¶
ABP provides resource-based localization:
// Using localized strings
public class BookAppService : ApplicationService
{
public async Task ValidateBookAsync(Book book)
{
if (book.Price < 0)
{
throw new UserFriendlyException(
L["NegativePriceNotAllowed"]
);
}
}
}
JSON resource files:
// en.json
{
"NegativePriceNotAllowed": "Price cannot be negative",
"BookCreatedSuccessfully": "Book created successfully"
}
12. Background Jobs¶
ABP provides a background job system for long-running tasks:
// Define a background job
public class EmailSendingJob : AsyncBackgroundJob<EmailSendingArgs>
{
public override async Task ExecuteAsync(EmailSendingArgs args)
{
// Send email
await _emailSender.SendAsync(args.To, args.Subject, args.Body);
}
}
// Queue a job
await _backgroundJobManager.EnqueueAsync(
new EmailSendingArgs
{
To = "user@example.com",
Subject = "Welcome",
Body = "Welcome to Cargonerds!"
}
);
13. Event Bus¶
ABP provides a distributed event bus for loosely coupled communication:
// Define an event
public class BookCreatedEto
{
public Guid Id { get; set; }
public string Name { get; set; }
}
// Publish an event
await _distributedEventBus.PublishAsync(
new BookCreatedEto
{
Id = book.Id,
Name = book.Name
}
);
// Subscribe to an event
public class BookCreatedEventHandler :
IDistributedEventHandler<BookCreatedEto>,
ITransientDependency
{
public async Task HandleEventAsync(BookCreatedEto eventData)
{
// Handle the event
}
}
Best Practices¶
1. Keep Domain Layer Clean¶
- No dependencies on infrastructure
- Use interfaces for external services
- Business rules belong in domain
2. Use Application Services for Use Cases¶
- One application service per aggregate root
- Keep methods focused on single use cases
- Return DTOs, never entities
3. Leverage ABP Features¶
- Use built-in repositories when possible
- Apply authorization attributes
- Use automatic auditing
- Implement localization from the start
4. Follow Naming Conventions¶
- AppService suffix for application services
- Dto suffix for data transfer objects
- Repository suffix for custom repositories
- Manager suffix for domain services
5. Proper Exception Handling¶
- Use
UserFriendlyExceptionfor user errors - Use
BusinessExceptionfor business rule violations - Let ABP handle exceptions in controllers
Cargonerds-Specific Patterns¶
Organization Filtering¶
The Hub module implements organization-based filtering:
public class ShipmentAppService : HubEntityBaseService<Shipment, ShipmentDto>
{
protected override async Task<IQueryable<Shipment>> WithDetailsAsync()
{
var query = await base.WithDetailsAsync();
var filterCtx = Get<CurrentFilterContextProvider>();
// Apply organization filtering
return query.ApplyOrganizationFilter(filterCtx);
}
}
Code Generation¶
Cargonerds uses Nextended.CodeGen for automatic DTO generation:
{
"Namespace": "Hub",
"Suffix": "Dto",
"OutputPath": "../Hub.Application.Contracts/Generated",
"MappingOutputPath": "../Hub.Application/Extensions/Generated"
}
This automatically generates DTOs and mapping extensions from domain entities.