Files
MIESYSTEM/MicroORM/Repository.cs
2025-12-26 22:27:20 -06:00

610 lines
17 KiB
C#

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Linq.Expressions;
using System.Reflection;
using Dapper;
using MicroORM.Exceptions;
using MicroORM.Interfaces;
namespace MicroORM;
public class Repository<T>: IRepository<T>
{
private readonly Func<Task<IDbConnection>> _connectionFactory;
private IRepository<T> _repositoryImplementation;
public Repository(Func<Task<IDbConnection>> connectionFactory)
{
_connectionFactory = connectionFactory;
DapperTypeMapRegistrar.RegisterFor(typeof(T));
}
static Repository()
{
DapperTypeMapRegistrar.RegisterFor(typeof(T));
}
public async Task<int> SaveAsync<T>(T entity)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
using var conn = await _connectionFactory();
await conn.EnsureOpenAsync();
using var tx = conn.BeginTransaction();
try
{
await ResolveRelationsAsync(entity, conn, tx);
int id;
if (GetId(entity) == 0)
id = await InsertAsync(entity, conn, tx);
else
{
await UpdateAsync(entity, conn, tx);
id = GetId(entity);
}
tx.Commit();
return id;
}
catch
{
tx.Rollback();
throw;
}
}
public async Task<IReadOnlyList<int>> SaveAsyncList<T>(IEnumerable<T> entities)
{
if (entities == null)
throw new ArgumentNullException(nameof(entities));
var list = entities as IList<T> ?? entities.ToList();
if (list.Count == 0)
return Array.Empty<int>();
using var conn = await _connectionFactory();
await conn.EnsureOpenAsync();
using var tx = conn.BeginTransaction();
try
{
var ids = new List<int>(list.Count);
foreach (var entity in list)
{
if (entity == null)
throw new OrmException("La lista contiene una entidad nula");
await ResolveRelationsAsync(entity, conn, tx);
int id;
if (GetId(entity) == 0)
{
id = await InsertAsync(entity, conn, tx);
SetId(entity, id); // MUY IMPORTANTE
}
else
{
await UpdateAsync(entity, conn, tx);
id = GetId(entity);
}
ids.Add(id);
}
tx.Commit();
return ids;
}
catch
{
tx.Rollback();
throw;
}
}
public async Task<int> InsertAsync<T>(T entity, IDbConnection? connection = null, IDbTransaction? transaction = null)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
bool ownsConnection = connection == null;
bool ownsTransaction = transaction == null;
IDbConnection? conn = connection;
IDbTransaction? tx = transaction;
try
{
var table = OrmMetadata.GetTableName<T>();
var key = OrmMetadata.GetKeyProperty<T>();
var props = OrmMetadata.GetScaffoldProperties<T>().Where(p => !OrmMetadata.IsIdentity(p)).ToArray();
if (!props.Any())
throw new OrmException($"No hay columnas insertables para {typeof(T).Name}");
var columns = props.Select(p => OrmMetadata.GetColumnName(p));
var parameters = props.Select(p => "@" + p.Name);
var sql = $@"
INSERT INTO {table}
({string.Join(", ", columns)})
VALUES ({string.Join(", ", parameters)})
RETURNING {OrmMetadata.GetColumnName(key)};
";
if (conn == null)
{
conn = await _connectionFactory();
await conn.EnsureOpenAsync();
}
if (tx == null)
{
tx = conn.BeginTransaction();
}
var id = await conn.ExecuteScalarAsync<int>(
sql,
entity,
tx
);
if (ownsTransaction)
tx.Commit();
return id;
}
catch (Exception ex)
{
if (ownsTransaction)
try { tx?.Rollback(); } catch { /* swallow */ }
throw new OrmException(
$"Error INSERT en {typeof(T).Name}",
ex
);
}
finally
{
if (ownsTransaction)
tx?.Dispose();
if (ownsConnection)
conn?.Dispose();
}
}
public async Task<bool> UpdateAsync<T>(T entity, IDbConnection? connection = null, IDbTransaction? transaction = null)
{
if (entity == null)
throw new ArgumentNullException(nameof(entity));
bool ownsConnection = connection == null;
bool ownsTransaction = transaction == null;
IDbConnection? conn = connection;
IDbTransaction? tx = transaction;
try
{
var table = OrmMetadata.GetTableName<T>();
var key = OrmMetadata.GetKeyProperty<T>();
var keyValue = key.GetValue(entity);
if (keyValue == null)
throw new OrmException($"La entidad {typeof(T).Name} no tiene valor en la clave primaria");
var props = OrmMetadata.GetScaffoldProperties<T>().Where(p => !OrmMetadata.IsIdentity(p)).ToArray();
if (!props.Any())
return false;
var sets = props.Select(p =>
$"{OrmMetadata.GetColumnName(p)} = @{p.Name}");
var sql = $@"
UPDATE {table}
SET {string.Join(", ", sets)}
WHERE {OrmMetadata.GetColumnName(key)} = @{key.Name};
";
if (conn == null)
{
conn = await _connectionFactory();
await conn.EnsureOpenAsync();
}
if (tx == null)
{
tx = conn.BeginTransaction();
}
var affected = await conn.ExecuteAsync(
sql,
entity,
tx
);
if (ownsTransaction)
tx.Commit();
return affected > 0;
}
catch (Exception ex)
{
if (ownsTransaction)
try { tx?.Rollback(); } catch { /* ignore */ }
throw new OrmException(
$"Error UPDATE en {typeof(T).Name}",
ex
);
}
finally
{
if (ownsTransaction)
tx?.Dispose();
if (ownsConnection)
conn?.Dispose();
}
}
public async Task<T?> GetByIdAsync(object id)
{
if (id == null)
throw new ArgumentNullException(nameof(id));
try
{
var table = OrmMetadata.GetTableName<T>();
var key = OrmMetadata.GetKeyProperty<T>();
var sql = $@"
SELECT *
FROM {table}
WHERE {OrmMetadata.GetColumnName(key)} = @Id;
";
using var conn = await _connectionFactory();
await conn.EnsureOpenAsync();
return await conn.QueryFirstOrDefaultAsync<T>(sql, new { Id = id });
}
catch (Exception ex)
{
throw new OrmException(
$"Error SELECT por ID en {typeof(T).Name}",
ex
);
}
}
public async Task<IEnumerable<T>> GetAllAsync(Expression<Func<T, bool>>? filter = null, Expression<Func<T, object>>? orderBy = null, bool descending = false)
{
try
{
var table = OrmMetadata.GetTableName<T>();
var parameters = new DynamicParameters();
var sql = $"SELECT * FROM {table}";
// WHERE dinámico
if (filter != null)
{
var where = ExpressionToSqlTranslator.Translate(filter, parameters);
sql += $" WHERE {where}";
}
// ORDER BY dinámico
if (orderBy != null)
{
var column = ExpressionToSqlTranslator.GetOrderByColumn(orderBy);
sql += $" ORDER BY {column} {(descending ? "DESC" : "ASC")}";
}
sql += ";";
using var conn = await _connectionFactory();
await conn.EnsureOpenAsync();
return await conn.QueryAsync<T>(sql, parameters);
}
catch (Exception ex)
{
throw new OrmException(
$"Error SELECT dinámico en {typeof(T).Name}",
ex
);
}
}
public async Task<bool> DeleteAsync(object id)
{
if (id == null)
throw new ArgumentNullException(nameof(id));
try
{
var table = OrmMetadata.GetTableName<T>();
var key = OrmMetadata.GetKeyProperty<T>();
var sql = $@"
DELETE FROM {table}
WHERE {OrmMetadata.GetColumnName(key)} = @Id;
";
using var conn = await _connectionFactory();
await conn.EnsureOpenAsync();
return await conn.ExecuteAsync(sql, new { Id = id }) > 0;
}
catch (Exception ex)
{
throw new OrmException(
$"Error DELETE en {typeof(T).Name}",
ex
);
}
}
public async Task<T?> QuerySingleAsync<T>(string sql, object? parameters = null)
{
if (string.IsNullOrWhiteSpace(sql))
throw new ArgumentException("SQL inválido", nameof(sql));
try
{
using var conn = await _connectionFactory();
await conn.EnsureOpenAsync();
return await conn.QueryFirstOrDefaultAsync<T>(
sql,
parameters
);
}
catch (Exception ex)
{
throw new OrmException(
$"Error en QuerySingle<{typeof(T).Name}>",
ex
);
}
}
public async Task<IEnumerable<T>> QueryAsync<T>(string sql, object? parameters = null)
{
if (string.IsNullOrWhiteSpace(sql))
throw new ArgumentException("SQL inválido", nameof(sql));
try
{
using var conn = await _connectionFactory();
await conn.EnsureOpenAsync();
return await conn.QueryAsync<T>(sql, parameters);
}
catch (Exception ex)
{
throw new OrmException(
$"Error en Query<{typeof(T).Name}>",
ex
);
}
}
public async Task<T?> ExecuteScalarAsync<T>(string sql, object? parameters = null)
{
if (string.IsNullOrWhiteSpace(sql))
throw new ArgumentException("SQL inválido", nameof(sql));
try
{
using var conn = await _connectionFactory();
await conn.EnsureOpenAsync();
return await conn.ExecuteScalarAsync<T>(
sql,
parameters
);
}
catch (Exception ex)
{
throw new OrmException(
$"Error en ExecuteScalar<{typeof(T).Name}>",
ex
);
}
}
private async Task ResolveRelationsAsync<T>(
T entity,
IDbConnection conn,
IDbTransaction tx)
{
var navProps = typeof(T)
.GetProperties()
.Where(p =>
p.PropertyType.IsClass &&
p.PropertyType != typeof(string) &&
p.GetCustomAttribute<NotMappedAttribute>() == null);
foreach (var nav in navProps)
{
var related = nav.GetValue(entity);
if (related == null)
continue;
var relatedType = nav.PropertyType;
var relatedId = GetId(related);
if (relatedId == 0)
relatedId = await InsertDynamicAsync(related, conn, tx);
else
await UpdateDynamicAsync(related, conn, tx);
SetForeignKey(entity, nav, relatedId);
}
}
private void SetForeignKey<T>(
T entity,
PropertyInfo navProp,
int fkValue)
{
var fkAttr = navProp.GetCustomAttribute<ForeignKeyAttribute>();
if (fkAttr != null)
{
var fkProp = typeof(T).GetProperty(fkAttr.Name);
fkProp?.SetValue(entity, fkValue);
return;
}
var fkName = navProp.Name + "Id";
var prop = typeof(T).GetProperty(fkName);
if (prop == null)
throw new OrmException($"No se pudo resolver FK para {navProp.Name}");
prop.SetValue(entity, fkValue);
}
private async Task<int> InsertDynamicAsync(
object entity,
IDbConnection conn,
IDbTransaction tx)
{
var method = typeof(Repository<>)
.GetMethod(nameof(InsertAsync))!
.MakeGenericMethod(entity.GetType());
return await (Task<int>)method.Invoke(this, new[] { entity, conn, tx })!;
}
private async Task<int> UpdateDynamicAsync(
object entity,
IDbConnection conn,
IDbTransaction tx)
{
var method = typeof(Repository<>)
.GetMethod(nameof(UpdateAsync))!
.MakeGenericMethod(entity.GetType());
return await (Task<int>)method.Invoke(this, new[] { entity, conn, tx })!;
}
private static int GetId(object entity)
{
if (entity == null) throw new ArgumentNullException(nameof(entity));
var type = entity.GetType();
var keyProp = type.GetProperties()
.FirstOrDefault(p => p.GetCustomAttribute<KeyAttribute>() != null);
if (keyProp == null)
{
keyProp = type.GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase)
?? type.GetProperty(type.Name + "Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
}
if (keyProp == null)
throw new OrmException($"No se encontró la propiedad clave (Key/Id/{type.Name}Id) en el tipo {type.FullName}");
var value = keyProp.GetValue(entity);
if (value == null) return 0;
if (value is int i) return i;
if (value is long l)
{
if (l > int.MaxValue || l < int.MinValue)
throw new OrmException($"El valor de la clave primaria ({l}) excede el rango de int para {type.FullName}");
return (int)l;
}
try
{
var converted = Convert.ToInt32(value);
return converted;
}
catch (Exception)
{
if (value is Guid)
throw new OrmException($"La clave primaria de {type.FullName} es un GUID; el flujo actual espera claves numéricas. Ajusta el método si deseas soportar GUIDs.");
throw new OrmException($"No fue posible convertir la clave primaria de {type.FullName} (tipo {value.GetType().Name}) a int.");
}
}
private static void SetId(object entity, int id)
{
if (entity == null) throw new ArgumentNullException(nameof(entity));
var type = entity.GetType();
// Buscar la propiedad clave (igual que en GetId)
var keyProp = type.GetProperties()
.FirstOrDefault(p => p.GetCustomAttribute<KeyAttribute>() != null);
if (keyProp == null)
{
keyProp = type.GetProperty("Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase)
?? type.GetProperty(type.Name + "Id", BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
}
if (keyProp == null)
throw new OrmException($"No se encontró la propiedad clave (Key/Id/{type.Name}Id) en el tipo {type.FullName}");
if (!keyProp.CanWrite)
throw new OrmException($"La propiedad clave {keyProp.Name} en {type.FullName} no tiene setter público.");
var targetType = Nullable.GetUnderlyingType(keyProp.PropertyType) ?? keyProp.PropertyType;
object valueToSet;
if (targetType == typeof(int))
{
valueToSet = id;
}
else if (targetType == typeof(long))
{
valueToSet = (long)id;
}
else if (targetType == typeof(short))
{
valueToSet = (short)id;
}
else if (targetType == typeof(byte))
{
valueToSet = (byte)id;
}
else if (targetType == typeof(Guid))
{
throw new OrmException($"La clave primaria de {type.FullName} es GUID; SetId con int no es aplicable.");
}
else
{
try
{
valueToSet = Convert.ChangeType(id, targetType);
}
catch (Exception ex)
{
throw new OrmException($"No fue posible convertir int a {targetType.Name} para la clave de {type.FullName}.", ex);
}
}
keyProp.SetValue(entity, valueToSet);
}
}