Introducing the Repository Pattern in DelphiMVCFramework: Clean Architecture Made Simple
Introduction
After years of working with DelphiMVCFramework and helping developers build better REST APIs, I’ve noticed a recurring pattern: while the ActiveRecord implementation is powerful and convenient, many teams needed a cleaner separation of concerns, especially when building larger applications with complex business logic.
Let me be clear upfront: ActiveRecord remains the foundational technology for data access in DelphiMVCFramework ๐๏ธ. Everything you can do with repositories, you can also achieve with ActiveRecord directly - it just requires a bit more code. The Repository Pattern is simply a convenient abstraction layer that makes certain architectural patterns easier to implement, particularly when you need dependency injection, cleaner separation of concerns, or more testable code.
That’s why I’m excited to introduce native Repository Pattern support in DelphiMVCFramework ๐. This new feature brings professional-grade architecture patterns to your Delphi applications while maintaining the simplicity and elegance you’ve come to expect from the framework. Under the hood, repositories delegate all operations to ActiveRecord, ensuring zero code duplication and leveraging the battle-tested ActiveRecord implementation you already know and trust.
Why the Repository Pattern?
The Repository Pattern acts as a mediator between your domain/business logic and data mapping layers. Think of it as a collection-like interface for accessing domain entities. Here are the key benefits:
1. Separation of Concerns ๐ฏ
Your controllers no longer need to know about ActiveRecord details. They work with a clean interface (IMVCRepository<T>
) that can be easily mocked for testing.
2. Dependency Injection Ready ๐
Repositories are perfect for dependency injection. You can inject IMVCRepository<TCustomer>
directly into your controllers, making your code more testable and maintainable.
3. Testability โ
Mock repositories are easy to create for unit testing. You can test your business logic without touching the database.
4. Flexibility ๐ง
Need custom queries? Just extend the base repository with domain-specific methods while keeping all the standard CRUD operations.
Getting Started: Basic Usage
Let’s start with a simple example. Here’s how you define an entity using ActiveRecord:
[MVCNameCase(ncLowerCase)]
[MVCTable('customers')]
TCustomer = class(TMVCActiveRecord)
private
[MVCTableField('id', [foPrimaryKey, foAutoGenerated])]
fID: NullableInt64;
[MVCTableField('code')]
fCode: NullableString;
[MVCTableField('company_name')]
fCompanyName: NullableString;
[MVCTableField('city')]
fCity: string;
[MVCTableField('rating')]
fRating: NullableInt32;
public
property ID: NullableInt64 read fID write fID;
property Code: NullableString read fCode write fCode;
property CompanyName: NullableString read fCompanyName write fCompanyName;
property City: string read fCity write fCity;
property Rating: NullableInt32 read fRating write fRating;
end;
Now, instead of using ActiveRecord methods directly in your controller, you have two options:
Option 1: Use the Generic Repository Directly
If you don’t need custom logic or domain-specific methods, you can simply use IMVCRepository<T>
directly without creating a custom class:
// In your controller or service
procedure RegisterServices(Container: IMVCServiceContainer);
begin
Container.RegisterType(
TMVCRepository<TCustomer>,
IMVCRepository<TCustomer>,
TRegistrationType.SingletonPerRequest
);
end;
// Then inject it directly
function TCustomersController.GetCustomers(
[MVCInject] CustomersRepository: IMVCRepository<TCustomer>): IMVCResponse;
begin
Result := OKResponse(CustomersRepository.GetAll);
end;
This is perfect for simple CRUD operations where the standard repository methods are sufficient. โก
Option 2: Create a Custom Repository Class
If you need to add custom methods or domain-specific logic, create your own repository class:
type
TCustomerRepository = class(TMVCRepository<TCustomer>)
public
// Add custom methods here if needed
function GetCustomersByCity(const City: string): TObjectList<TCustomer>;
function GetTopRatedCustomers: TObjectList<TCustomer>;
end;
Implementing Custom Repository Methods
One of the most powerful features is the ability to extend repositories with domain-specific methods. Here’s how:
function TCustomerRepository.GetCustomersByCity(const City: string): TObjectList<TCustomer>;
begin
Result := GetWhere('city = ?', [City]);
end;
function TCustomerRepository.GetTopRatedCustomers: TObjectList<TCustomer>;
begin
Result := SelectByNamedQuery('BestCustomers', [], []);
end;
Notice how these methods use the base repository capabilities (GetWhere
, SelectByNamedQuery
) but provide a more expressive, domain-specific API for your controllers.
Using Repositories in Controllers with Dependency Injection
Here’s where things get really interesting. DelphiMVCFramework’s built-in dependency injection container makes it incredibly easy to use repositories in your controllers:
[MVCPath('/api/customers')]
TCustomersController = class(TMVCController)
public
[MVCPath]
[MVCHTTPMethods([httpGET])]
function GetCustomers(
[MVCFromQueryString('rql','')] RQLFilter: String;
[MVCInject] CustomersRepository: IMVCRepository<TCustomer>): IMVCResponse;
[MVCPath('/($ID)')]
[MVCHTTPMethods([httpGET])]
function GetCustomerByID(
const ID: Integer;
[MVCInject] CustomersRepository: IMVCRepository<TCustomer>): IMVCResponse;
[MVCPath]
[MVCHTTPMethods([httpPOST])]
function CreateCustomer(
[MVCFromBody] const Customer: TCustomer;
[MVCInject] CustomersRepository: IMVCRepository<TCustomer>): IMVCResponse;
end;
The implementation is clean and focused on HTTP concerns:
function TCustomersController.GetCustomers(
RQLFilter: String;
CustomersRepository: IMVCRepository<TCustomer>): IMVCResponse;
begin
Result := OKResponse(CustomersRepository.SelectRQL(RQLFilter));
end;
function TCustomersController.CreateCustomer(
const Customer: TCustomer;
CustomersRepository: IMVCRepository<TCustomer>): IMVCResponse;
begin
CustomersRepository.Store(Customer);
Result := CreatedResponse('/api/customers/' + Customer.ID.Value.ToString);
end;
Notice how the controller methods receive the repository as a parameter with the [MVCInject]
attribute. The framework automatically resolves and injects the dependency! ๐
Registering Repositories in the Container
To enable dependency injection, you need to register your repositories in the container. This is typically done in your WebModule:
procedure RegisterServices(Container: IMVCServiceContainer);
begin
Container.RegisterType(
TMVCRepository<TCustomer>,
IMVCRepository<TCustomer>,
TRegistrationType.SingletonPerRequest
);
end;
The SingletonPerRequest
registration type ensures that each HTTP request gets its own repository instance, which is automatically disposed of when the request completes.
Rich Query Capabilities
The repository interface provides multiple ways to query your data:
SQL Where Clauses
lCustomers := Repository.GetWhere('city = ? AND rating >= ?', ['Rome', 4]);
RQL (Resource Query Language)
lCustomers := Repository.SelectRQL('eq(rating,5)');
lCustomers := Repository.SelectRQL('ge(rating,4)&sort(+code)');
Raw SQL
lCustomers := Repository.Select(
'SELECT * FROM customers WHERE city = ? AND rating >= ? ORDER BY code',
['Rome', 3]
);
Named Queries
lCustomers := Repository.SelectByNamedQuery('BestCustomers', [], []);
Transaction Support
Repositories work seamlessly with transactions using the convenient UseTransactionContext
pattern:
function TCustomersController.BulkCreateCustomers(
const Customers: TObjectList<TCustomer>;
CustomersRepository: IMVCRepository<TCustomer>): IMVCResponse;
begin
var lCtx := TMVCRepository.UseTransactionContext;
for var lCustomer in Customers do
begin
CustomersRepository.Insert(lCustomer);
end;
Result := CreatedResponse();
end;
The transaction is automatically committed when lCtx
goes out of scope, or rolled back if an exception occurs. Clean and simple! ๐
Advanced Features
Custom Connection Management
Need to use a specific database connection? Use TMVCRepositoryWithConnection
:
var
lConnection: TFDConnection;
lRepo: IMVCRepository<TCustomer>;
begin
lConnection := TFDConnection.Create(nil);
lConnection.ConnectionDefName := 'SecondaryDB';
lConnection.Connected := True;
lRepo := TMVCRepositoryWithConnection<TCustomer>.Create(lConnection, True);
// Use the repository with the custom connection
end;
The Store Method
The Store
method is particularly useful - it automatically determines whether to insert or update based on whether the entity has a primary key:
// Insert if PK is null
lCustomer := TCustomer.Create;
lCustomer.Code := 'NEW001';
Repository.Store(lCustomer); // INSERT
// Update if PK exists
lCustomer := Repository.GetByPK(lID);
lCustomer.CompanyName := 'Updated Name';
Repository.Store(lCustomer); // UPDATE
Utility Methods
The repository includes several helpful utility methods:
// Check existence without loading the entity
if Repository.Exists(CustomerID) then
// Do something
// Count records
lCount := Repository.Count('ge(rating,4)');
// Delete all (use with caution!)
lDeleted := Repository.DeleteAll;
Complete Example: Repository Showcase
I’ve created two comprehensive samples in the framework:
- repository_showcase - Demonstrates every feature of the Repository Pattern with over 20 examples
- simple_api_using_repository_with_injection - A complete REST API using repositories and dependency injection
These samples cover:
- Basic CRUD operations
- Query methods (SQL, RQL, Named Queries)
- Custom repository methods
- Transaction management
- Error handling
- Bulk operations
- Custom connections
- And much more!
Migrating from ActiveRecord to Repository Pattern ๐
One of the best features of this implementation is the seamless migration path. Since the Repository Pattern is built on top of ActiveRecord, you don’t need to redo your entity mappings or change your database structure. Your existing ActiveRecord entities work immediately with repositories! โจ
Here’s what this means in practice:
Before (using ActiveRecord directly):
function TCustomersController.GetCustomers: IMVCResponse;
begin
Result := OKResponse(TMVCActiveRecord.All<TCustomer>);
end;
After (using Repository Pattern):
function TCustomersController.GetCustomers(
[MVCInject] CustomersRepository: IMVCRepository<TCustomer>): IMVCResponse;
begin
Result := OKResponse(CustomersRepository.GetAll);
end;
Your TCustomer
entity class remains exactly the same! You’re simply using a different access pattern. This means you can:
- Migrate incrementally ๐ - Convert one controller or service at a time
- Mix both approaches ๐ - Use ActiveRecord in some places, Repository in others
- No rewrites required ๐จ - Your entity mappings, validations, and business logic stay untouched
- Choose what fits best ๐ - Use the pattern that makes sense for each specific case
If your application is already using ActiveRecord and you want to introduce dependency injection or improve testability, you can start using IMVCRepository<T>
immediately without any refactoring of your entity layer.
When to Use Repositories vs. ActiveRecord Directly
Use Repositories when:
- Building larger applications with complex business logic
- You need dependency injection for testing
- You want clear separation between layers
- Multiple developers are working on the same codebase
- You’re following Domain-Driven Design principles
- You’re migrating to a more testable architecture
Use ActiveRecord directly when:
- Building simple CRUD applications
- Prototyping or proof-of-concept projects
- The additional abstraction layer isn’t needed
- You prefer the simplicity of direct data access
- You’re working on a small, focused microservice
Both approaches are valid, and you can even mix them in the same application! The best part is that switching between them requires minimal code changes.
Join Me at ITDevCon 2025! ๐ค
I’ll be presenting an in-depth session on the Repository Pattern and other advanced DelphiMVCFramework features at ITDevCon 2025 on November 6-7 in Milan, Italy ๐ฎ๐น.
We’ll dive deeper into:
- Advanced repository patterns ๐๏ธ
- Testing strategies with repositories ๐งช
- Domain-Driven Design with Delphi ๐
- Best practices for large-scale applications ๐
- Live coding demonstrations ๐ป
If you’re serious about building better REST APIs with Delphi, this is an event you don’t want to miss! Register now at www.itdevcon.it
Conclusion
The Repository Pattern support in DelphiMVCFramework brings enterprise-grade architecture to your REST APIs without sacrificing the simplicity and productivity that Delphi developers love. Whether you’re building a small microservice or a large-scale application, repositories give you the flexibility and structure you need. ๐ช
The implementation is lightweight, leveraging the existing ActiveRecord foundation, so there’s no code duplication and minimal overhead. You get clean, testable, maintainable code that follows industry best practices. โจ
Try it out in your next project, and let me know what you think! The framework is open source, so dive into the samples, experiment, and don’t hesitate to contribute improvements. ๐ค
See you at ITDevCon 2025! ๐
Links: ๐
- DelphiMVCFramework on GitHub
- ITDevCon 2025 - November 6-7, Milan
- Sample projects in:
/samples/repository_showcase
and/samples/simple_api_using_repository_with_injection
Happy coding! ๐จโ๐ป
Daniele Teti
Comments
comments powered by Disqus