Become a member!

Introducing the Repository Pattern in DelphiMVCFramework: Clean Architecture Made Simple

  • ๐Ÿ‘‰ This article is available in english too.
  • ๐Ÿ‘‰ Questo articolo รจ disponibile anche in italiano.
  • ๐Ÿ‘‰ Este artรญculo tambiรฉn estรก disponible en espaรฑol.

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:

  1. repository_showcase - Demonstrates every feature of the Repository Pattern with over 20 examples
  2. 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: ๐Ÿ”—

Happy coding! ๐Ÿ‘จโ€๐Ÿ’ป

Daniele Teti

Comments

comments powered by Disqus