SqlComplexOperations 2.1.0

dotnet add package SqlComplexOperations --version 2.1.0
                    
NuGet\Install-Package SqlComplexOperations -Version 2.1.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="SqlComplexOperations" Version="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="SqlComplexOperations" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="SqlComplexOperations" />
                    
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 SqlComplexOperations --version 2.1.0
                    
#r "nuget: SqlComplexOperations, 2.1.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.
#:package SqlComplexOperations@2.1.0
                    
#: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=SqlComplexOperations&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=SqlComplexOperations&version=2.1.0
                    
Install as a Cake Tool

SqlComplexOperations

Link da LIB

Objetivo

Quando precisamos processar uma quantidade significativa de dados em uma aplicação, por vezes executar de forma unitária cada registro pode não ser a melhor saída. <br><br> O projeto foi criado com o objetivo de contornar este problema, possibilitando uma forma de processamento mais eficaz utilizando recursos do SQL e .NET.

Estrutura do projeto

  • Component: Possui a implementação da biblioteca em si.

  • Poc: Possui projetos utilizando a implementação principal para testes e validações.

  • Bench: Possui o projeto que realiza benchmarks utilizando a abordagem proposta e implementações tradicionais.

Bancos de dados suportados

** (PostgreSQL é suportado com algumas limitações).

Sumario

Guia de uso

Com a biblioteca referenciada em seu projeto, realize a injeção da mesma utilizando o método ConfigureSqlComplexOperations para SqlServer:

builder.Services.ConfigureSqlComplexOperations();

Ou ConfigurePostgreSqlComplexOperations para Postgres:

builder.Services.ConfigurePostgreSqlComplexOperations();

Exemplo de uso comando MERGE (update | insert | delete)

Configurando o builder para executarmos a instrução no banco de dados. Exemplo abaixo utilizando uma classe de exemplo "Fornecedor":

var builder = await _mergeBuilder.Create<Fornecedor>()
    .SetDataSource(dataSource)
    .SetMergeColumns(x => x.Documento)
    .SetUpdatedColumns(x => x)
    .SetTransaction(transaction.GetDbTransaction())
    .UseSnakeCaseNamingConvention()
    .WithCondition(ConditionTypes.NOT_EQUAL, ConditionOperator.OR, x => new { x.Cep, x.Nome })
    .SetIgnoreOnIsertOperation(x => x.Id)
    .UseEnumStatusConfiguration(x => x.Status)
    .Execute();

Após configurar todos os parâmetros desejados, basta invocar o método Execute.

Parâmetros

Pare configurar o builder, os possuímos os parâmetros:

  • Create [obrigatório]:

    • Cria o builder com a entidade de banco e o nomme da tabela que pode ser passado como string ou em caso de não passado como parametro, usa o nome da classe.

      .Create<Fornecedor>("fornecedores")
      
  • SetDataSource [obrigatório]:

    • Lista de objetos que deseja adicionar, mesclar, atualizar ou deletar, no banco de dados. Deve possuir a mesma estrutura da tabela do banco.

      .SetDataSource(dataSource)
      
  • SetMergeColumns [obrigatório]:

    • Lista de campos que deseja utilizar para comparar se o mesmo existe ou não na tabela de destino.

    • Caso as colunas comparadas existam, o comando entenderá uma atualização. Caso contrário, um novo registro será inserido.

    • Uma ou mais colunas podem ser configuradas, desde que elas existam na entidade configurada.

      .SetMergeColumns(x => x.Documento)
      
  • SetUpdatedColumns [obrigatório]:

    • Colunas que serão afetadas no caso de uma atualização na tabela de destino.

    • Podemos configurar para executar em todas:

      .SetUpdatedColumns(x => x)
      
    • Podemos também configurar as colunas individualmente, sendo uma ou várias:

      .SetUpdatedColumns(x => new { x.Cep, x.Nome } )
      
  • SetTransaction [obrigatório]:

    • Precisamos disponibilizar uma transação para a biblioteca realizar o comando no banco de dados.

    • Ao realizar a operação, você precisa realizar o commit no banco de dados para efetivar as alterações.

      .SetTransaction(transaction.GetDbTransaction())  
      
  • UseDatabaseSchema [opcional]:

    • Caso a sua tabela não esteja no schema default do SQL você poderá informar o schema usando o UseDatabaseSchema

      .UseDatabaseSchema("dif_schema")
      
  • DeleteWhenDataIsNotInDataSource [opcional]:

    • Caso o dado esteja na sua tabela mas não esteja no data source, você pode deleta-lo. (Combinado com UseStatusConfiguration, a deleção pode ser logica, caso não seja usado o UseStatusConfiguration a deleção é real).

      .UseDatabaseSchema("dif_schema")
      

      Limitação PostgreSQL: Para posgtreSQL esse comando delete será rodado apos o merge pois o mesmo não suporta o DELETE dentro do comando merge.

  • WithCondition [opcional]:

    • Podemos configurar condições para o comando só realizar as alterações em registros existentes e que realmente sofreram alterações.

    • Antes de realizar o update, o comando checa se os campos que você deseja comparar estão diferentes entre o data source e a tabela de destino, evitando processamento desnecessário no banco de dados.

    • As condições devem ser configuradas citando um tipo de operação - quanto utilizada mais de uma coluna - (AND ou OR) e o tipo de comparação (EQUAL ou NOT_EQUAL).

    • Exemplo utilizando uma comparação NOT_EQUAL em duas colunas com instrução OU:

         .WithCondition(ConditionTypes.NOT_EQUAL, ConditionOperator.OR, x => new { x.Cep, x.Nome })
      
    • Exemplo utilizando uma comparação EQUAL em uma coluna:

         .WithCondition(ConditionTypes.EQUALS, x => x.Nome)
      
  • SetIgnoreOnIsertOperation [opcional]:

    • Podemos configurar colunas que são ignoradas ao realizar a instrução de inserção, como por exemplo campos auto identity ou colunas que você simplesmente não queira utilizar na inserção do registro.

      .SetIgnoreOnIsertOperation(x => x.Id)
      
  • UseEnumAsString [opcional]:

    • Em caso de querer usar enumeradores como string na base.

      .UseEnumAsString()
      
  • UseStatusConfiguration [opcional]:

    • Utilize uma coluna de status em sua tabela de destino para receber a informação se, após a execução do comando merge, o registro foi alterado, inserido ou simplesmente não foi afetado (pode ser usado também para deleções lógicas).

    • O status poode ser salvo como string ou como inteiro no seu banco de dados (em casos salvos como inteiro o valor terá os valores do enum BulkMergeStatus).

      .UseStatusConfiguration(false, x => x.Status) //Para salvar como inteiro.
      .UseStatusConfiguration(true, x => x.Status) //Para salvar como string.
      
    • Enum BulkMergeStatus

      PROCESSED = 0,
      UPDATED = 1,
      INSERTED = 2,
      PROCESSED_ERROR = 3,
      DELETED = 4
      
  • UsePropertyNameAttribute [opcional]:

    • Em casos de nomes de coluna no .NET diferentes do nome de banco, você pode usar essa configuração.

    • Uso na propriedade da entidade (.NET)

          [PropertyName("Dif_Name")]
          public string Nome { get; set; }
      
          .UsePropertyNameAttribute()
      
  • UseSnakeCaseNamingConvention [opcional]:

    • Caso utilize em seu banco de dados alguma convensão específica, a biblioteca da suporte a:
      • UseSnakeCaseNamingConvention

        .UseSnakeCaseNamingConvention()
        
  • SetResponseType [opcional]:

    • Você pode setar o tipo de resposta que você espera receber, possiveis respostas:

      • NONE (Sem nenhum retorno) - [default]

      • ROW_COUNT (Retorna o numero de linhas afetadas)

      • SIMPLE (Retorna quantos registros foram atualizados, quantos foram inseridos e quantos foram deletados (Alem do total de registros alterados))

      • COMPLETE (Retorna um de → para dos registros atualizados, o registro inserido (Caso exista) e o registro deletado (Caso exista))**

          .SetResponseType(ResponseType.SIMPLE)
        

      Limitação PostgreSQL: Para posgtreSQL o unico response type suportado é o NONE, isso porque o comando merge no postgreSQL não aceita tipos de resultado.

Exemplo de uso comando COPY (insert)

Configurando o builder para executarmos a instrução de inserção no banco de dados. Exemplo abaixo utilizando uma classe de exemplo "Fornecedor":

var builder = _bulkInsertBuilder.Create<Fornecedor>()
    .SetDataSource(dataSource)
    .SetTransaction(_dbTransaction)
    .Execute();

Após configurar todos os parâmetros desejados, basta invocar o método Execute.

Parâmetros

Pare configurar o builder, os possuímos os parâmetros:

  • Create [obrigatório]:

    • Cria o builder com a entidade de banco e o nomme da tabela que pode ser passado como string ou em caso de não passado como parametro, usa o nome da classe.

      .Create<Fornecedor>("fornecedores")
      
  • SetDataSource [obrigatório]:

    • Lista de objetos que deseja adicionar no banco de dados. Deve possuir a mesma estrutura da tabela do banco.

      .SetDataSource(dataSource)
      
  • SetTransaction [obrigatório]:

    • Precisamos disponibilizar uma transação para a biblioteca realizar o comando no banco de dados.

    • Ao realizar a operação, você precisa realizar o commit no banco de dados para efetivar as alterações.

      .SetTransaction(transaction.GetDbTransaction())  
      
  • UseDatabaseSchema [opcional]:

    • Caso a sua tabela não esteja no schema default do SQL você poderá informar o schema usando o UseDatabaseSchema

      .UseDatabaseSchema("dif_schema")
      
  • UsePropertyNameAttribute [opcional]:

    • Em casos de nomes de coluna no .NET diferentes do nome de banco, você pode usar essa configuração.

    • Uso na propriedade da entidade (.NET)

          [PropertyName("Dif_Name")]
          public string Nome { get; set; }
      
          .UsePropertyNameAttribute()
      
  • UseEnumAsString [opcional]:

    • Em caso de querer usar enumeradores como string na base.

      .UseEnumAsString()
      
  • UseSnakeCaseNamingConvention [opcional]:

    • Caso utilize em seu banco de dados alguma convensão específica, a biblioteca da suporte a:
      • UseSnakeCaseNamingConvention

        .UseSnakeCaseNamingConvention()
        

Benchmarks

Processamento de 1000 registros (realizando o update em 100% dos registros)

//|         Method |       Mean |      Error |     StdDev |     Median |
//|--------------- |-----------:|-----------:|-----------:|-----------:|
//| UpdateOneByOne | 9,374.0 ms | 4,796.4 ms | 1,245.6 ms | 8,974.7 ms |
//|         Upsert |   640.1 ms | 3,296.8 ms |   856.2 ms |   283.1 ms |

Processamento de 1000 registros (realizando o update em 40% dos registros, descartando 60% de dados inalterados)

//|          Method |       Mean |      Error |   StdDev |     Median |
//|---------------- |-----------:|-----------:|---------:|-----------:|
//| ExecuteOneByOne | 4,371.5 ms | 2,710.0 ms | 703.8 ms | 4,487.3 ms |
//|   ExecuteUpsert |   512.2 ms | 2,610.2 ms | 677.9 ms |   214.0 ms |

Processamento de 1000 registros (500 registros novos, 200 updates e 300 descartados)

//|          Method |        Mean |      Error |     StdDev |      Median |
//|---------------- |------------:|-----------:|-----------:|------------:|
//| ExecuteOneByOne | 11,393.5 ms | 6,724.8 ms | 4,448.0 ms | 11,230.3 ms |
//|   ExecuteUpsert |    498.7 ms |   931.6 ms |   616.2 ms |    299.2 ms |

Processamento de 1000 registros (500 registros novos, 200 updates e 300 descartados) (minimizando queries de busca no banco)

//|          Method |       Mean |      Error |   StdDev |     Median |
//|---------------- |-----------:|-----------:|---------:|-----------:|
//| ExecuteOneByOne | 3,492.1 ms | 3,190.9 ms | 828.7 ms | 3,690.2 ms |
//|   ExecuteUpsert |   492.3 ms | 2,404.4 ms | 624.4 ms |   216.1 ms |

Processamento de 1000 registros (500 registros novos, 200 updates e 300 descartados) (minimizando queries de busca no banco e insert unico no processamento sem bulk)

|          Method |       Mean |      Error |     StdDev |     Median |
|---------------- |-----------:|-----------:|-----------:|-----------:|
| ExecuteOneByOne | 4,793.3 ms | 2,808.9 ms | 1,857.9 ms | 4,215.0 ms |
|   ExecuteUpsert |   508.5 ms | 1,032.9 ms |   683.2 ms |   278.3 ms |    

Exemplo de testes de unidade usando a LIB

Utilizando MOQ

public class LibTestExampleMoq
{
    private readonly ApplicationContext _context;
    private readonly Mock<IMergeBuilder> _mergeBuilderMock;
    private readonly Mock<IUnitOfWork> _unitOfWorkMock;
    private readonly ClubeService _clubeService;

    public LibTestExampleMoq()
    {
        var contextOptions = new DbContextOptionsBuilder<ApplicationContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        _context = new ApplicationContext(contextOptions);

        _mergeBuilderMock = new Mock<IMergeBuilder>();
        _unitOfWorkMock = new Mock<IUnitOfWork>();

        var dbTransactionMock = new Mock<IDbTransaction>();
        _unitOfWorkMock.Setup(x => x.GetDbTransaction()).Returns(dbTransactionMock.Object);

        _clubeService = new(_context, _mergeBuilderMock.Object, _unitOfWorkMock.Object);
    }

    [Fact]
    public async void Upsert_WithMoq()
    {
        //ARRANGE
        var list = new List<ClubeDto> {
            new() { Nome = "São Paulo", Abreviacao = "SPFC", Apelido = "Tricolor" },
            new() { Nome = "Coritiba", Abreviacao = "CFC", Apelido = "Coxa" }
        };

        var entityList = new List<Clube> {
            new() { Nome = "Coritiba", Abreviacao = "CFC", Apelido = "Verdão" }
        };

        await _context.AddRangeAsync(entityList);
        await _context.SaveChangesAsync();

        var databaseServiceMock = new Mock<IDatabaseService>();
        var builderMock = new Mock<MergeBuilder<Clube>>(databaseServiceMock.Object, DatabaseType.MICROSOFT_SQL_SERVER) { CallBase = true };
        _mergeBuilderMock.Setup(x => x.Create<Clube>()).Returns(builderMock.Object);
        builderMock.Setup(x => x.Execute()).Returns(Task.FromResult(new OutputModel()));

        //ACTION
        await _clubeService.Upsert(list);

        //ASSERT
        _unitOfWorkMock.Verify(x => x.CommitTransaction(), Times.Once);
    }
}

Utilizando NSubstitute

public class LibTestExampleNSubstitute
{
    private readonly ApplicationContext _context;
    private readonly IMergeBuilder _mergeBuilderMock;
    private readonly IUnitOfWork _unitOfWorkMock;
    private readonly ClubeService _clubeService;

    public LibTestExampleNSubstitute()
    {
        var contextOptions = new DbContextOptionsBuilder<ApplicationContext>()
            .UseInMemoryDatabase(Guid.NewGuid().ToString())
            .Options;

        _context = new ApplicationContext(contextOptions);

        _mergeBuilderMock = Substitute.For<IMergeBuilder>();
        _unitOfWorkMock = Substitute.For<IUnitOfWork>();

        var dbTransactionMock = Substitute.For<IDbTransaction>();
        _unitOfWorkMock.GetDbTransaction().Returns(dbTransactionMock);

        _clubeService = new(_context, _mergeBuilderMock, _unitOfWorkMock);
    }

    [Fact]
    public async void Upsert_WithMoq()
    {
        //ARRANGE
        var list = new List<ClubeDto> {
            new() { Nome = "São Paulo", Abreviacao = "SPFC", Apelido = "Tricolor" },
            new() { Nome = "Coritiba", Abreviacao = "CFC", Apelido = "Coxa" }
        };

        var entityList = new List<Clube> {
            new() { Nome = "Coritiba", Abreviacao = "CFC", Apelido = "Verdão" }
        };

        await _context.AddRangeAsync(entityList);
        await _context.SaveChangesAsync();

        var databaseServiceMock = Substitute.For<IDatabaseService>();
        var builderMock = Substitute.ForPartsOf<MergeBuilder<Clube>>(databaseServiceMock, DatabaseType.MICROSOFT_SQL_SERVER);
        _mergeBuilderMock.Create<Clube>().Returns(builderMock);
        builderMock.Execute().Returns(Task.FromResult(new OutputModel()));

        //ACTION
        await _clubeService.Upsert(list);

        //ASSERT
        _unitOfWorkMock.Received(1).CommitTransaction();
    }
}
Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  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. 
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
2.1.0 2,863 11/3/2025
2.0.1 131 10/31/2025
2.0.0 16,130 12/9/2024
1.0.1 2,013 11/13/2024
1.0.0 162 11/8/2024

Ajustes gerais para postgreSQL. Mudança do Default Response type para: NONE