Skip to content

Extending the Library

Casey edited this page Apr 6, 2025 · 3 revisions

EntityAxis was built with flexibility in mind. While it includes helpful out-of-the-box implementations using MediatR, EF Core, and FluentValidation, none of these are required to use the core abstractions.

This page demonstrates how you can build your own implementations using alternative technologies or application-specific logic while still aligning with the clean abstractions defined in EntityAxis.Abstractions.


🔧 Replaceable by Design

At the core of EntityAxis are interfaces like:

  • ICreate<TEntity, TKey>
  • IUpdate<TEntity, TKey>
  • IDelete<TEntity, TKey>
  • IGetById<TEntity, TKey>
  • IGetAll<TEntity, TKey>
  • IGetPaged<TEntity, TKey>
  • ICommandService<TEntity, TKey>
  • IQueryService<TEntity, TKey>

You are free to implement these interfaces however you want — using Dapper, HTTP APIs, files, or even in-memory stores.


✍️ Writing Your Own Command Handler

Instead of using the built-in MediatR handlers, you can create your own that uses the command service:

public class DeactivateProductHandler : IRequestHandler<DeactivateProductCommand, Guid>
{
    private readonly IUpdate<Product, Guid> _update;
    private readonly IGetById<Product, Guid> _getById;

    public DeactivateProductHandler(IUpdate<Product, Guid> update, IGetById<Product, Guid> getById)
    {
        _update = update;
        _getById = getById;
    }

    public async Task<Guid> Handle(DeactivateProductCommand request, CancellationToken cancellationToken)
    {
        var product = await _getById.GetByIdAsync(request.Id, cancellationToken)
                      ?? throw new EntityNotFoundException(nameof(Product), request.Id);

        product.IsActive = false;

        return await _update.UpdateAsync(product, cancellationToken);
    }
}

public record DeactivateProductCommand(Guid Id) : IRequest<Guid>;

If you're using AddMediatR with assembly scanning, your custom handler will be picked up automatically:

services.AddMediatR(config =>
    config.RegisterServicesFromAssembly(typeof(DeactivateProductHandler).Assembly));

Make sure your handler is public and located in an assembly included in the scan.

ℹ️ This is the preferred way to register handlers, as it keeps your setup clean and modular.

If you want to register your handler manually instead, you can do so like this:

services.AddTransient<IRequestHandler<DeactivateProductCommand, Guid>, DeactivateProductHandler>();

🗂️ Implementing a Dapper-Based Query Service

You can write a fully custom query implementation that uses Dapper instead of EF Core.

public class DapperProductQueryService : IQueryService<Product, Guid>
{
    private readonly IDbConnection _connection;

    public DapperProductQueryService(IDbConnection connection)
    {
        _connection = connection;
    }

    public async Task<Product?> GetByIdAsync(Guid id, CancellationToken cancellationToken = default)
    {
        var sql = "SELECT * FROM Products WHERE Id = @Id";
        return await _connection.QuerySingleOrDefaultAsync<Product>(sql, new { Id = id });
    }

    public Task<List<Product>> GetAllAsync(CancellationToken cancellationToken = default)
    {
        return _connection.QueryAsync<Product>("SELECT * FROM Products")
            .ContinueWith(t => t.Result.ToList());
    }

    public async Task<PagedResult<Product>> GetPagedAsync(int page, int pageSize, CancellationToken cancellationToken = default)
    {
        var sql = @"SELECT * FROM Products ORDER BY Name OFFSET @Offset ROWS FETCH NEXT @PageSize ROWS ONLY;
                    SELECT COUNT(*) FROM Products;";

        using var multi = await _connection.QueryMultipleAsync(sql, new
        {
            Offset = (page - 1) * pageSize,
            PageSize = pageSize
        });

        var items = (await multi.ReadAsync<Product>()).ToList();
        var total = await multi.ReadSingleAsync<int>();

        return new PagedResult<Product>(items, total, page, pageSize);
    }
}

🎯 Creating a CQRS-Aligned Custom Service

Need your own interface and custom business logic? Just implement the core interfaces explicitly:

public class ProductCommandService : IProductCommandService
{
    public Task<Guid> CreateAsync(Product product, CancellationToken cancellationToken = default)
    {
        // Custom business logic here
        return Task.FromResult(product.Id);
    }

    public Task<Guid> UpdateAsync(Product product, CancellationToken cancellationToken = default)
    {
        // Update logic
        return Task.FromResult(product.Id);
    }

    public Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)
    {
        // Delete logic
        return Task.CompletedTask;
    }
}

public interface IProductCommandService : ICommandService<Product, Guid> { }

You can still register this with:

services.AddEntityAxisCommandService<IProductCommandService, ProductCommandService, Product, Guid>();

🧱 Using the Abstractions Without MediatR

You don't have to use MediatR at all. If you prefer calling services directly in your application layer, EntityAxis still helps you organize around consistent abstractions.


♻️ Overriding Default DI Registrations

If you're using the builder-based registration (e.g., AddEntityAxisHandlers), but want to replace one of the default handlers with your own custom implementation, you can do so like this:

public class CustomCreateProductHandler
    : IRequestHandler<CreateEntityCommand<ProductCreateModel, Product, Guid>, Guid>
{
    private readonly ICreate<Product, Guid> _create;

    public CustomCreateProductHandler(ICreate<Product, Guid> create)
    {
        _create = create;
    }

    public async Task<Guid> Handle(CreateEntityCommand<ProductCreateModel, Product, Guid> request, CancellationToken cancellationToken)
    {
        var product = new Product
        {
            Id = Guid.NewGuid(),
            Name = request.Model.Name,
        };

        return await _create.CreateAsync(product, cancellationToken);
    }
}

Then override the default DI registration like this:

services.AddEntityAxisHandlers<ProductCreateModel, ProductUpdateModel, Product, Guid>();

// This MUST match the exact generic signature that EntityAxis registered
services.AddTransient<IRequestHandler<CreateEntityCommand<ProductCreateModel, Product, Guid>, Guid>, CustomCreateProductHandler>();

This ensures that your custom handler takes precedence while leaving the rest of the registrations intact.


✅ Summary

  • EntityAxis provides base implementations, but you are never locked in.
  • The abstractions were designed so you can integrate any underlying tech (EF, Dapper, APIs, etc.).
  • Handlers, services, validators, and registration are all swappable.
  • This makes EntityAxis suitable for everything from enterprise apps to microservices to vertical slices.

📚 See Also

Clone this wiki locally