ORMData.Sqlite
10.1.2
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
<PackageReference Include="ORMData.Sqlite" Version="10.1.2" />
<PackageVersion Include="ORMData.Sqlite" Version="10.1.2" />
<PackageReference Include="ORMData.Sqlite" />
paket add ORMData.Sqlite --version 10.1.2
#r "nuget: ORMData.Sqlite, 10.1.2"
#:package ORMData.Sqlite@10.1.2
#addin nuget:?package=ORMData.Sqlite&version=10.1.2
#tool nuget:?package=ORMData.Sqlite&version=10.1.2
ORMData
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 ServerUsePostgreSql(connectionString)- Configura PostgreSQLUseMySql(connectionString)- Configura MySQLUseMariaDb(connectionString)- Configura MariaDBUseSqlite(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 resultadosQueryFirstOrDefaultAsync<T>(sql, parameters)- Primera fila o nullExecuteSqlRawAsync(sql, parameters)- Ejecutar comandoExecuteScalarAsync<T>(sql, parameters)- Valor escalarExecuteStoredProcedureAsync<T>(name, parameters)- Stored procedureExecuteInTransactionAsync(operation)- Transacción automáticaExecuteSharedConnectionAsync(operation)- Conexión compartida
Objetos ADO.NET:
ExecuteDataSetAsync(sql, parameters)- DataSet con múltiples tablasExecuteDataTableAsync(sql, parameters)- DataTable para bindingExecuteDataViewAsync(sql, parameters)- DataView filtrable/ordenableExecuteReaderAsync(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
- Rendimiento: Evita el overhead de abrir múltiples conexiones
- Atomicidad: Todas las operaciones comparten la misma transacción
- Consistencia: Los datos se ven de forma consistente durante toda la operación
- Gestión Automática: No necesitas pasar la conexión manualmente entre métodos
- 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
ConnectionScopese gestiona automáticamente - Thread-safe: Cada thread/task tiene su propia instancia de conexión
- Dispose obligatorio: Siempre usa
usingpara 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éricoIDbConnectionManager: Gestión de conexiones a base de datosISqlDialect: Abstracción para dialectos SQL específicos de cada proveedorWhereBuilder: Construcción de cláusulas WHERE desde expresiones lambdaConnectionScope: Gestión de ámbitos de conexión
Dialectos SQL
SqlServerDialect: Implementación para SQL ServerPostgreSqlDialect: 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 IDsGetByIdsDictionaryAsync<TKey>(IEnumerable<TKey> ids)- Obtener como diccionario indexado por IDGetAllAsync()- Obtener todos los registrosFindAsync(Expression<Func<T, bool>> predicate)- Buscar con expresiónFindFirstOrDefaultAsync(Expression<Func<T, bool>> predicate)- Buscar primero o nuloFirstOrDefaultAsync(Expression<Func<T, bool>>? predicate = null)- Obtener el primer registroSingleOrDefaultAsync(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 registroAddRangeAsync(IEnumerable<T> entities)- Agregar múltiples registrosUpdateAsync(T entity)- Actualizar un registroUpdateRangeAsync(IEnumerable<T> entities)- Actualizar múltiples registrosDeleteAsync<TKey>(TKey id)- Eliminar por ID (soporta múltiples tipos)DeleteAsync(T entity)- Eliminar entidadDeleteRangeAsync(Expression<Func<T, bool>> predicate)- Eliminar múltiples registros con expresión
Operaciones por Lotes (Bulk Operations)
BulkInsertAsync(IEnumerable<T> entities)- Inserción masiva optimizadaBulkUpdateAsync(IEnumerable<T> entities)- Actualización masiva optimizadaBulkDeleteAsync(IEnumerable<T> entities)- Eliminación masiva optimizada
Operaciones de Conteo y Existencia
CountAsync(Expression<Func<T, bool>>? predicate = null)- Contar registrosExistsAsync(Expression<Func<T, bool>> predicate)- Verificar existencia con expresiónExistsAsync<TKey>(TKey id)- Verificar existencia por IDAnyAsync(Expression<Func<T, bool>>? predicate = null)- Verificar si existen registros
SQL Personalizado
QueryAsync(string sql, object? parameters = null)- Consulta personalizadaQueryFirstOrDefaultAsync(string sql, object? parameters = null)- Primera o nuloExecuteAsync(string sql, object? parameters = null)- Ejecutar comandoExecuteScalarAsync<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 | Versions 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. |
-
net10.0
- Microsoft.Data.Sqlite (>= 10.0.1)
- ORMData (>= 10.1.2)
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.
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.