Skip to content

Repository Pattern

The Persistence Module includes Repository implementation with basic CRUD operations

Repositories implement the IRepository<TId, TEntity> interface or IReadOnlyRepository<TId, TEntity>. An implementation is provided by the Suite. For example the EFCore Module provides the EFCoreRepository<TId, TEntity> which implements the interface using DbSets.

There are two ways of using repositories, Generic and Custom.

Generic Repositories

Generic Repositories allows you to simply inject a repository interface for your entity whenever you need to perform basic operations, without the need to define its implementation.

When injecting an IRepository<TId, TEntity>, or its read-only variant, they will act as a factory; very similar to how the ILogger<T> works.

Since applications may be composed of multiple DbContexts, the generic repositories relies on the IEntityTypeConfiguration<TDbContext, TEntity> for determining which DbContext to use for an entity.

In practice, this means that when injecting a repository for an entity you don't need to specify the DbContext. Which is important, since the DbContext is something internal from the infrastructure layer implementation, and you'll most likely want to inject repositories in your application layer, that is only aware of your domain.

Let's see an example, first we define an IEntityTypeConfiguration<TDbContext, TEntity> for our entity:

C#
internal class InspectionEntityTypeConfiguration
    : IEntityTypeConfiguration<IInspectorDbContext, Inspection>
{
    public void Configure(EntityTypeBuilder<Inspection> builder)
    {
        builder.ToTable("Inspections");
        builder.HasKey(x => x.Id);
        builder.Property(x => x.Description);
    }
}

For the sake of completeness, the DbContext:

C#
1
2
3
4
5
6
7
8
[DefaultSchemaName("iInspector")]
internal class IInspectorDbContext: SuiteDbContext<IInspectorDbContext>
{
    public IInspectorDbContext(DbContextOptions<IInspectorDbContext> options)
        : base(contextOptions)
    {
    }
}

When we want to inject a repository for our entity, we simply need to inject IRepository<int, Inspection>:

C#
public class InspectionsAppService : IAppService
{
    private readonly IRepository<int, Inspection> inspectionRepository;

    public InspectionsAppService(
        // Inject with the Id and Entity Type. Will be auto generated.
        IRepository<int, Inspection> inspectionRepository)
    {
        this.inspectionRepository = inspectionRepository;
    }

    public async Task<IEnumerable<InspectionOutputDTO>> GetAll(CancellationToken ct = default)
    {
        var inspections = await this.inspectionRepository.AllAsync(ct);
        // [..]
    }
}

That's the only thing you need to do, besides having a DbContext properly configured. You don't need to provide an implementation for IRepository<int, Inspection>; it will be proxied for your convenience.

Read Only Repositories

When you are performing read only operations it's best practice to inject a IReadOnlyRepository<TId, TEntity>, as we can see in the following example:

C#
public class InspectionsAppService : IAppService
{
    private readonly IReadOnlyRepository<int, Inspection> inspectionRepository;

    public InspectionsAppService(
        // Inject with the Id and Entity Type. Will be auto generated.
        IReadOnlyRepository<int, Inspection> inspectionRepository)
    {
        this.inspectionRepository = inspectionRepository;
    }

    public async Task<IEnumerable<InspectionOutputDTO>> GetAll(CancellationToken ct = default)
    {
        var result = await this.inspectionRepository.AllAsync(ct);
        // [..]
    }
}

Adding Custom Behavior

There are basically two approaches for adding behavior to Repositories, extension methods and custom repository implementations. We recommend using the former whenever possible as it is cleaner, requires less code and doesn't require dealing with DI.

Extension Methods

To customize the behavior of a Repository you can write an Extension Method to encapsulate the complexity of the custom behavior nicely. For example inside the extension method you can take advantage of the Specification library supported by IRepository.

C#
public static async Task<Issue> FindByCodeAsync(
    this IReadOnlyRepository<Guid, Issue> repo,
    string code,
    CancellationToken cancellationToken = default)
{
    var specs = AggregateSpecification.For<Issue>()
        .AndFilter(new IssueCodeFilterSpecification(code))
        .AddInclude(new IssueIncludePrioritySpecification())
        .AndFilter(new ValidIssueSpecification());

    var issue = await repo.SearchAsync(specs, cancellationToken)
        .ConfigureAwait(false)
        .SingleOrDefault();

    if(issue is null) {
        new EntityNotFoundException<TEntity>(code)
    }

    return issue;
}

On the example above we manage to add a FindByCodeAsync method to our IReadOnlyRepository<Guid, Issue> without having to register a new type on the DI.

These extension methods can be placed in the Domain layer in most cases, since Specifications are also in the Domain Layer. However, when using Include Specifications they will need to be added to the Persistence layer.

Custom Repositories

If after analyzing the option of using extension methods (which is the recommended way for most cases) you still, for some reason, need to create a custom IRepository type, you can do it by following the instructions below.

This is also useful if you want to override/extend any of the Suite repository behaviors.

To get started you'll need to define an interface for your custom repository in your Domain layer.

Your new interface should not be generic and it should extend from IRepository<TId, TEntity> or its readonly variant.

For example:

C#
1
2
3
4
5
6
7
8
public interface IServiceOrderRepository
    : IRepository<Guid, ServiceOrderEntity>
{
    /// custom methods extending IRepository behavior
    Task<ServiceOrderEntity> GetByAssigneeId(
        Guid assigneeId,
        CancellationToken ct = default);
}

Then we need to provide an implementation for your repository interface in your infrastructure layer. You could implement all methods, but the recommended approach is to extend EFCoreRepository<TDbContext, TId, TEntity> and implement only the custom methods that you've added to your interface.

Note that you'd also use your app's DbContext as the TDbContext when extending EFCoreRepository.

C#
// Register our repository using the interface
// That's how we'll inject it later on.
[TransientDependency(typeof(IServiceOrderRepository))]
internal class ServiceOrderRepository
    : EFCoreRepository<MyAppDbContext, Guid, ServiceOrderEntity>,
    IServiceOrderRepository
    where TEntity : class
{
    public ServiceOrderRepository(
        IEFCoreDatabaseContextProvider<MyAppDbContext> dbContextProvider)
        : base(dbContextProvider)
    {
    }

    // Implement our custom repository.
    Task<ServiceOrderEntity> GetByCustomFieldAsync(
        string fieldName,
        object fieldValue,
        CancellationToken ct = default)
    {
        throw new NotImplementedException("TODO Do Something here");
    }
}

Things to note:

  1. We register the repository against the container using the interface. This will allow us to inject it by the interface later on, and we can leave the class internal to our infrastructure layer.
  2. Then we extend from EFCoreRepository so that we don't need to implement IRepository methods.
  3. We implement our custom repository interface and implement its methods.

Then, when we need to inject our repository, we do so by the interface:

C#
public class ServiceOrdersAppService : IAppService
{
    private readonly IServiceOrderRepository serviceOrderRepository;

    public ServiceOrdersAppService(
        // Inject using our custom interface
        IServiceOrderRepository serviceOrderRepository)
    {
        this.serviceOrderRepository = serviceOrderRepository;
    }

    public async Task<IEnumerable<ServiceOrderOutputDTO>> GetAll(CancellationToken ct = default)
    {
        var result = await this.serviceOrderRepository.AllAsync(ct);
        // [..]
    }
}

Use what you need. In a class that you simply need to read inject IReadOnlyRepository, if you need to modify data, inject IRepository. Remember, Least Privilege.

If later on, you realize you need a custom repository, create the interface and implementation. You can keep injecting the generic repository variant in your current code, you just need to register it with the container with both symbols. For example:

C#
[TransientDependency(
    typeof(IServiceOrderRepository),
    typeof(IRepository<Guid, ServiceOrderEntity>),
    typeof(IReadOnlyRepository<Guid, ServiceOrderEntity>))]
public class ServiceOrderRepository
    : EFCoreRepository<MyAppDbContext, Guid, ServiceOrderEntity>
    IServiceOrderRepository
{
   // [..]
}