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: IRepository { private readonly Func> _connectionFactory; private IRepository _repositoryImplementation; public Repository(Func> connectionFactory) { _connectionFactory = connectionFactory; DapperTypeMapRegistrar.RegisterFor(typeof(T)); } static Repository() { DapperTypeMapRegistrar.RegisterFor(typeof(T)); } public async Task SaveAsync(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> SaveAsyncList(IEnumerable entities) { if (entities == null) throw new ArgumentNullException(nameof(entities)); var list = entities as IList ?? entities.ToList(); if (list.Count == 0) return Array.Empty(); using var conn = await _connectionFactory(); await conn.EnsureOpenAsync(); using var tx = conn.BeginTransaction(); try { var ids = new List(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 InsertAsync(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(); var key = OrmMetadata.GetKeyProperty(); var props = OrmMetadata.GetScaffoldProperties().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( 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 UpdateAsync(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(); var key = OrmMetadata.GetKeyProperty(); 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().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 GetByIdAsync(object id) { if (id == null) throw new ArgumentNullException(nameof(id)); try { var table = OrmMetadata.GetTableName(); var key = OrmMetadata.GetKeyProperty(); var sql = $@" SELECT * FROM {table} WHERE {OrmMetadata.GetColumnName(key)} = @Id; "; using var conn = await _connectionFactory(); await conn.EnsureOpenAsync(); return await conn.QueryFirstOrDefaultAsync(sql, new { Id = id }); } catch (Exception ex) { throw new OrmException( $"Error SELECT por ID en {typeof(T).Name}", ex ); } } public async Task> GetAllAsync(Expression>? filter = null, Expression>? orderBy = null, bool descending = false) { try { var table = OrmMetadata.GetTableName(); 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(sql, parameters); } catch (Exception ex) { throw new OrmException( $"Error SELECT dinámico en {typeof(T).Name}", ex ); } } public async Task DeleteAsync(object id) { if (id == null) throw new ArgumentNullException(nameof(id)); try { var table = OrmMetadata.GetTableName(); var key = OrmMetadata.GetKeyProperty(); 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 QuerySingleAsync(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( sql, parameters ); } catch (Exception ex) { throw new OrmException( $"Error en QuerySingle<{typeof(T).Name}>", ex ); } } public async Task> QueryAsync(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(sql, parameters); } catch (Exception ex) { throw new OrmException( $"Error en Query<{typeof(T).Name}>", ex ); } } public async Task ExecuteScalarAsync(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( sql, parameters ); } catch (Exception ex) { throw new OrmException( $"Error en ExecuteScalar<{typeof(T).Name}>", ex ); } } private async Task ResolveRelationsAsync( T entity, IDbConnection conn, IDbTransaction tx) { var navProps = typeof(T) .GetProperties() .Where(p => p.PropertyType.IsClass && p.PropertyType != typeof(string) && p.GetCustomAttribute() == 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 entity, PropertyInfo navProp, int fkValue) { var fkAttr = navProp.GetCustomAttribute(); 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 InsertDynamicAsync( object entity, IDbConnection conn, IDbTransaction tx) { var method = typeof(Repository<>) .GetMethod(nameof(InsertAsync))! .MakeGenericMethod(entity.GetType()); return await (Task)method.Invoke(this, new[] { entity, conn, tx })!; } private async Task UpdateDynamicAsync( object entity, IDbConnection conn, IDbTransaction tx) { var method = typeof(Repository<>) .GetMethod(nameof(UpdateAsync))! .MakeGenericMethod(entity.GetType()); return await (Task)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() != 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() != 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); } }