Flowsy.Repository.Sql 2.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Flowsy.Repository.Sql --version 2.0.0                
NuGet\Install-Package Flowsy.Repository.Sql -Version 2.0.0                
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Flowsy.Repository.Sql" Version="2.0.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Flowsy.Repository.Sql --version 2.0.0                
#r "nuget: Flowsy.Repository.Sql, 2.0.0"                
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Flowsy.Repository.Sql as a Cake Addin
#addin nuget:?package=Flowsy.Repository.Sql&version=2.0.0

// Install Flowsy.Repository.Sql as a Cake Tool
#tool nuget:?package=Flowsy.Repository.Sql&version=2.0.0                

Flowsy Repository Sql

Repository implementation for SQL databases.

Description

This package is an implementation of the abstractions defined in Flowsy Repository Core and offers a simple way of creating repositories targeting SQL databases, designed to perform their tasks through stored routines of the underlying database.

To achieve this goal, the only requirement is to keep our database aligned to certain conventions, which is, by any means, necessary even for projects not using this package.

These conventions can be customized and set as the default values for all the repositories of our applications, but also set individually for every repository.

Key Concepts

  • A repository handles a single type of entity.
  • A repository expects certain stored routines to exist to perform actions on the underlying database.
  • A repository will determine the name of the routines to execute based on the name of the entity and certain configurable naming conventions.
  • The DbRepository class is intended to be the base class of the repositories needed by the application.
    • It implements the basic functionality to execute queries.
    • Its methods are declared as virtual, so any subclass can customize the behavior as needed.
  • Flowsy Repository Sql relies on Dapper to perform queries and map database records to entities.

Quick Start

1. Configuration

The simplest way to configure all the repositories at once is to set the Default property of DbRepositoryConfiguration. The only requirement would be to keep the same naming conventions throughout all of our database objects.

DbRepositoryConfiguration.Default = new DbRepositoryConfiguration(
    connectionKey: "Default", // The configuration to establish database connections will be resolved by a key named "Default"
    schemaName: "public", // The stored routines are in the 'public' schema
    resolveIdentityPropertyName: entityType => $"{entityType.Name}Id", // Customer -> CustomerId
    autoIdentity: true, // Auto increment primary keys
    routineConvention: new DbConvention(NamingConvention.LowerSnakeCase, "fn_", string.Empty), // fn_ prefix for stored routines
    parameterConvention: new DbConvention(NamingConvention.LowerSnakeCase, "p_", string.Empty), // p_ prefix for stored routine parameters
    enumFormat: DbEnumFormat.Name // Use the string representation instead of the ordinal value for enums when executing queries
    );

If required, each repository can be configured independently by calling:

DbRepositoryConfiguration.Register(typeof(MyRepository), new DbRepositoryConfiguration(/* ... */));

All the parameters not set in the DbRepositoryConfiguration constructor for a specific repository type, will fallback to the settings of DbRepositoryConfiguration.Default.

At a high level, the stored routines to be invoked by the repositories are represented by instances of DbRepositoryAction. The DbRepositoryConfiguration class has a set of properties to configure the built-in actions supported by DbRepository.

2. Domain Modeling

The only requirement for our entities is to implement IEntity. The IEntity interface has no methods and is used only to mark classes within an assembly to be treated as entities of a repository.

// Customer.cs
using Flowsy.Repository.Core;

public class Customer : IEntity
{
    // Basic customer information
    
    public int CustomerId { get; set; }
    
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    
    public DateTime CreatedAt { get; set; } 
    public DateTime? UpdatedAt { get; set; } 
}

// Payment.cs
using Flowsy.Repository.Core;

public class Payment : IEntity
{
    // Information of every payment made by a customer

    public long PaymentId { get; set; }
    public int CustomerId { get; set; }
    
    public decimal Amount { get; set; }
    public string Comment { get; set; }
    
    public DateTime CreatedAt { get; set; }
}

// CustomerExtended.cs
using Flowsy.Repository.Core;

public class CustomerExtended : Customer
{
     // All the basic information of a customer plus a total paid computed in some query
     public decimal TotalPaid { get; set; }
} 

3. Define Interfaces

// ICustomerRepository.cs
using Flowsy.Repository.Core;

// ICustomerRepository inherits all the methods of IRepository
// and adds the specific methods required to manage Customer entities 
public interface ICustomerRepository : IRepository<Customer, int>
{  
    Task<T?> GetOneExtendedAsync<T>(string email, CancellationToken cancellationToken);
}

// IPaymentRepository.cs
using Flowsy.Repository.Core;

// IPaymentRepository inherits all the methods of IRepository 
public interface IPaymentRepository : IRepository<Payment, long>
{
}

4. Implementation Details

// CustomerRepository.cs
// By inheriting from DbRepository, CustomerRepository gets the basic implementation
// for create, update, patch, delete, and find entities with no additional code.
// The only methods to be implemented are the one defined in ICustomerRepository. 
public class CustomerRepository : DbRepository<Customer, int>, ICustomerRepository
{
    public CustomerRepository(IDbConnectionFactory connectionFactory) : base(connectionFactory)
    {
    }

    public CustomerRepository(IDbTransaction transaction) : base(transaction)
    {
    }

    // Search by email and return an object of type CustomerExtended.
    public Task<CustomerExtended?> GetOneExtendedAsync(string email, CancellationToken cancellationToken)
    {
        return GetOneExtendedAsync<CustomerExtended>(email, cancellationToken);
    }

    // Search by email and return and object of type T which shall expose the same properties as CustomerExtended.  
    public Task<T?> GetOneExtendedAsync<T>(string email, CancellationToken cancellationToken)
    {
        // The following version of the GetOneExtendedAsync method is inherited from DbRepository and accepts a generic filter as argument.
        // This version of the method will resolve the name and parameters of the stored routine to execute:
        // public.fn_customer_select_one_extended(p_email text)
        // We need to create that stored routine in our database, which shall execute a query to join the required tables and return the expected result.
        return GetOneExtendedAsync<T>(
            new 
            { 
                Email = email
            },
            cancellationToken
            );
    }
}

// PaymentRepository.cs
// In this example, only two constructors are needed to have a fully-functional repository able to
// create, update, delete and retrieve entities using all the functionallity inherited from DbRepository.
public class PaymentRepository : DbRepository<Payment, long>, IPaymentRepository
{
    public PaymentRepository(IDbConnectionFactory connectionFactory) : base(connectionFactory)
    {
    }

    public PaymentRepository(IDbTransaction transaction) : base(transaction)
    {
    }
}

5. Using the Repositories

The following examples assume our application is using Dependency Injection.

5.1 Configure a Connection Factory

All the repositories can be instantiated by the dependency injection system using a single connection factory.

builder.Services.AddSingleton<IDbConnectionFactory>(serviceProvider => {
    var configuration = serviceProvider.GetRequiredService<IConfiguration>();
    var connectionConfigurations = new List<DbConnectionConfiguration>();
    // Populate connection configuration list from the IConfiguration instance
    return new DbConnectionFactory(connectionConfigurations.ToArray());
    });

5.2. Create a customer

public class CreateCustomerCommandHandler
{
    private readonly ICustomerRepository _customerRepository;
    
    public CreateCustomerCommandHandler(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }
    
    public async Task<CreateCustomerCommandResult> Handle(CreateCustomerCommand command, CancellationToken)
    {
        var customer = new Customer();
        // Populate customer from command
        
        var customerId = await _customerRepository.CreateAsync(customer, cancellationToken);
        
        return new CreateCustomerCommandResult(customerId);
    } 
}

5.3. Search customer by email to see their total paid.

public class CustomerByEmailQueryHandler
{
    private readonly ICustomerRepository _customerRepository;
    
    public CustomerByEmailQueryHandler(ICustomerRepository customerRepository)
    {
        _customerRepository = customerRepository;
    }
    
    public async Task<IEnumerable<CustomerExtended?>> Handle(CustomerByEmailQuery query, CancellationToken)
    {
        return _customerRepository.GetOneExtendedAsync(query.Email, cancellationToken);
    } 
}

5.4. Unit of Work

The class DbUnitOfWork implements the interface IUnitOfWork defined in the package Flowsy Repository Core. To create units of work, we can think about a specific use case and see what repositories are involved to complete the task.

For example, to create an invoice we also need to create invoice items inside an atomic operation, so first we can define the following interface:

using Flowsy.Repository.Core;

public interface ICreateInvoiceUnitOfWork : IUnitOfWork
{
    IInvoiceRepository InvoiceRepository { get; }
    IInvoiceItemRepository InvoiceItemRepository { get; }
}

Then, we implement that specific unit of work:

public class CreateInvoiceUnitOfWork : DbUnitOfWork
{
    private IInvoiceRepository? _invoiceRepository;
    private IInvoiceItemRepository? _invoiceItemRepository;
    
    public CreateInvoiceUnitOfWork(IDbConnection connection) : base(connection)
    {
    }
    
    public IInvoiceRepository InvoiceRepository
        => _invoiceRepository ??= new InvoiceRepository(Transaction);
    
    public IInvoiceItemRepository InvoiceItemRepository
        => _invoiceItemRepository ??= new InvoiceItemRepository(Transaction);
        
    // Transaction is an instance of IDbTransaction, defined as a protected property of DbUnitOfWork  
}

Then, we create a unit of work factory to create instances of CreateInvoiceUnitOfWork as required:

using Flowsy.Repository.Core;

public class DbUnitOfWorkFactory : IUnitOfWorkFactory
{
    private readonly IDbConnectionFactory _connectionFactory;
    
    public DbUnitOfWorkFactory(IDbConnectionFactory connectionFactory)
    {
        _connectionFactory = connectionFactory;
    }

    public T Create<T>(string dataStoreKey = "Default") where T : IUnitOfWork
    {
        IUnitOfWork? unitOfWork = null;
        
        var type = typeof(T);
        
        // Create IUnitOfWork instance
        if (type == typeof(ICreateInvoiceUnitOfWork))
            unitOfWork = new CreateInvoiceUnitOfWork(_dbConnectionFactory.GetConnection(dataStoreKey));

        return (T) (unitOfWork ?? throw new NotSupportedException());
    }
}

And finally the application can execute a command that safely runs a transaction without needing to know what kind of database or fancy mechanisms were used to do the job.

public class CreateInvoiceCommandHandler
{
    private readonly IUnitOfWorkFactory _unitOfWorkFactory;
    
    public CreateInvoiceCommandHandler(IUnitOfWorkFactory unitOfWorkFactory)
    {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public async Task<CreateInvoiceCommandResult> Handle(CreateInvoiceCommand command, CancellationToken cancellationToken)
    {
        // Begin operation
        // IUnitOfWork inherits from IDisposable and IAsyncDisposable, if any exception is thrown, the current operation shall be rolled back
        await using var unitOfWork = _unitOfWorkFactory.Create<ICreateInvoiceUnitOfWork>();

        var invoice = new Invoice();
        // Populate invoice object from properties of command object 
        
        // Create the Invoice entity
        var invoiceId = await unitOfWork.InvoiceRepository.CreateAsync(invoice, cancellationToken);
        
        // Create all the InvoiceItem entities
        foreach (var item in command.Items)
        {
            var invoiceItem = new InvoiceItem();
            // Populate invoiceItem object from properties of item object
            
            // Create each InvoiceItem entity
            await unitOfWork.InvoiceItemRepository.CreateAsync(invoiceItem, cancellationToken); 
        }

        // Commit the current operation        
        await unitOfWork.CommitAsync(cancellationToken);
        
        // Return the result of the operation
        return new CreateInvoiceCommandResult
        {
            InvoiceId = invoiceId
        };
    }
}
Product Compatible and additional computed target framework versions.
.NET net6.0 is compatible.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
20.3.0 196 8/27/2023
20.2.0 124 8/27/2023
20.1.2 126 8/25/2023
20.1.1 184 7/25/2023
20.1.0 237 6/30/2023
20.0.1 166 6/9/2023
20.0.0 173 6/5/2023
19.0.0 182 6/5/2023
18.2.7 166 6/2/2023
18.2.6 214 4/20/2023
18.2.5 218 4/12/2023
18.2.4 256 3/25/2023
18.2.3 215 3/25/2023
18.2.2 303 3/14/2023
18.2.1 257 3/4/2023
18.2.0 249 3/3/2023
18.1.2 277 2/27/2023
18.1.1 258 2/27/2023
18.1.0 265 2/27/2023
18.0.0 239 2/27/2023
17.0.0 271 2/25/2023
16.0.0 264 2/25/2023
15.1.0 273 2/25/2023
15.0.0 267 2/24/2023
14.0.1 267 2/24/2023
14.0.0 277 2/23/2023
13.0.0 275 2/23/2023
12.2.0 264 2/23/2023
12.1.0 255 2/22/2023
12.0.2 349 1/26/2023
12.0.1 290 1/26/2023
12.0.0 328 1/3/2023
11.0.0 307 1/3/2023
10.1.4 338 12/17/2022
10.1.3 301 12/17/2022
10.1.2 323 12/17/2022
10.1.1 327 12/17/2022
10.1.0 294 12/15/2022
10.0.2 298 12/9/2022
10.0.1 283 12/9/2022
10.0.0 323 11/21/2022
9.0.0 335 11/21/2022
8.0.1 350 11/20/2022
8.0.0 336 11/16/2022
7.0.4 362 11/14/2022
7.0.3 338 11/13/2022
7.0.2 346 11/13/2022
7.0.1 364 11/13/2022
7.0.0 371 11/13/2022
6.2.0 358 11/13/2022
6.1.0 340 11/13/2022
6.0.0 364 11/13/2022
4.0.3 378 11/10/2022
4.0.2 356 11/8/2022
4.0.1 344 11/7/2022
4.0.0 356 11/7/2022
3.0.0 370 11/7/2022
2.0.4 371 11/6/2022
2.0.3 360 11/6/2022
2.0.2 346 11/3/2022
2.0.1 377 10/27/2022
2.0.0 385 10/27/2022
1.0.2 400 10/25/2022
1.0.1 371 10/25/2022
1.0.0 389 10/25/2022