Rochas.DapperRepository 1.9.8

dotnet add package Rochas.DapperRepository --version 1.9.8
                    
NuGet\Install-Package Rochas.DapperRepository -Version 1.9.8
                    
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="Rochas.DapperRepository" Version="1.9.8" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Rochas.DapperRepository" Version="1.9.8" />
                    
Directory.Packages.props
<PackageReference Include="Rochas.DapperRepository" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Rochas.DapperRepository --version 1.9.8
                    
#r "nuget: Rochas.DapperRepository, 1.9.8"
                    
#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.
#:package Rochas.DapperRepository@1.9.8
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Rochas.DapperRepository&version=1.9.8
                    
Install as a Cake Addin
#tool nuget:?package=Rochas.DapperRepository&version=1.9.8
                    
Install as a Cake Tool

Rochas.DapperRepository

English | Português | Español | Français | Deutsch


English

What is it?

Rochas.DapperRepository is a lightweight generic repository for data access built on top of Dapper. It follows the Repository and Unit of Work patterns described by Martin Fowler, with no new query language, no state tracking and no dynamic proxies. It is designed to work directly with POCOs decorated with metadata attributes that drive querying, persistence and entity relationships.

It supports SQL Server, MySQL, PostgreSQL and SQLite through the DatabaseEngine enum.

The package targets .NET Standard 2.1 and is published on NuGet as Rochas.DapperRepository (current version 1.9.5).

Features

  • Full CRUD (Add, AddRange, Update, Remove, Get) with sync and async overloads
  • Typed filter queries (Query), with AND/OR conjunction via filterConjunction
  • Tokenized search over [Filterable] properties (Search)
  • Pagination returning PaginatedResult<T>
  • Fluent query builder with OrderBy, OrderByDescending, GroupBy (with aggregates) and Paginate — the builder is awaitable and generates SQL in the correct order (SELECT → FROM → WHERE → GROUP BY → ORDER BY)
  • Range filters with [RangeFilter]
  • Automatic composition loading with [RelatedEntity] (1-1, 1-N, N-1, N-N)
  • Pluggable caching via ICacheProvider (in-memory, Redis/Garnet, composite L1/L2, master-to-slave persistence channel)
  • DW/ETL support with [RelationalColumn] (automatic JOIN generation) and [DataAggregationColumn] (SUM, COUNT, MIN, MAX, AVG)
  • Raw SQL queries (QueryRaw)

Installation

dotnet add package Rochas.DapperRepository

Quick start

using Rochas.DapperRepository;
using Rochas.Data.Specification.Annotations;
using Rochas.Data.Specification.Enums;

var connString = "Data Source=sample.db;Cache=Shared";

using var repo = new GenericRepository<SampleEntity>(DatabaseEngine.SQLite, connString);

var result = await repo.Search("Celulares");

Registration in DI

services.AddScoped<IGenericRepository<SampleEntity>>(provider =>
    new GenericRepository<SampleEntity>(
        DatabaseEngine.SQLite,
        configuration.GetConnectionString("Default")));

Then inject IGenericRepository<SampleEntity> into your services.

Entity example

[Table] and [Column] are optional. When omitted, the class name is used for the table and the property name for the column. Use them when your database uses different names (for example, snake_case):

[Cacheable]
[Table("sample_entities")]
public class SampleEntity
{
    [Key]
    [AutoGenerated]
    public int Id { get; set; }

    [Column("creation_date")]
    public DateTime CreationDate { get; set; }

    [RangeFilter(LinkedRangeProperty = "CreationDateEnd")]
    public DateTime CreatedAt { get; set; }

    [NotMapped]
    public DateTime CreationDateEnd { get; set; }

    [Filterable]
    public string Name { get; set; }

    public int Age { get; set; }

    public bool Active { get; set; }

    [RelatedEntity(Cardinality = RelationCardinality.OneToMany,
                   ForeignKeyAttribute = "ParentId")]
    public IList<ChildEntity> Childs { get; set; }
}

CRUD operations

repo.Add(new SampleEntity { Name = "Renato Rocha" });

repo.AddRange(list);

var filter = new SampleEntity { DocNumber = 12345 };
var entityToUpdate = await repo.Get(filter);
entityToUpdate.Age = 40;
int affected = await repo.Update(entityToUpdate, filter);

int removed = await repo.Remove(filter);

Query (typed filter)

var all = await repo.Query(new SampleEntity()); // all records

var filter = new SampleEntity { Name = "Roberto", Email = "gmail.com" };
var results = await repo.Query(filter, filterConjunction: true);

Use the filterConjunction parameter to define the behavior of the attributes in the query: logical AND when enabled, logical OR when disabled. With conjunction the applied criterion is equality, otherwise similarity.

Search (tokenized over [Filterable] properties)

var results = await repo.Search("Paulo");

var page = await repo.Search("Paulo", page: 1, pageSize: 20);

Pagination

var result = await repo.Query(filter, page: 1, pageSize: 10);

var result = repo.QuerySync(filter, page: 1, pageSize: 10);

PaginatedResult<T> exposes Items, TotalCount, Page, PageSize and PageCount.

Fluent query builder

var result = await repo.Query(filter)
    .OrderBy("Name")
    .Paginate(1, 10);

var result = await repo.Query(filter)
    .GroupBy("Category")
    .OrderByDescending("Price");

var list = repo.QuerySync(filter)
    .OrderBy("Name")
    .ToList();

The order of the methods does not matter; the builder collects the sorting and grouping attributes and executes the query in the correct SQL order.

GroupBy with aggregates

var aggregates = new Dictionary<string, DataAggregationType>
{
    { "Price", DataAggregationType.Sum },
    { "Price", DataAggregationType.Average }
};
var result = await repo.Query(filter).GroupBy(["Category"], aggregates);

The builder generates the SELECT ... GROUP BY ... automatically with the requested aggregation functions (SUM, COUNT, MIN, MAX, AVG).

DW/ETL fact entities

For Data Warehouse and ETL work, [RelationalColumn] generates automatic JOINs (INNER/LEFT) with dimensional tables and [DataAggregationColumn] generates computed aggregate columns:

[Table("fact_sales")]
public class FactSalesEntity
{
    [Key] [AutoGenerated]
    public int Id { get; set; }

    [Column("product_id")]
    public int ProductId { get; set; }

    [Column("total_amount")]
    public decimal TotalAmount { get; set; }

    // Automatic JOIN: dim_product.product_name AS ProductName
    [RelationalColumn(
        TableName = "dim_product",
        ColumnName = "product_name",
        ColumnAlias = "ProductName",
        KeyColumn = "product_id",
        ForeignKeyColumn = "id",
        JunctionType = RelationalJunctionType.Mandatory)]
    public string ProductName { get; set; }

    // Aggregation: SUM(fact_sales.total_amount) AS SumTotalAmount
    [DataAggregationColumn(ColumnName = "total_amount", AggregationType = DataAggregationType.Sum)]
    public decimal SumTotalAmount { get; set; }
}

var result = await repo.Query(new FactSalesEntity());

Generated SQL:

SELECT dim_product.product_name AS ProductName,
       SUM(fact_sales.total_amount) AS SumTotalAmount,
       fact_sales.id, fact_sales.product_id, fact_sales.total_amount
FROM fact_sales
INNER JOIN dim_product ON fact_sales.product_id = dim_product.id
WHERE 1 = 1

RelationalJunctionType.Mandatory generates INNER JOIN; Optional generates LEFT JOIN. Properties marked with [RelationalColumn] or [DataAggregationColumn] are read-only and ignored in INSERT/UPDATE operations.

Pluggable caching

Mark the entity as [Cacheable] to enable caching. The default provider is in-memory, but Redis, Garnet or composite providers can be injected without changing repository code:

DataCache.Initialize(memorySizeLimit: 100); // in-memory, 100 MB limit
DataCache.Initialize();                     // in-memory, no limit

DataCache.Initialize(new DistributedCacheProvider("localhost:6379")); // Redis / Garnet

DataCache.Initialize(new CompositeCacheProvider( // L1 in-memory + L2 distributed
    new InMemoryCacheProvider(),
    new DistributedCacheProvider("redis:6379")));

Benchmark — DapperRepository vs EF Core

Windows 11, Intel i5-7500T, .NET 9.0, SQLite, 5,000 rows

Scenario EF Core DapperRepository Winner
InsertIndividual 3.2 ms 3.1 ms ORM 1.1x
BulkInsert_100 32.8 ms 33.5 ms Tie
GetById 526 μs 409 μs ORM 1.3x
Update_Individual 2.9 ms 2.7 ms ORM 1.1x
Delete_Individual 3.2 ms 3.5 ms Tie
CountSync 508 μs 536 μs Tie
Search_Filterable 1.0 ms 1.2 ms Tie
Sort_5000_rows_ORDER_BY 61.6 ms 38.6 ms ORM 1.6x
Sort_MultiColumn 61 ms 41 ms ORM 1.5x
GroupBy_Simple 7.3 ms 1.0 ms ORM 7.3x
GroupBy_AggAll 3.1 ms 1.0 ms ORM 3.1x
GroupBy_Having 2.5 ms 1.1 ms ORM 2.3x
QueryRaw_Select 3.5 ms 3.2 ms ORM 1.1x
SearchPaginated 2.5 ms 3.1 ms Tie*

Benchmark — Channel vs Replica (Cluster Replication)

PersistenceChannel (CacheIndexer) vs DapperRepository native replicaConnStrings, 3-node SQLite cluster

Scenario Channel Replica Speedup
Single Invoice 115 ms (8.7 inv/s) 19 ms (52.6 inv/s) Replica 6x faster*
Bulk 100 585 ms (171 inv/s) 9,479 ms (10.5 inv/s) Channel 16.2x
Bulk 1,000 6,679 ms (150 inv/s) 70,835 ms (14.1 inv/s) Channel 10.6x

*Single Invoice: Replica wins because there is no contention; the AddSync path hits one DB directly without lock overhead. Bulk: Channel wins because the PersistenceChannel decouples serialization from persistence via fan-out, while Replica's Parallel.ForEach + Mutex lock creates severe contention under load (P99 638 ms, Max 2,822 ms).

Tests and coverage

The Rochas.DapperRepository.Test project (xUnit, net9.0) contains 227 tests covering async CRUD, composition, builders, GROUP BY/aggregates, pluggable cache, SQL parsers, type handlers and engine detection, with 80% line coverage on the main assembly and 100% on Rochas.Data.Specification.


Português

O que é?

Rochas.DapperRepository é um repositório genérico leve para acesso a dados construído sobre o Dapper. Segue os padrões de Repositório e Unidade de Trabalho descritos por Martin Fowler, sem novas linguagens de consulta, sem controle de estado e sem proxies dinâmicos. Foi desenhado para uso direto com POCOs decorados com atributos de metadados que orientam a consulta, a persistência e os relacionamentos entre entidades.

Suporta SQL Server, MySQL, PostgreSQL e SQLite através do enum DatabaseEngine.

O pacote tem como alvo o .NET Standard 2.1 e é publicado no NuGet como Rochas.DapperRepository (versão atual 1.9.5).

Funcionalidades

  • CRUD completo (Add, AddRange, Update, Remove, Get) com sobrecargas síncronas e assíncronas
  • Consultas por filtro tipado (Query), com conjunção E/OU via filterConjunction
  • Busca tokenizada sobre propriedades [Filterable] (Search)
  • Paginação retornando PaginatedResult<T>
  • Builder fluente de consultas com OrderBy, OrderByDescending, GroupBy (com agregações) e Paginate — o builder é awaitable e gera o SQL na ordem correta (SELECT → FROM → WHERE → GROUP BY → ORDER BY)
  • Filtros de intervalo com [RangeFilter]
  • Leitura automática de composição com [RelatedEntity] (1-1, 1-N, N-1, N-N)
  • Cache plugável via ICacheProvider (in-memory, Redis/Garnet, composto L1/L2, canal de persistência master→slave)
  • Suporte a DW/ETL com [RelationalColumn] (geração automática de JOIN) e [DataAggregationColumn] (SUM, COUNT, MIN, MAX, AVG)
  • Consultas SQL diretas (QueryRaw)

Instalação

dotnet add package Rochas.DapperRepository

Exemplo de uso rápido

using Rochas.DapperRepository;
using Rochas.Data.Specification.Annotations;
using Rochas.Data.Specification.Enums;

var connString = "Data Source=sample.db;Cache=Shared";

using var repo = new GenericRepository<SampleEntity>(DatabaseEngine.SQLite, connString);

var result = await repo.Search("Celulares");

Registro no DI

services.AddScoped<IGenericRepository<SampleEntity>>(provider =>
    new GenericRepository<SampleEntity>(
        DatabaseEngine.SQLite,
        configuration.GetConnectionString("Default")));

Em seguida, injete IGenericRepository<SampleEntity> nos seus serviços.

Exemplo de entidade

[Table] e [Column] são opcionais. Quando omitidos, o nome da classe é usado para a tabela e o nome da propriedade para a coluna. Use-os quando o banco de dados usar nomes diferentes (por exemplo, snake_case):

[Cacheable]
[Table("sample_entities")]
public class SampleEntity
{
    [Key]
    [AutoGenerated]
    public int Id { get; set; }

    [Column("creation_date")]
    public DateTime CreationDate { get; set; }

    [RangeFilter(LinkedRangeProperty = "CreationDateEnd")]
    public DateTime CreatedAt { get; set; }

    [NotMapped]
    public DateTime CreationDateEnd { get; set; }

    [Filterable]
    public string Name { get; set; }

    public int Age { get; set; }

    public bool Active { get; set; }

    [RelatedEntity(Cardinality = RelationCardinality.OneToMany,
                   ForeignKeyAttribute = "ParentId")]
    public IList<ChildEntity> Childs { get; set; }
}

Operações CRUD

repo.Add(new SampleEntity { Name = "Renato Rocha" });

repo.AddRange(list);

var filter = new SampleEntity { DocNumber = 12345 };
var entityToUpdate = await repo.Get(filter);
entityToUpdate.Age = 40;
int affected = await repo.Update(entityToUpdate, filter);

int removed = await repo.Remove(filter);

Query (consulta por filtro tipado)

var all = await repo.Query(new SampleEntity()); // listar todos

var filter = new SampleEntity { Name = "Roberto", Email = "gmail.com" };
var results = await repo.Query(filter, filterConjunction: true);

Utilize o parâmetro filterConjunction para definir o comportamento dos atributos na consulta: conjunção lógica E quando ligado, disjunção lógica OU quando desligado. Com conjunção o critério aplicado é de igualdade, caso contrário de semelhança.

Search (busca tokenizada em propriedades [Filterable])

var results = await repo.Search("Paulo");

var page = await repo.Search("Paulo", page: 1, pageSize: 20);

Paginação

var result = await repo.Query(filter, page: 1, pageSize: 10);

var result = repo.QuerySync(filter, page: 1, pageSize: 10);

PaginatedResult<T> expõe Items, TotalCount, Page, PageSize e PageCount.

Builder fluente de consultas

var result = await repo.Query(filter)
    .OrderBy("Name")
    .Paginate(1, 10);

var result = await repo.Query(filter)
    .GroupBy("Category")
    .OrderByDescending("Price");

var list = repo.QuerySync(filter)
    .OrderBy("Name")
    .ToList();

A ordem dos métodos não importa; o builder coleta os atributos de ordenação e agrupamento e executa a consulta na ordem correta do SQL.

GroupBy com agregações

var aggregates = new Dictionary<string, DataAggregationType>
{
    { "Price", DataAggregationType.Sum },
    { "Price", DataAggregationType.Average }
};
var result = await repo.Query(filter).GroupBy(["Category"], aggregates);

O builder gera o SELECT ... GROUP BY ... automaticamente com as funções de agregação solicitadas (SUM, COUNT, MIN, MAX, AVG).

Entidades fato DW/ETL

Para trabalhos de Data Warehouse e ETL, [RelationalColumn] gera JOINs automáticos (INNER/LEFT) com tabelas dimensionais e [DataAggregationColumn] gera colunas calculadas de agregação:

[Table("fact_sales")]
public class FactSalesEntity
{
    [Key] [AutoGenerated]
    public int Id { get; set; }

    [Column("product_id")]
    public int ProductId { get; set; }

    [Column("total_amount")]
    public decimal TotalAmount { get; set; }

    // JOIN automático: dim_product.product_name AS ProductName
    [RelationalColumn(
        TableName = "dim_product",
        ColumnName = "product_name",
        ColumnAlias = "ProductName",
        KeyColumn = "product_id",
        ForeignKeyColumn = "id",
        JunctionType = RelationalJunctionType.Mandatory)]
    public string ProductName { get; set; }

    // Agregação: SUM(fact_sales.total_amount) AS SumTotalAmount
    [DataAggregationColumn(ColumnName = "total_amount", AggregationType = DataAggregationType.Sum)]
    public decimal SumTotalAmount { get; set; }
}

var result = await repo.Query(new FactSalesEntity());

SQL gerado:

SELECT dim_product.product_name AS ProductName,
       SUM(fact_sales.total_amount) AS SumTotalAmount,
       fact_sales.id, fact_sales.product_id, fact_sales.total_amount
FROM fact_sales
INNER JOIN dim_product ON fact_sales.product_id = dim_product.id
WHERE 1 = 1

RelationalJunctionType.Mandatory gera INNER JOIN; Optional gera LEFT JOIN. Propriedades marcadas com [RelationalColumn] ou [DataAggregationColumn] são somente leitura e ignoradas em operações de INSERT/UPDATE.

Cache plugável

Marque a entidade com [Cacheable] para habilitar o cache. O provedor padrão é in-memory, mas provedores Redis, Garnet ou compostos podem ser injetados sem alterar o código do repositório:

DataCache.Initialize(memorySizeLimit: 100); // in-memory, limite de 100 MB
DataCache.Initialize();                     // in-memory, sem limite

DataCache.Initialize(new DistributedCacheProvider("localhost:6379")); // Redis / Garnet

DataCache.Initialize(new CompositeCacheProvider( // L1 in-memory + L2 distribuído
    new InMemoryCacheProvider(),
    new DistributedCacheProvider("redis:6379")));

📊 Benchmark — DapperRepository vs EF Core

Windows 11, Intel i5-7500T, .NET 9.0, SQLite, 5.000 linhas

Cenário EF Core DapperRepository Vitória
InsertIndividual 3.2 ms 3.1 ms ORM 1.1x
BulkInsert_100 32.8 ms 33.5 ms Empate
GetById 526 μs 409 μs ORM 1.3x
Update_Individual 2.9 ms 2.7 ms ORM 1.1x
Delete_Individual 3.2 ms 3.5 ms Empate
CountSync 508 μs 536 μs Empate
Search_Filterable 1.0 ms 1.2 ms Empate
Sort_5000_rows_ORDER_BY 61.6 ms 38.6 ms ORM 1.6x
Sort_MultiColumn 61 ms 41 ms ORM 1.5x
GroupBy_Simple 7.3 ms 1.0 ms ORM 7.3x
GroupBy_AggAll 3.1 ms 1.0 ms ORM 3.1x
GroupBy_Having 2.5 ms 1.1 ms ORM 2.3x
QueryRaw_Select 3.5 ms 3.2 ms ORM 1.1x
SearchPaginated 2.5 ms 3.1 ms Empate*

Benchmark — Channel vs Replica (Replicação em Cluster)

PersistenceChannel (CacheIndexer) vs DapperRepository nativo replicaConnStrings, cluster SQLite de 3 nós

Cenário Channel Replica Aceleração
Invoice Única 115 ms (8,7 inv/s) 19 ms (52,6 inv/s) Replica 6x mais rápido*
Bulk 100 585 ms (171 inv/s) 9.479 ms (10,5 inv/s) Channel 16,2x
Bulk 1.000 6.679 ms (150 inv/s) 70.835 ms (14,1 inv/s) Channel 10,6x

*Invoice Única: Replica vence porque não há contenção; o caminho AddSync atinge um DB diretamente sem overhead de lock. Bulk: Channel vence porque o PersistenceChannel desacopla serialização de persistência via fan-out, enquanto o Parallel.ForEach + Mutex do Replica cria contenção severa sob carga (P99 638 ms, Max 2.822 ms).

Testes e cobertura

O projeto Rochas.DapperRepository.Test (xUnit, net9.0) contém 227 testes cobrindo CRUD assíncrono, composição, builders, GROUP BY/agregações, cache plugável, parsers SQL, type handlers e detecção de engine, com 80% de cobertura de linha no assembly principal e 100% em Rochas.Data.Specification.


Español

¿Qué es?

Rochas.DapperRepository es un repositorio genérico ligero para acceso a datos construido sobre Dapper. Sigue los patrones de Repositorio y Unidad de Trabajo descritos por Martin Fowler, sin nuevos lenguajes de consulta, sin seguimiento de estado y sin proxies dinámicos. Está diseñado para trabajar directamente con POCOs decorados con atributos de metadatos que guían las consultas, la persistencia y las relaciones entre entidades.

Es compatible con SQL Server, MySQL, PostgreSQL y SQLite a través del enum DatabaseEngine.

El paquete apunta a .NET Standard 2.1 y se publica en NuGet como Rochas.DapperRepository (versión actual 1.9.5).

Características

  • CRUD completo (Add, AddRange, Update, Remove, Get) con sobrecargas síncronas y asíncronas
  • Consultas por filtro tipado (Query), con conjunción Y/O mediante filterConjunction
  • Búsqueda tokenizada sobre propiedades [Filterable] (Search)
  • Paginación que devuelve PaginatedResult<T>
  • Constructor fluido de consultas con OrderBy, OrderByDescending, GroupBy (con agregaciones) y Paginate — el builder es awaitable y genera el SQL en el orden correcto (SELECT → FROM → WHERE → GROUP BY → ORDER BY)
  • Filtros de rango con [RangeFilter]
  • Carga automática de composición con [RelatedEntity] (1-1, 1-N, N-1, N-N)
  • Caché conectable mediante ICacheProvider (en memoria, Redis/Garnet, compuesto L1/L2, canal de persistencia maestro→esclavo)
  • Soporte DW/ETL con [RelationalColumn] (generación automática de JOIN) y [DataAggregationColumn] (SUM, COUNT, MIN, MAX, AVG)
  • Consultas SQL directas (QueryRaw)

Instalación

dotnet add package Rochas.DapperRepository

Inicio rápido

using Rochas.DapperRepository;
using Rochas.Data.Specification.Annotations;
using Rochas.Data.Specification.Enums;

var connString = "Data Source=sample.db;Cache=Shared";

using var repo = new GenericRepository<SampleEntity>(DatabaseEngine.SQLite, connString);

var result = await repo.Search("Celulares");

Registro en DI

services.AddScoped<IGenericRepository<SampleEntity>>(provider =>
    new GenericRepository<SampleEntity>(
        DatabaseEngine.SQLite,
        configuration.GetConnectionString("Default")));

Luego inyecte IGenericRepository<SampleEntity> en sus servicios.

Ejemplo de entidad

[Table] y [Column] son opcionales. Cuando se omiten, se usa el nombre de la clase para la tabla y el nombre de la propiedad para la columna. Úselos cuando la base de datos use nombres diferentes (por ejemplo, snake_case):

[Cacheable]
[Table("sample_entities")]
public class SampleEntity
{
    [Key]
    [AutoGenerated]
    public int Id { get; set; }

    [Column("creation_date")]
    public DateTime CreationDate { get; set; }

    [RangeFilter(LinkedRangeProperty = "CreationDateEnd")]
    public DateTime CreatedAt { get; set; }

    [NotMapped]
    public DateTime CreationDateEnd { get; set; }

    [Filterable]
    public string Name { get; set; }

    public int Age { get; set; }

    public bool Active { get; set; }

    [RelatedEntity(Cardinality = RelationCardinality.OneToMany,
                   ForeignKeyAttribute = "ParentId")]
    public IList<ChildEntity> Childs { get; set; }
}

Operaciones CRUD

repo.Add(new SampleEntity { Name = "Renato Rocha" });

repo.AddRange(list);

var filter = new SampleEntity { DocNumber = 12345 };
var entityToUpdate = await repo.Get(filter);
entityToUpdate.Age = 40;
int affected = await repo.Update(entityToUpdate, filter);

int removed = await repo.Remove(filter);

Query (consulta por filtro tipado)

var all = await repo.Query(new SampleEntity()); // listar todos

var filter = new SampleEntity { Name = "Roberto", Email = "gmail.com" };
var results = await repo.Query(filter, filterConjunction: true);

Use el parámetro filterConjunction para definir el comportamiento de los atributos en la consulta: conjunción lógica Y cuando está activada, disyunción lógica O cuando está desactivada. Con conjunción el criterio aplicado es de igualdad, de lo contrario de similitud.

Search (búsqueda tokenizada en propiedades [Filterable])

var results = await repo.Search("Paulo");

var page = await repo.Search("Paulo", page: 1, pageSize: 20);

Paginación

var result = await repo.Query(filter, page: 1, pageSize: 10);

var result = repo.QuerySync(filter, page: 1, pageSize: 10);

PaginatedResult<T> expone Items, TotalCount, Page, PageSize y PageCount.

Constructor fluido de consultas

var result = await repo.Query(filter)
    .OrderBy("Name")
    .Paginate(1, 10);

var result = await repo.Query(filter)
    .GroupBy("Category")
    .OrderByDescending("Price");

var list = repo.QuerySync(filter)
    .OrderBy("Name")
    .ToList();

El orden de los métodos no importa; el builder recopila los atributos de ordenación y agrupación y ejecuta la consulta en el orden correcto del SQL.

GroupBy con agregaciones

var aggregates = new Dictionary<string, DataAggregationType>
{
    { "Price", DataAggregationType.Sum },
    { "Price", DataAggregationType.Average }
};
var result = await repo.Query(filter).GroupBy(["Category"], aggregates);

El builder genera el SELECT ... GROUP BY ... automáticamente con las funciones de agregación solicitadas (SUM, COUNT, MIN, MAX, AVG).

Entidades de hecho DW/ETL

Para trabajos de Data Warehouse y ETL, [RelationalColumn] genera JOINs automáticos (INNER/LEFT) con tablas dimensionales y [DataAggregationColumn] genera columnas calculadas de agregación:

[Table("fact_sales")]
public class FactSalesEntity
{
    [Key] [AutoGenerated]
    public int Id { get; set; }

    [Column("product_id")]
    public int ProductId { get; set; }

    [Column("total_amount")]
    public decimal TotalAmount { get; set; }

    // JOIN automático: dim_product.product_name AS ProductName
    [RelationalColumn(
        TableName = "dim_product",
        ColumnName = "product_name",
        ColumnAlias = "ProductName",
        KeyColumn = "product_id",
        ForeignKeyColumn = "id",
        JunctionType = RelationalJunctionType.Mandatory)]
    public string ProductName { get; set; }

    // Agregación: SUM(fact_sales.total_amount) AS SumTotalAmount
    [DataAggregationColumn(ColumnName = "total_amount", AggregationType = DataAggregationType.Sum)]
    public decimal SumTotalAmount { get; set; }
}

var result = await repo.Query(new FactSalesEntity());

SQL generado:

SELECT dim_product.product_name AS ProductName,
       SUM(fact_sales.total_amount) AS SumTotalAmount,
       fact_sales.id, fact_sales.product_id, fact_sales.total_amount
FROM fact_sales
INNER JOIN dim_product ON fact_sales.product_id = dim_product.id
WHERE 1 = 1

RelationalJunctionType.Mandatory genera INNER JOIN; Optional genera LEFT JOIN. Las propiedades marcadas con [RelationalColumn] o [DataAggregationColumn] son de solo lectura y se ignoran en las operaciones INSERT/UPDATE.

Caché conectable

Marque la entidad como [Cacheable] para habilitar el caché. El proveedor predeterminado es en memoria, pero se pueden inyectar proveedores Redis, Garnet o compuestos sin cambiar el código del repositorio:

DataCache.Initialize(memorySizeLimit: 100); // en memoria, límite de 100 MB
DataCache.Initialize();                     // en memoria, sin límite

DataCache.Initialize(new DistributedCacheProvider("localhost:6379")); // Redis / Garnet

DataCache.Initialize(new CompositeCacheProvider( // L1 en memoria + L2 distribuido
    new InMemoryCacheProvider(),
    new DistributedCacheProvider("redis:6379")));

Benchmark — DapperRepository vs EF Core

Windows 11, Intel i5-7500T, .NET 9.0, SQLite, 5.000 filas

Escenario EF Core DapperRepository Ganador
InsertIndividual 3.2 ms 3.1 ms ORM 1.1x
BulkInsert_100 32.8 ms 33.5 ms Empate
GetById 526 μs 409 μs ORM 1.3x
Update_Individual 2.9 ms 2.7 ms ORM 1.1x
Delete_Individual 3.2 ms 3.5 ms Empate
CountSync 508 μs 536 μs Empate
Search_Filterable 1.0 ms 1.2 ms Empate
Sort_5000_rows_ORDER_BY 61.6 ms 38.6 ms ORM 1.6x
Sort_MultiColumn 61 ms 41 ms ORM 1.5x
GroupBy_Simple 7.3 ms 1.0 ms ORM 7.3x
GroupBy_AggAll 3.1 ms 1.0 ms ORM 3.1x
GroupBy_Having 2.5 ms 1.1 ms ORM 2.3x
QueryRaw_Select 3.5 ms 3.2 ms ORM 1.1x
SearchPaginated 2.5 ms 3.1 ms Empate*

Benchmark — Channel vs Replica (Replicación en Cluster)

PersistenceChannel (CacheIndexer) vs DapperRepository nativo replicaConnStrings, cluster SQLite de 3 nodos

Escenario Channel Replica Aceleración
Factura Individual 115 ms (8,7 inv/s) 19 ms (52,6 inv/s) Replica 6x más rápido*
Bulk 100 585 ms (171 inv/s) 9.479 ms (10,5 inv/s) Channel 16,2x
Bulk 1.000 6.679 ms (150 inv/s) 70.835 ms (14,1 inv/s) Channel 10,6x

*Factura Individual: Replica gana porque no hay contención; la ruta AddSync accede a un DB directamente sin overhead de lock. Bulk: Channel gana porque el PersistenceChannel desacopla serialización de persistencia vía fan-out, mientras que el Parallel.ForEach + Mutex de Replica crea contención severa bajo carga (P99 638 ms, Max 2.822 ms).

Pruebas y cobertura

El proyecto Rochas.DapperRepository.Test (xUnit, net9.0) contiene 227 pruebas que cubren CRUD asíncrono, composición, builders, GROUP BY/agregaciones, caché conectable, parsers SQL, type handlers y detección de engine, con 80% de cobertura de línea en el ensamblado principal y 100% en Rochas.Data.Specification.


Français

Qu'est-ce que c'est ?

Rochas.DapperRepository est un dépôt générique léger pour l'accès aux données, construit sur Dapper. Il suit les modèles Repository et Unit of Work décrits par Martin Fowler, sans nouveau langage de requête, sans suivi d'état et sans proxys dynamiques. Il est conçu pour fonctionner directement avec des POCO décorés d'attributs de métadonnées qui pilotent les requêtes, la persistance et les relations entre entités.

Il prend en charge SQL Server, MySQL, PostgreSQL et SQLite via l'enum DatabaseEngine.

Le paquet cible .NET Standard 2.1 et est publié sur NuGet sous le nom Rochas.DapperRepository (version actuelle 1.9.5).

Fonctionnalités

  • CRUD complet (Add, AddRange, Update, Remove, Get) avec surcharges synchrones et asynchrones
  • Requêtes par filtre typé (Query), avec conjonction ET/OU via filterConjunction
  • Recherche tokenisée sur les propriétés [Filterable] (Search)
  • Pagination renvoyant PaginatedResult<T>
  • Constructeur de requêtes fluide avec OrderBy, OrderByDescending, GroupBy (avec agrégats) et Paginate — le builder est awaitable et génère le SQL dans le bon ordre (SELECT → FROM → WHERE → GROUP BY → ORDER BY)
  • Filtres de plage avec [RangeFilter]
  • Chargement automatique de la composition avec [RelatedEntity] (1-1, 1-N, N-1, N-N)
  • Cache enfichable via ICacheProvider (en mémoire, Redis/Garnet, composite L1/L2, canal de persistance maître→esclave)
  • Support DW/ETL avec [RelationalColumn] (génération automatique de JOIN) et [DataAggregationColumn] (SUM, COUNT, MIN, MAX, AVG)
  • Requêtes SQL directes (QueryRaw)

Installation

dotnet add package Rochas.DapperRepository

Démarrage rapide

using Rochas.DapperRepository;
using Rochas.Data.Specification.Annotations;
using Rochas.Data.Specification.Enums;

var connString = "Data Source=sample.db;Cache=Shared";

using var repo = new GenericRepository<SampleEntity>(DatabaseEngine.SQLite, connString);

var result = await repo.Search("Celulares");

Enregistrement dans le DI

services.AddScoped<IGenericRepository<SampleEntity>>(provider =>
    new GenericRepository<SampleEntity>(
        DatabaseEngine.SQLite,
        configuration.GetConnectionString("Default")));

Injectez ensuite IGenericRepository<SampleEntity> dans vos services.

Exemple d'entité

[Table] et [Column] sont facultatifs. Lorsqu'ils sont omis, le nom de la classe est utilisé pour la table et le nom de la propriété pour la colonne. Utilisez-les lorsque votre base de données utilise des noms différents (par exemple, snake_case) :

[Cacheable]
[Table("sample_entities")]
public class SampleEntity
{
    [Key]
    [AutoGenerated]
    public int Id { get; set; }

    [Column("creation_date")]
    public DateTime CreationDate { get; set; }

    [RangeFilter(LinkedRangeProperty = "CreationDateEnd")]
    public DateTime CreatedAt { get; set; }

    [NotMapped]
    public DateTime CreationDateEnd { get; set; }

    [Filterable]
    public string Name { get; set; }

    public int Age { get; set; }

    public bool Active { get; set; }

    [RelatedEntity(Cardinality = RelationCardinality.OneToMany,
                   ForeignKeyAttribute = "ParentId")]
    public IList<ChildEntity> Childs { get; set; }
}

Opérations CRUD

repo.Add(new SampleEntity { Name = "Renato Rocha" });

repo.AddRange(list);

var filter = new SampleEntity { DocNumber = 12345 };
var entityToUpdate = await repo.Get(filter);
entityToUpdate.Age = 40;
int affected = await repo.Update(entityToUpdate, filter);

int removed = await repo.Remove(filter);

Query (requête par filtre typé)

var all = await repo.Query(new SampleEntity()); // tous les enregistrements

var filter = new SampleEntity { Name = "Roberto", Email = "gmail.com" };
var results = await repo.Query(filter, filterConjunction: true);

Utilisez le paramètre filterConjunction pour définir le comportement des attributs dans la requête : conjonction logique ET lorsqu'il est activé, disjonction logique OU lorsqu'il est désactivé. Avec la conjonction, le critère appliqué est l'égalité, sinon la similarité.

Search (recherche tokenisée sur les propriétés [Filterable])

var results = await repo.Search("Paulo");

var page = await repo.Search("Paulo", page: 1, pageSize: 20);

Pagination

var result = await repo.Query(filter, page: 1, pageSize: 10);

var result = repo.QuerySync(filter, page: 1, pageSize: 10);

PaginatedResult<T> expose Items, TotalCount, Page, PageSize et PageCount.

Constructeur de requêtes fluide

var result = await repo.Query(filter)
    .OrderBy("Name")
    .Paginate(1, 10);

var result = await repo.Query(filter)
    .GroupBy("Category")
    .OrderByDescending("Price");

var list = repo.QuerySync(filter)
    .OrderBy("Name")
    .ToList();

L'ordre des méthodes n'a pas d'importance ; le builder collecte les attributs de tri et de regroupement et exécute la requête dans le bon ordre SQL.

GroupBy avec agrégats

var aggregates = new Dictionary<string, DataAggregationType>
{
    { "Price", DataAggregationType.Sum },
    { "Price", DataAggregationType.Average }
};
var result = await repo.Query(filter).GroupBy(["Category"], aggregates);

Le builder génère automatiquement le SELECT ... GROUP BY ... avec les fonctions d'agrégation demandées (SUM, COUNT, MIN, MAX, AVG).

Entités de fait DW/ETL

Pour les travaux de Data Warehouse et d'ETL, [RelationalColumn] génère des JOIN automatiques (INNER/LEFT) avec les tables de dimensions et [DataAggregationColumn] génère des colonnes d'agrégation calculées :

[Table("fact_sales")]
public class FactSalesEntity
{
    [Key] [AutoGenerated]
    public int Id { get; set; }

    [Column("product_id")]
    public int ProductId { get; set; }

    [Column("total_amount")]
    public decimal TotalAmount { get; set; }

    // JOIN automatique : dim_product.product_name AS ProductName
    [RelationalColumn(
        TableName = "dim_product",
        ColumnName = "product_name",
        ColumnAlias = "ProductName",
        KeyColumn = "product_id",
        ForeignKeyColumn = "id",
        JunctionType = RelationalJunctionType.Mandatory)]
    public string ProductName { get; set; }

    // Agrégation : SUM(fact_sales.total_amount) AS SumTotalAmount
    [DataAggregationColumn(ColumnName = "total_amount", AggregationType = DataAggregationType.Sum)]
    public decimal SumTotalAmount { get; set; }
}

var result = await repo.Query(new FactSalesEntity());

SQL généré :

SELECT dim_product.product_name AS ProductName,
       SUM(fact_sales.total_amount) AS SumTotalAmount,
       fact_sales.id, fact_sales.product_id, fact_sales.total_amount
FROM fact_sales
INNER JOIN dim_product ON fact_sales.product_id = dim_product.id
WHERE 1 = 1

RelationalJunctionType.Mandatory génère INNER JOIN ; Optional génère LEFT JOIN. Les propriétés marquées avec [RelationalColumn] ou [DataAggregationColumn] sont en lecture seule et sont ignorées dans les opérations INSERT/UPDATE.

Cache enfichable

Marquez l'entité avec [Cacheable] pour activer le cache. Le fournisseur par défaut est en mémoire, mais des fournisseurs Redis, Garnet ou composites peuvent être injectés sans modifier le code du dépôt :

DataCache.Initialize(memorySizeLimit: 100); // en mémoire, limite de 100 Mo
DataCache.Initialize();                     // en mémoire, sans limite

DataCache.Initialize(new DistributedCacheProvider("localhost:6379")); // Redis / Garnet

DataCache.Initialize(new CompositeCacheProvider( // L1 en mémoire + L2 distribué
    new InMemoryCacheProvider(),
    new DistributedCacheProvider("redis:6379")));

Benchmark — DapperRepository vs EF Core

Windows 11, Intel i5-7500T, .NET 9.0, SQLite, 5 000 lignes

Scénario EF Core DapperRepository Gagnant
InsertIndividual 3.2 ms 3.1 ms ORM 1.1x
BulkInsert_100 32.8 ms 33.5 ms Égalité
GetById 526 μs 409 μs ORM 1.3x
Update_Individual 2.9 ms 2.7 ms ORM 1.1x
Delete_Individual 3.2 ms 3.5 ms Égalité
CountSync 508 μs 536 μs Égalité
Search_Filterable 1.0 ms 1.2 ms Égalité
Sort_5000_rows_ORDER_BY 61.6 ms 38.6 ms ORM 1.6x
Sort_MultiColumn 61 ms 41 ms ORM 1.5x
GroupBy_Simple 7.3 ms 1.0 ms ORM 7.3x
GroupBy_AggAll 3.1 ms 1.0 ms ORM 3.1x
GroupBy_Having 2.5 ms 1.1 ms ORM 2.3x
QueryRaw_Select 3.5 ms 3.2 ms ORM 1.1x
SearchPaginated 2.5 ms 3.1 ms Égalité*

Benchmark — Channel vs Replica (Réplication en Cluster)

PersistenceChannel (CacheIndexer) vs DapperRepository natif replicaConnStrings, cluster SQLite à 3 nœuds

Scénario Channel Replica Accélération
Facture Individuelle 115 ms (8,7 inv/s) 19 ms (52,6 inv/s) Replica 6x plus rapide*
Bulk 100 585 ms (171 inv/s) 9 479 ms (10,5 inv/s) Channel 16,2x
Bulk 1 000 6 679 ms (150 inv/s) 70 835 ms (14,1 inv/s) Channel 10,6x

*Facture Individuelle: Replica gagne car il n'y a pas de contention ; le chemin AddSync accède directement à une DB sans surcharge de lock. Bulk: Channel gagne car le PersistenceChannel découple sérialisation et persistance via fan-out, tandis que le Parallel.ForEach + Mutex de Replica crée une contention sévère sous charge (P99 638 ms, Max 2 822 ms).

Tests et couverture

Le projet Rochas.DapperRepository.Test (xUnit, net9.0) contient 227 tests couvrant le CRUD asynchrone, la composition, les builders, le GROUP BY/les agrégats, le cache enfichable, les analyseurs SQL, les type handlers et la détection de moteur, avec 80 % de couverture de lignes sur l'assemblage principal et 100 % dans Rochas.Data.Specification.


Deutsch

Was ist das?

Rochas.DapperRepository ist ein leichtgewichtiges generisches Repository für den Datenzugriff, aufgebaut auf Dapper. Es folgt den von Martin Fowler beschriebenen Repository- und Unit-of-Work-Mustern, ohne neue Abfragesprache, ohne Zustandsverfolgung und ohne dynamische Proxies. Es wurde für die direkte Verwendung mit POCOs entworfen, die mit Metadaten-Attributen dekoriert sind, die Abfragen, Persistenz und Entitätsbeziehungen steuern.

Unterstützt werden SQL Server, MySQL, PostgreSQL und SQLite über die Enum DatabaseEngine.

Das Paket zielt auf .NET Standard 2.1 und wird auf NuGet als Rochas.DapperRepository veröffentlicht (aktuelle Version 1.9.5).

Funktionen

  • Vollständiges CRUD (Add, AddRange, Update, Remove, Get) mit synchronen und asynchronen Überladungen
  • Typisierte Filterabfragen (Query) mit UND/ODER-Verknüpfung über filterConjunction
  • Tokenisierte Suche über [Filterable]-Eigenschaften (Search)
  • Seitierung mit Rückgabe von PaginatedResult<T>
  • Fließender Query-Builder mit OrderBy, OrderByDescending, GroupBy (mit Aggregaten) und Paginate — der Builder ist awaitable und generiert SQL in der richtigen Reihenfolge (SELECT → FROM → WHERE → GROUP BY → ORDER BY)
  • Bereichsfilter mit [RangeFilter]
  • Automatisches Laden von Kompositionen mit [RelatedEntity] (1-1, 1-N, N-1, N-N)
  • Austauschbares Caching über ICacheProvider (in-memory, Redis/Garnet, Composite L1/L2, Master-to-Slave-Persistenzkanal)
  • DW/ETL-Unterstützung mit [RelationalColumn] (automatische JOIN-Erzeugung) und [DataAggregationColumn] (SUM, COUNT, MIN, MAX, AVG)
  • Direkte SQL-Abfragen (QueryRaw)

Installation

dotnet add package Rochas.DapperRepository

Schnellstart

using Rochas.DapperRepository;
using Rochas.Data.Specification.Annotations;
using Rochas.Data.Specification.Enums;

var connString = "Data Source=sample.db;Cache=Shared";

using var repo = new GenericRepository<SampleEntity>(DatabaseEngine.SQLite, connString);

var result = await repo.Search("Celulares");

Registrierung in DI

services.AddScoped<IGenericRepository<SampleEntity>>(provider =>
    new GenericRepository<SampleEntity>(
        DatabaseEngine.SQLite,
        configuration.GetConnectionString("Default")));

Anschließend injizieren Sie IGenericRepository<SampleEntity> in Ihre Dienste.

Entitätsbeispiel

[Table] und [Column] sind optional. Wenn sie weggelassen werden, wird der Klassenname für die Tabelle und der Eigenschaftsname für die Spalte verwendet. Verwenden Sie sie, wenn Ihre Datenbank andere Namen verwendet (z. B. snake_case):

[Cacheable]
[Table("sample_entities")]
public class SampleEntity
{
    [Key]
    [AutoGenerated]
    public int Id { get; set; }

    [Column("creation_date")]
    public DateTime CreationDate { get; set; }

    [RangeFilter(LinkedRangeProperty = "CreationDateEnd")]
    public DateTime CreatedAt { get; set; }

    [NotMapped]
    public DateTime CreationDateEnd { get; set; }

    [Filterable]
    public string Name { get; set; }

    public int Age { get; set; }

    public bool Active { get; set; }

    [RelatedEntity(Cardinality = RelationCardinality.OneToMany,
                   ForeignKeyAttribute = "ParentId")]
    public IList<ChildEntity> Childs { get; set; }
}

CRUD-Operationen

repo.Add(new SampleEntity { Name = "Renato Rocha" });

repo.AddRange(list);

var filter = new SampleEntity { DocNumber = 12345 };
var entityToUpdate = await repo.Get(filter);
entityToUpdate.Age = 40;
int affected = await repo.Update(entityToUpdate, filter);

int removed = await repo.Remove(filter);

Query (Abfrage per typisiertem Filter)

var all = await repo.Query(new SampleEntity()); // alle Datensätze

var filter = new SampleEntity { Name = "Roberto", Email = "gmail.com" };
var results = await repo.Query(filter, filterConjunction: true);

Verwenden Sie den Parameter filterConjunction, um das Verhalten der Attribute in der Abfrage festzulegen: logische UND-Verknüpfung, wenn aktiviert, logische ODER-Verknüpfung, wenn deaktiviert. Mit Verknüpfung wird das Kriterium Gleichheit angewendet, andernfalls Ähnlichkeit.

Search (tokenisierte Suche über [Filterable]-Eigenschaften)

var results = await repo.Search("Paulo");

var page = await repo.Search("Paulo", page: 1, pageSize: 20);

Seitierung

var result = await repo.Query(filter, page: 1, pageSize: 10);

var result = repo.QuerySync(filter, page: 1, pageSize: 10);

PaginatedResult<T> stellt Items, TotalCount, Page, PageSize und PageCount bereit.

Fließender Query-Builder

var result = await repo.Query(filter)
    .OrderBy("Name")
    .Paginate(1, 10);

var result = await repo.Query(filter)
    .GroupBy("Category")
    .OrderByDescending("Price");

var list = repo.QuerySync(filter)
    .OrderBy("Name")
    .ToList();

Die Reihenfolge der Methoden spielt keine Rolle; der Builder sammelt die Sortier- und Gruppierungsattribute und führt die Abfrage in der korrekten SQL-Reihenfolge aus.

GroupBy mit Aggregaten

var aggregates = new Dictionary<string, DataAggregationType>
{
    { "Price", DataAggregationType.Sum },
    { "Price", DataAggregationType.Average }
};
var result = await repo.Query(filter).GroupBy(["Category"], aggregates);

Der Builder generiert automatisch das SELECT ... GROUP BY ... mit den angeforderten Aggregatfunktionen (SUM, COUNT, MIN, MAX, AVG).

DW/ETL-Faktenentitäten

Für Data-Warehouse- und ETL-Arbeiten erzeugt [RelationalColumn] automatische JOINs (INNER/LEFT) mit Dimensionstabellen und [DataAggregationColumn] erzeugt berechnete Aggregatspalten:

[Table("fact_sales")]
public class FactSalesEntity
{
    [Key] [AutoGenerated]
    public int Id { get; set; }

    [Column("product_id")]
    public int ProductId { get; set; }

    [Column("total_amount")]
    public decimal TotalAmount { get; set; }

    // Automatischer JOIN: dim_product.product_name AS ProductName
    [RelationalColumn(
        TableName = "dim_product",
        ColumnName = "product_name",
        ColumnAlias = "ProductName",
        KeyColumn = "product_id",
        ForeignKeyColumn = "id",
        JunctionType = RelationalJunctionType.Mandatory)]
    public string ProductName { get; set; }

    // Aggregat: SUM(fact_sales.total_amount) AS SumTotalAmount
    [DataAggregationColumn(ColumnName = "total_amount", AggregationType = DataAggregationType.Sum)]
    public decimal SumTotalAmount { get; set; }
}

var result = await repo.Query(new FactSalesEntity());

Generiertes SQL:

SELECT dim_product.product_name AS ProductName,
       SUM(fact_sales.total_amount) AS SumTotalAmount,
       fact_sales.id, fact_sales.product_id, fact_sales.total_amount
FROM fact_sales
INNER JOIN dim_product ON fact_sales.product_id = dim_product.id
WHERE 1 = 1

RelationalJunctionType.Mandatory erzeugt INNER JOIN; Optional erzeugt LEFT JOIN. Eigenschaften, die mit [RelationalColumn] oder [DataAggregationColumn] markiert sind, sind schreibgeschützt und werden bei INSERT/UPDATE-Operationen ignoriert.

Austauschbares Caching

Markieren Sie die Entität mit [Cacheable], um das Caching zu aktivieren. Der Standardanbieter ist in-memory, aber Redis-, Garnet- oder Composite-Anbieter können injiziert werden, ohne den Repository-Code zu ändern:

DataCache.Initialize(memorySizeLimit: 100); // in-memory, Limit 100 MB
DataCache.Initialize();                     // in-memory, ohne Limit

DataCache.Initialize(new DistributedCacheProvider("localhost:6379")); // Redis / Garnet

DataCache.Initialize(new CompositeCacheProvider( // L1 in-memory + L2 verteilt
    new InMemoryCacheProvider(),
    new DistributedCacheProvider("redis:6379")));

Benchmark — DapperRepository vs EF Core

Windows 11, Intel i5-7500T, .NET 9.0, SQLite, 5.000 Zeilen

Szenario EF Core DapperRepository Sieger
InsertIndividual 3.2 ms 3.1 ms ORM 1.1x
BulkInsert_100 32.8 ms 33.5 ms Unentschieden
GetById 526 μs 409 μs ORM 1.3x
Update_Individual 2.9 ms 2.7 ms ORM 1.1x
Delete_Individual 3.2 ms 3.5 ms Unentschieden
CountSync 508 μs 536 μs Unentschieden
Search_Filterable 1.0 ms 1.2 ms Unentschieden
Sort_5000_rows_ORDER_BY 61.6 ms 38.6 ms ORM 1.6x
Sort_MultiColumn 61 ms 41 ms ORM 1.5x
GroupBy_Simple 7.3 ms 1.0 ms ORM 7.3x
GroupBy_AggAll 3.1 ms 1.0 ms ORM 3.1x
GroupBy_Having 2.5 ms 1.1 ms ORM 2.3x
QueryRaw_Select 3.5 ms 3.2 ms ORM 1.1x
SearchPaginated 2.5 ms 3.1 ms Unentschieden*

Benchmark — Channel vs Replica (Cluster-Replikation)

PersistenceChannel (CacheIndexer) vs DapperRepository nativ replicaConnStrings, 3-Knoten-SQLite-Cluster

Szenario Channel Replica Beschleunigung
Einzelrechnung 115 ms (8,7 Inv/s) 19 ms (52,6 Inv/s) Replica 6x schneller*
Bulk 100 585 ms (171 Inv/s) 9.479 ms (10,5 Inv/s) Channel 16,2x
Bulk 1.000 6.679 ms (150 Inv/s) 70.835 ms (14,1 Inv/s) Channel 10,6x

*Einzelrechnung: Replica gewinnt, da es keine Kontention gibt; der AddSync-Pfad erreicht eine DB direkt ohne Lock-Overhead. Bulk: Channel gewinnt, da PersistenceChannel Serialisierung und Persistenz über Fan-Out entkoppelt, während Parallel.ForEach + Mutex von Replica schwere Kontention unter Last erzeugt (P99 638 ms, Max 2.822 ms).

Tests und Abdeckung

Das Projekt Rochas.DapperRepository.Test (xUnit, net9.0) enthält 227 Tests, die asynchrones CRUD, Komposition, Builder, GROUP BY/Aggregate, austauschbares Caching, SQL-Parser, Type-Handler und Engine-Erkennung abdecken, mit 80 % Zeilenabdeckung der Hauptassembly und 100 % in Rochas.Data.Specification.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  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.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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
1.9.8 91 8/16/2026
1.9.3 113 8/11/2026
1.7.5 89 8/10/2026
1.6.8 80 8/9/2026
1.6.3 99 8/9/2026
1.6.2 88 8/9/2026
1.6.0 101 8/8/2026
1.4.0 91 8/7/2026
1.3.0 442 11/30/2025
1.2.8 418 6/1/2025
1.2.7 563 3/13/2025
Loading failed

v1.9.8 - Contracts moved to Rochas.Data.Specification (DataSpecification), replacing the internal Rochas.DapperRepository.Specification project. v1.9.5 - [NEW] GroupBy(groupAttributes, aggregates) now applies dictionary aggregates (SUM/COUNT/AVG/MIN/MAX) to grouped queries; aggregates were previously ignored by the query builder.