ORMData.Sqlite 10.1.2

There is a newer version of this package available.
See the version list below for details.
dotnet add package ORMData.Sqlite --version 10.1.2
                    
NuGet\Install-Package ORMData.Sqlite -Version 10.1.2
                    
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="ORMData.Sqlite" Version="10.1.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ORMData.Sqlite" Version="10.1.2" />
                    
Directory.Packages.props
<PackageReference Include="ORMData.Sqlite" />
                    
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 ORMData.Sqlite --version 10.1.2
                    
#r "nuget: ORMData.Sqlite, 10.1.2"
                    
#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 ORMData.Sqlite@10.1.2
                    
#: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=ORMData.Sqlite&version=10.1.2
                    
Install as a Cake Addin
#tool nuget:?package=ORMData.Sqlite&version=10.1.2
                    
Install as a Cake Tool

ORMData

NuGet NuGet Downloads Build Status License: MIT

ORMData es un micro-ORM ligero y eficiente para .NET 8, diseñado para simplificar el acceso a datos con soporte para múltiples proveedores de bases de datos. Implementa su propio sistema de mapeo de objetos con alto rendimiento y ofrece un patrón Repository genérico con operaciones CRUD completas.

🚀 Características

  • Soporte Multi-Base de Datos: SQL Server, PostgreSQL, MySQL, MariaDB y SQLite (Arquitectura modular)
  • Patrón Repository Genérico: Implementación completa con operaciones CRUD
  • Expresiones Lambda: Consultas type-safe usando expresiones LINQ
  • Paginación: Soporte nativo para consultas paginadas con conteo total
  • Transacciones: Manejo simplificado de transacciones asíncronas
  • Stored Procedures: Ejecución de procedimientos almacenados
  • SQL Personalizado: Ejecuta consultas SQL directas cuando sea necesario
  • Atributos de Mapeo: Sistema de atributos para configuración de entidades
  • Inyección de Dependencias: Integración completa con Microsoft.Extensions.DependencyInjection

📦 Instalación

Desde NuGet (Estructura Modular)

A partir de la versión 10.1.0, ORMData se divide en paquetes específicos por proveedor para reducir el peso de las aplicaciones:

# SQL Server
dotnet add package ORMData.SqlServer

# PostgreSQL
dotnet add package ORMData.PostgreSql

# MySQL / MariaDB
dotnet add package ORMData.MySql

# SQLite
dotnet add package ORMData.Sqlite

# Compatibilidad Total (Instala todos los anteriores)
dotnet add package ORMData.All

Desde el Código Fuente

git clone https://github.com/master-tech-team/ORMData.git
cd ORMData
dotnet build

Dependencias

El diseño modular ahora separa las dependencias pesadas:

  • ORMData (Core): BCrypt.Net-Next, Newtonsoft.Json
  • ORMData.SqlServer: Microsoft.Data.SqlClient
  • ORMData.PostgreSql: Npgsql
  • ORMData.MySql: MySqlConnector
  • ORMData.Sqlite: Microsoft.Data.Sqlite

🔧 Configuración

1. Configurar appsettings.json

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=localhost;Database=MyDb;User Id=sa;Password=YourPassword;TrustServerCertificate=True;"
  },
  "DatabaseProvider": "SqlServer"  // SqlServer, PostgreSql, MySql, etc.
}

2. Registrar servicios en Program.cs

Ahora es necesario indicar qué proveedores se van a utilizar:

using ORMData;
using ORMData.SqlServer; // Para SQL Server
using ORMData.PostgreSql; // Para PostgreSQL

var builder = WebApplication.CreateBuilder(args);

// Registrar servicios de ORMData y habilitar los proveedores instalados
builder.Services.AddInfrastructureServices(builder.Configuration)
    .AddSqlServerProvider()
    .AddPostgreSqlProvider();

var app = builder.Build();
app.Run();

3. Configuración con Options Pattern (Nuevo)

ORMData ahora soporta el patrón Options para configuración más flexible:

using ORMData;
using ORMData.Enums;

var builder = WebApplication.CreateBuilder(args);

// Opción 1: Configuración desde appsettings.json (automática)
builder.Services.AddInfrastructureServices(builder.Configuration);

// Opción 2: Configuración programática con fluent API
builder.Services.AddInfrastructureServices(builder.Configuration, options =>
{
    options.UseSqlServer("Server=localhost;Database=MyDb;...")
           .ConnectionString = "...";
});

// Opción 3: Configuración completamente programática
builder.Services.AddDbContext(builder.Configuration, options =>
{
    options.UsePostgreSql("Host=localhost;Database=MyDb;Username=user;Password=pass");
});

var app = builder.Build();
app.Run();

Métodos Fluent Disponibles:

  • UseSqlServer(connectionString) - Configura SQL Server
  • UsePostgreSql(connectionString) - Configura PostgreSQL
  • UseMySql(connectionString) - Configura MySQL
  • UseMariaDb(connectionString) - Configura MariaDB
  • UseSqlite(connectionString) - Configura SQLite

Validación Automática: El sistema valida que el proveedor configurado coincida con la cadena de conexión.

🆕 Nuevas Características

Soporte para Claves Compuestas

ORMData ahora soporta entidades con claves primarias compuestas:

using ORMData.Core;

// Definir entidad con clave compuesta
public class OrderItem
{
    [Key]
    public int OrderId { get; set; }
    
    [Key]
    public int ProductId { get; set; }
    
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

// Configurar con Fluent API
public class MyDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<OrderItem>(entity =>
        {
            entity.HasKey(e => new { e.OrderId, e.ProductId });
        });
    }
}

// Usar con el repository
var item = await _orderItemRepository.GetByIdAsync(new { OrderId = 1, ProductId = 100 });

Parámetros Tipados (SqlParams)

Nueva clase SqlParams para parámetros con tipos explícitos y mejor rendimiento:

using ORMData.Extensions;

// Sintaxis más elegante con factory method
var users = await dbContext.Database.QueryAsync<User>(
    "SELECT * FROM Users WHERE Age > @MinAge AND City = @City",
    SqlParams.Params(
        ("MinAge", 18),
        ("City", "Madrid")
    )
);

// Sintaxis con tipos explícitos
var users = await dbContext.Database.QueryAsync<User>(
    "SELECT * FROM Users WHERE Age > @MinAge",
    SqlParams.Params()
        .Add<int>("MinAge", 18)
        .Add<string>("City", "Madrid")
        .Add<bool>("IsActive", true)
);

// Collection initializer
var parameters = new SqlParams
{
    ("UserId", 123),
    ("Email", "user@example.com"),
    ("CreatedDate", DateTime.Now)
};

// Output parameters
var parameters = SqlParams.Params(("UserId", 123))
    .AddOutput<int>("TotalCount")
    .AddOutput<decimal>("TotalAmount");

Ventajas de SqlParams:

  • ⚡ Más rápido - sin reflexión para inferir tipos
  • 🎯 Control explícito sobre DbType
  • 🔧 Soporte para output parameters con tamaños específicos

DatabaseFacade Mejorado

Acceso directo a operaciones de base de datos desde DbContext.Database:

public class UserService
{
    private readonly MyDbContext _dbContext;
    
    public UserService(MyDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    
    public async Task<List<User>> GetActiveUsers()
    {
        // Consultas SQL directas con parámetros tipados
        return await _dbContext.Database.QueryAsync<User>(
            "SELECT * FROM Users WHERE IsActive = @Active",
            SqlParams.Params(("Active", true))
        );
    }
    
    // Transacciones
    public async Task<User> CreateUserWithAudit(User user)
    {
        return await _dbContext.Database.ExecuteInTransactionAsync(async () =>
        {
            var created = await _dbContext.Users.AddAsync(user);
            await _dbContext.Database.ExecuteSqlRawAsync(
                "INSERT INTO AuditLog (Action, UserId) VALUES (@Action, @UserId)",
                SqlParams.Params(("Action", "UserCreated"), ("UserId", created.Id))
            );
            return created;
        });
    }
    
    // Conexión compartida para múltiples operaciones
    public async Task<(List<User>, List<Order>)> GetUserDataOptimized(int userId)
    {
        return await _dbContext.Database.ExecuteSharedConnectionAsync(async connection =>
        {
            // Todas estas operaciones usan la MISMA conexión
            var users = await connection.QueryAsync<User>(
                "SELECT * FROM Users WHERE Id = @Id",
                SqlParams.Params(("Id", userId))
            );
            var orders = await connection.QueryAsync<Order>(
                "SELECT * FROM Orders WHERE UserId = @UserId",
                SqlParams.Params(("UserId", userId))
            );
            return (users, orders);
        });
    }
}

Métodos Disponibles en DatabaseFacade:

Objetos Genéricos:

  • QueryAsync<T>(sql, parameters) - Consulta con resultados
  • QueryFirstOrDefaultAsync<T>(sql, parameters) - Primera fila o null
  • ExecuteSqlRawAsync(sql, parameters) - Ejecutar comando
  • ExecuteScalarAsync<T>(sql, parameters) - Valor escalar
  • ExecuteStoredProcedureAsync<T>(name, parameters) - Stored procedure
  • ExecuteInTransactionAsync(operation) - Transacción automática
  • ExecuteSharedConnectionAsync(operation) - Conexión compartida

Objetos ADO.NET:

  • ExecuteDataSetAsync(sql, parameters) - DataSet con múltiples tablas
  • ExecuteDataTableAsync(sql, parameters) - DataTable para binding
  • ExecuteDataViewAsync(sql, parameters) - DataView filtrable/ordenable
  • ExecuteReaderAsync(sql, parameters) - DbDataReader para lectura eficiente

Trabajar con Objetos ADO.NET

ORMData soporta objetos ADO.NET nativos para escenarios donde necesitas trabajar con DataTables, DataViews o DataReaders:

public class ReportService
{
    private readonly MyDbContext _dbContext;
    
    public ReportService(MyDbContext dbContext)
    {
        _dbContext = dbContext;
    }
    
    // DataTable - Ideal para binding a grids y controles
    public async Task<DataTable> GetUsersDataTable()
    {
        return await _dbContext.Database.ExecuteDataTableAsync(
            "SELECT * FROM Users WHERE IsActive = @Active",
            SqlParams.Params(("Active", true))
        );
    }
    
    // DataView - Para filtrado y ordenamiento dinámico
    public async Task<DataView> GetProductsDataView()
    {
        var dataView = await _dbContext.Database.ExecuteDataViewAsync(
            "SELECT * FROM Products WHERE Price > @MinPrice",
            SqlParams.Params(("MinPrice", 100))
        );
        
        // Aplicar filtros y ordenamiento
        dataView.RowFilter = "Category = 'Electronics'";
        dataView.Sort = "Price DESC";
        
        return dataView;
    }
    
    // DataSet - Múltiples tablas relacionadas
    public async Task<DataSet> GetOrdersWithDetails()
    {
        return await _dbContext.Database.ExecuteDataSetAsync(
            @"SELECT * FROM Orders WHERE CustomerId = @Id;
              SELECT * FROM OrderItems WHERE OrderId IN (SELECT Id FROM Orders WHERE CustomerId = @Id)",
            SqlParams.Params(("Id", 123))
        );
    }
    
    // DataReader - Lectura forward-only eficiente para grandes volúmenes
    public async Task ProcessLargeDataset()
    {
        var connection = await _dbContext.Database._connectionManager.GetConnectionAsync();
        using var reader = await _dbContext.Database.ExecuteReaderAsync(
            "SELECT * FROM LargeTable WHERE Date > @Date",
            SqlParams.Params(("Date", DateTime.Now.AddMonths(-1)))
        );
        
        while (await reader.ReadAsync())
        {
            var id = reader.GetInt32(0);
            var name = reader.GetString(1);
            var amount = reader.GetDecimal(2);
            
            // Procesar fila por fila sin cargar todo en memoria
            await ProcessRow(id, name, amount);
        }
    }
}

Casos de Uso para Objetos ADO.NET:

Objeto Mejor Para Ventajas
DataTable Binding a UI (grids, controles) Fácil de usar, soporta binding bidireccional
DataView Filtrado/ordenamiento dinámico Vistas personalizadas sin re-consultar
DataSet Múltiples tablas relacionadas Mantiene relaciones entre tablas
DataReader Grandes volúmenes de datos Mínimo uso de memoria, máximo rendimiento

📝 Uso Básico

Definir una Entidad

using ORMData.Attributes;

[Table("Users")]
public class User
{
    [Key]
    [Column("UserId")]
    public int Id { get; set; }
    
    [Column("Username")]
    public string UserName { get; set; } = string.Empty;
    
    [Column("Email")]
    public string Email { get; set; } = string.Empty;
    
    [Column("CreatedAt")]
    public DateTime CreatedAt { get; set; }
    
    [NotMapped]
    public string TempData { get; set; } = string.Empty;
    
    [ForeignKey("Roles", "RoleId")]
    public int RoleId { get; set; }
    
    [Json]
    public Dictionary<string, object>? Metadata { get; set; }
}

Atributos Disponibles

  • [Table]: Define el nombre de la tabla en la base de datos
  • [Key]: Marca una propiedad como clave primaria
  • [Column]: Especifica el nombre de la columna en la base de datos
  • [NotMapped]: Excluye una propiedad del mapeo
  • [ForeignKey]: Define una relación de clave foránea
  • [Json]: Serializa/deserializa propiedades complejas como JSON

Usar el Repository

public class UserService
{
    private readonly IBaseRepository<User> _userRepository;
    
    public UserService(IBaseRepository<User> userRepository)
    {
        _userRepository = userRepository;
    }
    
    // Obtener por ID
    public async Task<User?> GetUserById(int id)
    {
        return await _userRepository.GetByIdAsync(id);
    }
    
    // Obtener todos
    public async Task<IEnumerable<User>> GetAllUsers()
    {
        return await _userRepository.GetAllAsync();
    }
    
    // Buscar con expresión lambda
    public async Task<IEnumerable<User>> GetUsersByEmail(string email)
    {
        return await _userRepository.FindAsync(u => u.Email == email);
    }
    
    // Obtener con paginación
    public async Task<(IEnumerable<User> Users, int Total)> GetUsersPaged(int page, int pageSize)
    {
        return await _userRepository.GetPagedAsync(
            pageNumber: page,
            pageSize: pageSize,
            filter: u => u.CreatedAt > DateTime.Now.AddYears(-1),
            orderBy: u => u.CreatedAt,
            orderByDescending: true
        );
    }
    
    // Crear usuario
    public async Task<User> CreateUser(User user)
    {
        return await _userRepository.AddAsync(user);
    }
    
    // Actualizar usuario
    public async Task<User> UpdateUser(User user)
    {
        return await _userRepository.UpdateAsync(user);
    }
    
    // Eliminar usuario
    public async Task<bool> DeleteUser(int id)
    {
        return await _userRepository.DeleteAsync(id);
    }
    
    // Contar usuarios
    public async Task<int> CountActiveUsers()
    {
        return await _userRepository.CountAsync(u => u.CreatedAt > DateTime.Now.AddDays(-30));
    }
    
    // Verificar existencia
    public async Task<bool> EmailExists(string email)
    {
        return await _userRepository.ExistsAsync(u => u.Email == email);
    }
}

🔥 Características Avanzadas

Soporte para Múltiples Tipos de ID

ORMData soporta diferentes tipos de claves primarias: int, Guid, long, y string.

// Entidad con Guid como clave
[Table("Products")]
public class Product
{
    [Key]
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
}

// Entidad con long como clave
[Table("Orders")]
public class Order
{
    [Key]
    public long Id { get; set; }
    public DateTime OrderDate { get; set; }
}

// Entidad con string como clave
[Table("Categories")]
public class Category
{
    [Key]
    public string Code { get; set; } = string.Empty;
    public string Name { get; set; } = string.Empty;
}

// Uso con diferentes tipos de ID
var product = await _productRepository.GetByIdAsync(Guid.Parse("550e8400-e29b-41d4-a716-446655440000"));
var order = await _orderRepository.GetByIdAsync(123456789L);
var category = await _categoryRepository.GetByIdAsync("ELECTRONICS");

// Obtener múltiples por IDs
var productIds = new[] { guid1, guid2, guid3 };
var products = await _productRepository.GetByIdsAsync(productIds);

// Obtener como diccionario para acceso rápido
var productDict = await _productRepository.GetByIdsDictionaryAsync(productIds);
var product1 = productDict[guid1];

// Verificar existencia por ID
var exists = await _productRepository.ExistsAsync(guid1);

// Eliminar por ID con tipo genérico
await _productRepository.DeleteAsync(guid1);

Operaciones por Lotes Optimizadas

// BulkInsert - Inserción masiva optimizada (más rápido que AddRangeAsync)
var users = new List<User>();
for (int i = 0; i < 1000; i++)
{
    users.Add(new User { UserName = $"user{i}", Email = $"user{i}@example.com" });
}
var insertedCount = await _userRepository.BulkInsertAsync(users);
Console.WriteLine($"{insertedCount} usuarios insertados");

// BulkUpdate - Actualización masiva
foreach (var user in users)
{
    user.Email = user.Email.Replace("example.com", "newdomain.com");
}
var updatedCount = await _userRepository.BulkUpdateAsync(users);

// BulkDelete - Eliminación masiva por entidades
var inactiveUsers = await _userRepository.FindAsync(u => u.IsActive == false);
var deletedCount = await _userRepository.BulkDeleteAsync(inactiveUsers);

Operaciones Avanzadas de Consulta

// AnyAsync - Verificar si existen registros (más eficiente que Count > 0)
var hasActiveUsers = await _userRepository.AnyAsync(u => u.IsActive);
var hasAnyUser = await _userRepository.AnyAsync(); // Sin filtro

// SingleOrDefaultAsync - Obtener un único registro (lanza excepción si hay más de uno)
try
{
    var admin = await _userRepository.SingleOrDefaultAsync(u => u.Role == "Admin");
}
catch (InvalidOperationException)
{
    Console.WriteLine("Hay más de un administrador");
}

// FirstOrDefaultAsync - Obtener el primero
var firstUser = await _userRepository.FirstOrDefaultAsync();
var firstActive = await _userRepository.FirstOrDefaultAsync(u => u.IsActive);

// ExistsAsync con ID genérico
var userExists = await _userRepository.ExistsAsync(userId);

Trabajar con Diccionarios de Entidades

// Obtener productos como diccionario por ID para acceso O(1)
var orderItems = await _orderItemRepository.GetAllAsync();
var productIds = orderItems.Select(oi => oi.ProductId).Distinct();
var productDict = await _productRepository.GetByIdsDictionaryAsync(productIds);

// Usar el diccionario para acceso rápido sin múltiples queries
foreach (var item in orderItems)
{
    if (productDict.TryGetValue(item.ProductId, out var product))
    {
        item.ProductName = product.Name;
        item.UnitPrice = product.Price;
        item.Total = item.Quantity * product.Price;
    }
}

Consultas SQL Personalizadas

// Consulta personalizada
var users = await _userRepository.QueryAsync(
    "SELECT * FROM Users WHERE Email LIKE @Pattern",
    new { Pattern = "%@gmail.com" }
);

// Ejecutar comando
var rowsAffected = await _userRepository.ExecuteAsync(
    "UPDATE Users SET LastLogin = @Now WHERE UserId = @Id",
    new { Now = DateTime.UtcNow, Id = userId }
);

// Ejecutar escalar
var maxId = await _userRepository.ExecuteScalarAsync<int>(
    "SELECT MAX(UserId) FROM Users"
);

Stored Procedures

// Ejecutar procedimiento almacenado
var users = await _userRepository.ExecuteStoredProcedureAsync(
    "GetUsersByRole",
    new { RoleId = 1 }
);

// Ejecutar procedimiento sin resultado
var affected = await _userRepository.ExecuteStoredProcedureNonQueryAsync(
    "UpdateUserStatus",
    new { UserId = 1, Status = "Active" }
);

Transacciones

// Transacción con retorno de valor
var result = await _userRepository.ExecuteInTransactionAsync(async () =>
{
    var user = await _userRepository.AddAsync(newUser);
    await _userRepository.ExecuteAsync(
        "INSERT INTO AuditLog (Action, UserId) VALUES (@Action, @UserId)",
        new { Action = "UserCreated", UserId = user.Id }
    );
    return user;
});

// Transacción sin retorno
await _userRepository.ExecuteInTransactionAsync(async () =>
{
    await _userRepository.DeleteAsync(userId);
    await _userRepository.ExecuteAsync(
        "DELETE FROM UserRoles WHERE UserId = @UserId",
        new { UserId = userId }
    );
});

Transacciones Manuales con IDbConnectionManager

Para control más fino sobre las transacciones, puedes usar IDbConnectionManager directamente:

public class OrderService
{
    private readonly IDbConnectionManager _connectionManager;
    private readonly IBaseRepository<Order> _orderRepository;
    private readonly IBaseRepository<OrderItem> _orderItemRepository;
    
    public OrderService(
        IDbConnectionManager connectionManager,
        IBaseRepository<Order> orderRepository,
        IBaseRepository<OrderItem> orderItemRepository)
    {
        _connectionManager = connectionManager;
        _orderRepository = orderRepository;
        _orderItemRepository = orderItemRepository;
    }
    
    // Transacción asíncrona con BeginTransactionAsync
    public async Task<Order> CreateOrderAsync(Order order, List<OrderItem> items)
    {
        using var connection = await _connectionManager.GetConnectionAsync();
        using var transaction = await _connectionManager.BeginTransactionAsync(IsolationLevel.ReadCommitted);
        
        try
        {
            // Insertar orden
            var createdOrder = await _orderRepository.AddAsync(order);
            
            // Insertar items
            foreach (var item in items)
            {
                item.OrderId = createdOrder.Id;
                await _orderItemRepository.AddAsync(item);
            }
            
            // Confirmar transacción
            transaction.Commit();
            return createdOrder;
        }
        catch
        {
            // Revertir en caso de error
            transaction.Rollback();
            throw;
        }
    }
    
    // Transacción síncrona con BeginTransaction
    public Order CreateOrder(Order order, List<OrderItem> items)
    {
        using var connection = _connectionManager.GetConnection();
        using var transaction = _connectionManager.BeginTransaction(IsolationLevel.Serializable);
        
        try
        {
            var createdOrder = _orderRepository.AddAsync(order).Result;
            
            foreach (var item in items)
            {
                item.OrderId = createdOrder.Id;
                _orderItemRepository.AddAsync(item).Wait();
            }
            
            transaction.Commit();
            return createdOrder;
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
    }
    
    // Transacción con múltiples operaciones y niveles de aislamiento personalizados
    public async Task<bool> TransferInventoryAsync(int fromWarehouseId, int toWarehouseId, int productId, int quantity)
    {
        using var connection = await _connectionManager.GetConnectionAsync();
        // Usar nivel de aislamiento Serializable para evitar lecturas fantasma
        using var transaction = await _connectionManager.BeginTransactionAsync(IsolationLevel.Serializable);
        
        try
        {
            // Reducir inventario del almacén origen
            var rowsAffected = await _orderRepository.ExecuteAsync(
                @"UPDATE Inventory 
                  SET Quantity = Quantity - @Quantity 
                  WHERE WarehouseId = @WarehouseId 
                    AND ProductId = @ProductId 
                    AND Quantity >= @Quantity",
                new { Quantity = quantity, WarehouseId = fromWarehouseId, ProductId = productId }
            );
            
            if (rowsAffected == 0)
            {
                throw new InvalidOperationException("Inventario insuficiente");
            }
            
            // Incrementar inventario del almacén destino
            await _orderRepository.ExecuteAsync(
                @"UPDATE Inventory 
                  SET Quantity = Quantity + @Quantity 
                  WHERE WarehouseId = @WarehouseId 
                    AND ProductId = @ProductId",
                new { Quantity = quantity, WarehouseId = toWarehouseId, ProductId = productId }
            );
            
            // Registrar la transferencia
            await _orderRepository.ExecuteAsync(
                @"INSERT INTO InventoryTransfers (FromWarehouseId, ToWarehouseId, ProductId, Quantity, TransferDate)
                  VALUES (@FromWarehouseId, @ToWarehouseId, @ProductId, @Quantity, @TransferDate)",
                new 
                { 
                    FromWarehouseId = fromWarehouseId, 
                    ToWarehouseId = toWarehouseId, 
                    ProductId = productId, 
                    Quantity = quantity,
                    TransferDate = DateTime.UtcNow
                }
            );
            
            transaction.Commit();
            return true;
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
    }
}

Niveles de Aislamiento de Transacciones

// ReadUncommitted - Permite lecturas sucias (menor aislamiento, mayor rendimiento)
var transaction = await _connectionManager.BeginTransactionAsync(IsolationLevel.ReadUncommitted);

// ReadCommitted - Evita lecturas sucias (nivel por defecto)
var transaction = await _connectionManager.BeginTransactionAsync(IsolationLevel.ReadCommitted);

// RepeatableRead - Evita lecturas no repetibles
var transaction = await _connectionManager.BeginTransactionAsync(IsolationLevel.RepeatableRead);

// Serializable - Mayor aislamiento, evita lecturas fantasma (menor rendimiento)
var transaction = await _connectionManager.BeginTransactionAsync(IsolationLevel.Serializable);

// Snapshot - Usa versionado de filas (solo SQL Server)
var transaction = await _connectionManager.BeginTransactionAsync(IsolationLevel.Snapshot);

Compartir la Misma Conexión (Connection Scope)

ORMData utiliza AsyncLocal<ConnectionScope> para reutilizar automáticamente la misma conexión de base de datos dentro del mismo contexto de ejecución. Esto evita abrir múltiples conexiones innecesarias y permite compartir transacciones.

public class ComplexService
{
    private readonly IDbConnectionManager _connectionManager;
    private readonly IBaseRepository<User> _userRepository;
    private readonly IBaseRepository<Order> _orderRepository;
    private readonly IBaseRepository<AuditLog> _auditRepository;
    
    public ComplexService(
        IDbConnectionManager connectionManager,
        IBaseRepository<User> userRepository,
        IBaseRepository<Order> orderRepository,
        IBaseRepository<AuditLog> auditRepository)
    {
        _connectionManager = connectionManager;
        _userRepository = userRepository;
        _orderRepository = orderRepository;
        _auditRepository = auditRepository;
    }
    
    // ✅ CORRECTO: Una sola conexión compartida entre múltiples operaciones
    public async Task<Order> ProcessOrderAsync(int userId, Order order)
    {
        // La primera llamada abre la conexión
        using var connection = await _connectionManager.GetConnectionAsync();
        using var transaction = await _connectionManager.BeginTransactionAsync();
        
        try
        {
            // Todas estas operaciones REUTILIZAN la misma conexión y transacción
            var user = await _userRepository.GetByIdAsync(userId);
            if (user == null)
                throw new InvalidOperationException("Usuario no encontrado");
            
            // Esta operación usa la MISMA conexión
            var newOrder = await _orderRepository.AddAsync(order);
            
            // Esta también usa la MISMA conexión
            await _auditRepository.AddAsync(new AuditLog
            {
                Action = "OrderCreated",
                UserId = userId,
                OrderId = newOrder.Id,
                Timestamp = DateTime.UtcNow
            });
            
            // Actualizar el usuario - MISMA conexión
            user.LastOrderDate = DateTime.UtcNow;
            await _userRepository.UpdateAsync(user);
            
            transaction.Commit();
            return newOrder;
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
    }
    
    // ✅ CORRECTO: Operaciones anidadas comparten la conexión
    public async Task<bool> TransferWithAuditAsync(int fromUserId, int toUserId, decimal amount)
    {
        using var connection = await _connectionManager.GetConnectionAsync();
        using var transaction = await _connectionManager.BeginTransactionAsync();
        
        try
        {
            // Primera operación
            await UpdateBalanceAsync(fromUserId, -amount); // Usa la conexión existente
            
            // Segunda operación
            await UpdateBalanceAsync(toUserId, amount); // Usa la MISMA conexión
            
            // Tercera operación
            await _auditRepository.AddAsync(new AuditLog
            {
                Action = "Transfer",
                FromUserId = fromUserId,
                ToUserId = toUserId,
                Amount = amount
            }); // También usa la MISMA conexión
            
            transaction.Commit();
            return true;
        }
        catch
        {
            transaction.Rollback();
            throw;
        }
    }
    
    private async Task UpdateBalanceAsync(int userId, decimal amount)
    {
        // Esta operación NO abre una nueva conexión
        // Reutiliza la conexión del contexto actual
        await _userRepository.ExecuteAsync(
            "UPDATE Users SET Balance = Balance + @Amount WHERE UserId = @UserId",
            new { Amount = amount, UserId = userId }
        );
    }
}

Ejemplo: Sin Conexión Compartida vs Con Conexión Compartida

// ❌ INCORRECTO: Cada operación abre su propia conexión
public async Task<Order> ProcessOrderIncorrectAsync(int userId, Order order)
{
    // Primera conexión
    var user = await _userRepository.GetByIdAsync(userId);
    
    // Segunda conexión (NO comparte transacción)
    var newOrder = await _orderRepository.AddAsync(order);
    
    // Tercera conexión (NO comparte transacción)
    await _auditRepository.AddAsync(new AuditLog { /* ... */ });
    
    // ⚠️ PROBLEMA: Si falla aquí, las operaciones anteriores ya se confirmaron
    user.LastOrderDate = DateTime.UtcNow;
    await _userRepository.UpdateAsync(user);
    
    return newOrder;
}

// ✅ CORRECTO: Una conexión compartida con transacción
public async Task<Order> ProcessOrderCorrectAsync(int userId, Order order)
{
    using var connection = await _connectionManager.GetConnectionAsync();
    using var transaction = await _connectionManager.BeginTransactionAsync();
    
    try
    {
        // Todas usan la MISMA conexión y transacción
        var user = await _userRepository.GetByIdAsync(userId);
        var newOrder = await _orderRepository.AddAsync(order);
        await _auditRepository.AddAsync(new AuditLog { /* ... */ });
        
        user.LastOrderDate = DateTime.UtcNow;
        await _userRepository.UpdateAsync(user);
        
        transaction.Commit();
        return newOrder;
    }
    catch
    {
        // Si algo falla, TODO se revierte
        transaction.Rollback();
        throw;
    }
}

Beneficios del Connection Scope

  1. Rendimiento: Evita el overhead de abrir múltiples conexiones
  2. Atomicidad: Todas las operaciones comparten la misma transacción
  3. Consistencia: Los datos se ven de forma consistente durante toda la operación
  4. Gestión Automática: No necesitas pasar la conexión manualmente entre métodos
  5. Thread-Safe: Cada contexto de ejecución (thread/task) tiene su propia conexión

Ejemplo Avanzado: Operaciones Complejas con Scope Compartido

public class InvoiceService
{
    private readonly IDbConnectionManager _connectionManager;
    private readonly IBaseRepository<Invoice> _invoiceRepository;
    private readonly IBaseRepository<InvoiceItem> _itemRepository;
    private readonly IBaseRepository<Payment> _paymentRepository;
    private readonly IBaseRepository<Customer> _customerRepository;
    
    public async Task<Invoice> CreateInvoiceWithPaymentAsync(
        int customerId, 
        List<InvoiceItem> items,
        Payment payment)
    {
        // Obtener conexión una sola vez
        using var connection = await _connectionManager.GetConnectionAsync();
        using var transaction = await _connectionManager.BeginTransactionAsync(IsolationLevel.Serializable);
        
        try
        {
            // 1. Verificar cliente (usa la conexión compartida)
            var customer = await _customerRepository.GetByIdAsync(customerId);
            if (customer == null)
                throw new InvalidOperationException("Cliente no encontrado");
            
            // 2. Crear factura (misma conexión)
            var invoice = new Invoice
            {
                CustomerId = customerId,
                InvoiceDate = DateTime.UtcNow,
                TotalAmount = items.Sum(i => i.Amount)
            };
            invoice = await _invoiceRepository.AddAsync(invoice);
            
            // 3. Agregar items (misma conexión, en loop)
            foreach (var item in items)
            {
                item.InvoiceId = invoice.Id;
                await _itemRepository.AddAsync(item);
            }
            
            // 4. Registrar pago (misma conexión)
            payment.InvoiceId = invoice.Id;
            await _paymentRepository.AddAsync(payment);
            
            // 5. Actualizar balance del cliente (misma conexión)
            customer.Balance += invoice.TotalAmount;
            customer.LastInvoiceDate = DateTime.UtcNow;
            await _customerRepository.UpdateAsync(customer);
            
            // 6. Actualizar estadísticas con SQL directo (misma conexión)
            await _invoiceRepository.ExecuteAsync(
                @"UPDATE CustomerStats 
                  SET TotalInvoices = TotalInvoices + 1,
                      TotalAmount = TotalAmount + @Amount
                  WHERE CustomerId = @CustomerId",
                new { Amount = invoice.TotalAmount, CustomerId = customerId }
            );
            
            // Todo fue exitoso, confirmar
            transaction.Commit();
            
            Console.WriteLine($"Se ejecutaron 6+ operaciones con UNA SOLA conexión");
            return invoice;
        }
        catch (Exception ex)
        {
            transaction.Rollback();
            Console.WriteLine($"Error: {ex.Message} - Todas las operaciones revertidas");
            throw;
        }
    }
}

Notas Importantes

  • No es necesario pasar la conexión: El ConnectionScope se gestiona automáticamente
  • Thread-safe: Cada thread/task tiene su propia instancia de conexión
  • Dispose obligatorio: Siempre usa using para liberar la conexión correctamente
  • Un scope por operación: Crea un scope al inicio de tu operación lógica
  • Transacciones automáticas: Si creas una transacción, todos los repositorios la usan

Operaciones por Lotes

// Insertar múltiples registros
var users = new List<User> { user1, user2, user3 };
var inserted = await _userRepository.AddRangeAsync(users);

// Actualizar múltiples registros
var updated = await _userRepository.UpdateRangeAsync(users);

// Eliminar múltiples registros con expresión
var deletedCount = await _userRepository.DeleteRangeAsync(u => u.CreatedAt < DateTime.Now.AddYears(-5));

// Eliminación masiva optimizada con BulkDelete
var oldUsers = await _userRepository.FindAsync(u => u.LastLogin < DateTime.Now.AddYears(-2));
var bulkDeleted = await _userRepository.BulkDeleteAsync(oldUsers);

🗃️ Proveedores de Base de Datos Soportados

El enum DatabaseProvider define los siguientes proveedores:

public enum DatabaseProvider
{
    SqlServer = 0,    // SQL Server
    PostgreSql = 1,   // PostgreSQL
    MySql = 2,        // MySQL / MariaDB
    MariaDb = 3,      // MariaDB
    Sqlite = 4,       // SQLite
    Oracle = 5        // Oracle
}

🏗️ Arquitectura

Componentes Principales

  • BaseRepository<T>: Implementación del patrón Repository genérico
  • IDbConnectionManager: Gestión de conexiones a base de datos
  • ISqlDialect: Abstracción para dialectos SQL específicos de cada proveedor
  • WhereBuilder: Construcción de cláusulas WHERE desde expresiones lambda
  • ConnectionScope: Gestión de ámbitos de conexión

Dialectos SQL

  • SqlServerDialect: Implementación para SQL Server
  • PostgreSqlDialect: Implementación para PostgreSQL

📋 API del IBaseRepository

Operaciones de Lectura

  • GetByIdAsync<TKey>(TKey id) - Obtener por ID (soporta int, Guid, long, string)
  • GetByIdsAsync<TKey>(IEnumerable<TKey> ids) - Obtener múltiples registros por IDs
  • GetByIdsDictionaryAsync<TKey>(IEnumerable<TKey> ids) - Obtener como diccionario indexado por ID
  • GetAllAsync() - Obtener todos los registros
  • FindAsync(Expression<Func<T, bool>> predicate) - Buscar con expresión
  • FindFirstOrDefaultAsync(Expression<Func<T, bool>> predicate) - Buscar primero o nulo
  • FirstOrDefaultAsync(Expression<Func<T, bool>>? predicate = null) - Obtener el primer registro
  • SingleOrDefaultAsync(Expression<Func<T, bool>> predicate) - Obtener único registro (error si hay más de uno)
  • GetPagedAsync(...) - Obtener con paginación

Operaciones de Escritura

  • AddAsync(T entity) - Agregar un registro
  • AddRangeAsync(IEnumerable<T> entities) - Agregar múltiples registros
  • UpdateAsync(T entity) - Actualizar un registro
  • UpdateRangeAsync(IEnumerable<T> entities) - Actualizar múltiples registros
  • DeleteAsync<TKey>(TKey id) - Eliminar por ID (soporta múltiples tipos)
  • DeleteAsync(T entity) - Eliminar entidad
  • DeleteRangeAsync(Expression<Func<T, bool>> predicate) - Eliminar múltiples registros con expresión

Operaciones por Lotes (Bulk Operations)

  • BulkInsertAsync(IEnumerable<T> entities) - Inserción masiva optimizada
  • BulkUpdateAsync(IEnumerable<T> entities) - Actualización masiva optimizada
  • BulkDeleteAsync(IEnumerable<T> entities) - Eliminación masiva optimizada

Operaciones de Conteo y Existencia

  • CountAsync(Expression<Func<T, bool>>? predicate = null) - Contar registros
  • ExistsAsync(Expression<Func<T, bool>> predicate) - Verificar existencia con expresión
  • ExistsAsync<TKey>(TKey id) - Verificar existencia por ID
  • AnyAsync(Expression<Func<T, bool>>? predicate = null) - Verificar si existen registros

SQL Personalizado

  • QueryAsync(string sql, object? parameters = null) - Consulta personalizada
  • QueryFirstOrDefaultAsync(string sql, object? parameters = null) - Primera o nulo
  • ExecuteAsync(string sql, object? parameters = null) - Ejecutar comando
  • ExecuteScalarAsync<TResult>(string sql, object? parameters = null) - Ejecutar escalar

Stored Procedures

  • ExecuteStoredProcedureAsync(string procedureName, object? parameters = null)
  • ExecuteStoredProcedureFirstOrDefaultAsync(string procedureName, object? parameters = null)
  • ExecuteStoredProcedureNonQueryAsync(string procedureName, object? parameters = null)

Transacciones

  • ExecuteInTransactionAsync<TResult>(Func<Task<TResult>> operation)

  • ExecuteInTransactionAsync(Func<Task> operation)

  • Sección de Database Migrations para agregar al README.md

Insertar después de la línea 81 (después de app.Run(); en la sección de configuración)


🗄️ Database Migrations

ORMData incluye un sistema de migraciones robusto y compatible con DI que soporta todos los proveedores de bases de datos. Las migraciones se aplican automáticamente desde archivos SQL organizados en una carpeta.

Configuración de Migraciones

1. Registrar el Migration Runner
using ORMData;

var builder = WebApplication.CreateBuilder(args);

// Registrar servicios de infraestructura
builder.Services.AddInfrastructureServices(builder.Configuration);

// Registrar el Migration Runner con configuración
builder.Services.AddMigrationRunner(options =>
{
    options.MigrationsFolder = Path.Combine(AppContext.BaseDirectory, "Migrations");
    options.AutoApplyOnStartup = true;  // Aplicar automáticamente al iniciar
    options.ConnectionStringKey = "DefaultConnection";  // Opcional
});

var app = builder.Build();

// Aplicar migraciones en el startup (si AutoApplyOnStartup = true)
app.UseMigrations();

app.Run();
2. Estructura de Carpeta de Migraciones

Organiza tus scripts SQL en una carpeta Migrations con nomenclatura secuencial:

Migrations/
├── 001_CreateUsersTable.sql
├── 002_CreateRolesTable.sql
├── 003_AddUserRoleRelation.sql
└── 004_AddIndexes.sql

Importante: Los archivos deben:

  • Comenzar con un número secuencial (001, 002, etc.)
  • Tener extensión .sql
  • Estar ordenados alfabéticamente para ejecución correcta
3. Ejemplo de Script de Migración

SQL Server:

-- 001_CreateUsersTable.sql
IF OBJECT_ID(N'dbo.Users', 'U') IS NULL
BEGIN
    CREATE TABLE dbo.Users
    (
        Id INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
        Username NVARCHAR(100) NOT NULL,
        Email NVARCHAR(255) NOT NULL,
        CreatedAt DATETIME NOT NULL DEFAULT GETDATE()
    );
END
GO

CREATE INDEX IX_Users_Email ON dbo.Users(Email);
GO

PostgreSQL:

-- 001_CreateUsersTable.sql
CREATE TABLE IF NOT EXISTS Users (
    Id SERIAL PRIMARY KEY,
    Username VARCHAR(100) NOT NULL,
    Email VARCHAR(255) NOT NULL,
    CreatedAt TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX IF NOT EXISTS IX_Users_Email ON Users(Email);

MySQL/MariaDB:

-- 001_CreateUsersTable.sql
CREATE TABLE IF NOT EXISTS Users (
    Id INT AUTO_INCREMENT PRIMARY KEY,
    Username VARCHAR(100) NOT NULL,
    Email VARCHAR(255) NOT NULL,
    CreatedAt DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE INDEX IX_Users_Email ON Users(Email);

SQLite:

-- 001_CreateUsersTable.sql
CREATE TABLE IF NOT EXISTS Users (
    Id INTEGER PRIMARY KEY AUTOINCREMENT,
    Username TEXT NOT NULL,
    Email TEXT NOT NULL,
    CreatedAt DATETIME NOT NULL DEFAULT (CURRENT_TIMESTAMP)
);

CREATE INDEX IF NOT EXISTS IX_Users_Email ON Users(Email);

Opciones de Configuración

public class MigrationOptions
{
    // Carpeta donde se encuentran los scripts SQL
    public string MigrationsFolder { get; set; } = "Migrations";
    
    // Aplicar migraciones automáticamente al iniciar la aplicación
    public bool AutoApplyOnStartup { get; set; } = false;
    
    // Clave del connection string (opcional, usa DefaultConnection por defecto)
    public string? ConnectionStringKey { get; set; }
}

Uso Manual de Migraciones

Si prefieres control manual sobre cuándo aplicar las migraciones:

public class DatabaseInitializationService
{
    private readonly IMigrationRunner _migrationRunner;
    private readonly ILogger<DatabaseInitializationService> _logger;

    public DatabaseInitializationService(
        IMigrationRunner migrationRunner,
        ILogger<DatabaseInitializationService> logger)
    {
        _migrationRunner = migrationRunner;
        _logger = logger;
    }

    public async Task InitializeDatabaseAsync()
    {
        try
        {
            _logger.LogInformation("Iniciando migraciones de base de datos...");
            await _migrationRunner.ApplyMigrationsAsync();
            _logger.LogInformation("Migraciones aplicadas exitosamente.");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error al aplicar migraciones.");
            throw;
        }
    }
}

Aplicación Condicional de Migraciones

Aplicar migraciones solo en ciertos entornos:

var app = builder.Build();

// Solo aplicar migraciones en desarrollo
if (app.Environment.IsDevelopment())
{
    using var scope = app.Services.CreateScope();
    var migrationRunner = scope.ServiceProvider.GetRequiredService<IMigrationRunner>();
    await migrationRunner.ApplyMigrationsAsync();
}

app.Run();

Soporte Multi-Base de Datos

El Migration Runner detecta automáticamente el proveedor de base de datos y aplica la sintaxis DDL correcta:

Proveedor Detección Sintaxis CREATE TABLE Auto-Increment
SQL Server DatabaseProvider.SqlServer IF OBJECT_ID(...) IDENTITY(1,1)
PostgreSQL DatabaseProvider.PostgreSql CREATE TABLE IF NOT EXISTS SERIAL
MySQL DatabaseProvider.MySql CREATE TABLE IF NOT EXISTS AUTO_INCREMENT
MariaDB DatabaseProvider.MariaDb CREATE TABLE IF NOT EXISTS AUTO_INCREMENT
SQLite DatabaseProvider.Sqlite CREATE TABLE IF NOT EXISTS AUTOINCREMENT

Tabla de Seguimiento de Migraciones

El sistema crea automáticamente una tabla __Migrations para rastrear qué migraciones se han aplicado:

-- Estructura de la tabla (varía según el proveedor)
CREATE TABLE __Migrations (
    Id INT IDENTITY(1,1) PRIMARY KEY,
    MigrationId NVARCHAR(800) NOT NULL,
    AppliedOn DATETIME NOT NULL DEFAULT GETDATE(),
    IsSuccessful BIT NOT NULL DEFAULT 1,
    ErrorMessage NVARCHAR(MAX) NULL
);

Características del Migration Runner

  • Soporte para GO statements: Divide scripts SQL Server en batches
  • Transacciones por migración: Cada script se ejecuta en su propia transacción
  • Rollback automático: Si falla una migración, se revierte automáticamente
  • Logging detallado: Registra cada paso del proceso de migración
  • Idempotencia: Las migraciones ya aplicadas no se vuelven a ejecutar
  • Detección de errores: Captura y registra errores con detalles completos

Método Obsoleto (Backward Compatibility)

⚠️ Deprecado: El siguiente método está marcado como obsoleto y se eliminará en futuras versiones.

// ❌ Método antiguo (obsoleto)
app.MigrationDataBase();

// ✅ Método nuevo (recomendado)
builder.Services.AddMigrationRunner(options => { /* config */ });
app.UseMigrations();

🤝 Contribuciones

Las contribuciones son bienvenidas. Por favor, abre un issue o pull request para sugerencias y mejoras.

📄 Licencia

Este proyecto está bajo la Licencia MIT.

👥 Autores

Master Tech Team


⭐ Si te resulta útil este proyecto, considera darle una estrella en GitHub!

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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 (1)

Showing the top 1 NuGet packages that depend on ORMData.Sqlite:

Package Downloads
ORMData.All

Meta-package that includes all database providers for ORMData ORM. Provides backward compatibility with previous monolithic versions. For production use, consider installing only the specific provider package you need (ORMData.SqlServer, ORMData.PostgreSql, ORMData.MySql, or ORMData.Sqlite) to reduce package size.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.1.4 86 1/29/2026
10.1.3 94 1/6/2026
10.1.2 184 12/23/2025
10.1.1 175 12/23/2025
10.1.0 180 12/22/2025

v10.1.1
Arquitectura Modular de Proveedores NuGet

       - Refactorización total para desacoplamiento de proveedores de base de datos.
       - Core de ORMData ahora es ligero y libre de dependencias de drivers específicos.
       - Introducción de IProviderFactory y abstracciones para modularidad.
       - Nuevos paquetes específicos: ORMData.SqlServer, ORMData.PostgreSql, ORMData.MySql, ORMData.Sqlite.
       - Paquete meta ORMData.All para compatibilidad total con versiones anteriores.
       - Actualización a .NET 10.
       - Reducción masiva del tamaño de paquetes individuales.