Nueva mejoras Y estabilidad
This commit is contained in:
46
MicroORM/DapperTypeMapRegistrar.cs
Normal file
46
MicroORM/DapperTypeMapRegistrar.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Reflection;
|
||||
using Dapper;
|
||||
|
||||
namespace MicroORM;
|
||||
|
||||
public static class DapperTypeMapRegistrar
|
||||
{
|
||||
public static void RegisterFor(Type type)
|
||||
{
|
||||
SqlMapper.SetTypeMap(
|
||||
type,
|
||||
new CustomPropertyTypeMap(
|
||||
type,
|
||||
(t, columnName) =>
|
||||
{
|
||||
// [Column]
|
||||
var prop = t.GetProperties()
|
||||
.FirstOrDefault(p =>
|
||||
p.GetCustomAttribute<ColumnAttribute>()?.Name
|
||||
.Equals(columnName, StringComparison.OrdinalIgnoreCase) == true
|
||||
);
|
||||
|
||||
if (prop != null)
|
||||
return prop;
|
||||
|
||||
// Directo
|
||||
prop = t.GetProperty(columnName,
|
||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
|
||||
if (prop != null)
|
||||
return prop;
|
||||
|
||||
// snake_case
|
||||
var pascal = string.Concat(
|
||||
columnName.Split('_')
|
||||
.Select(s => char.ToUpperInvariant(s[0]) + s[1..])
|
||||
);
|
||||
|
||||
return t.GetProperty(pascal,
|
||||
BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase);
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
18
MicroORM/DbConnectionExtensions.cs
Normal file
18
MicroORM/DbConnectionExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
|
||||
namespace MicroORM;
|
||||
|
||||
public static class DbConnectionExtensions
|
||||
{
|
||||
public static async Task EnsureOpenAsync(this IDbConnection connection)
|
||||
{
|
||||
if (connection.State != ConnectionState.Open)
|
||||
{
|
||||
if (connection is DbConnection db)
|
||||
await db.OpenAsync();
|
||||
else
|
||||
connection.Open();
|
||||
}
|
||||
}
|
||||
}
|
||||
7
MicroORM/Exceptions/OrmException.cs
Normal file
7
MicroORM/Exceptions/OrmException.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MicroORM.Exceptions;
|
||||
|
||||
public class OrmException : Exception
|
||||
{
|
||||
public OrmException(string message, Exception? inner = null)
|
||||
: base(message, inner) { }
|
||||
}
|
||||
68
MicroORM/ExpressionToSqlTranslator.cs
Normal file
68
MicroORM/ExpressionToSqlTranslator.cs
Normal file
@@ -0,0 +1,68 @@
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using Dapper;
|
||||
|
||||
namespace MicroORM;
|
||||
|
||||
public static class ExpressionToSqlTranslator
|
||||
{
|
||||
public static string Translate<T>(
|
||||
Expression<Func<T, bool>> expression,
|
||||
DynamicParameters parameters)
|
||||
{
|
||||
return Visit(expression.Body, parameters);
|
||||
}
|
||||
|
||||
private static string Visit(Expression exp, DynamicParameters parameters)
|
||||
{
|
||||
return exp switch
|
||||
{
|
||||
BinaryExpression b => VisitBinary(b, parameters),
|
||||
MemberExpression m => GetColumnName(m),
|
||||
ConstantExpression c => AddParameter(c, parameters),
|
||||
UnaryExpression u => Visit(u.Operand, parameters),
|
||||
_ => throw new NotSupportedException($"Expression no soportada: {exp.NodeType}")
|
||||
};
|
||||
}
|
||||
|
||||
private static string VisitBinary(BinaryExpression b, DynamicParameters parameters)
|
||||
{
|
||||
var left = Visit(b.Left, parameters);
|
||||
var right = Visit(b.Right, parameters);
|
||||
|
||||
var op = b.NodeType switch
|
||||
{
|
||||
ExpressionType.Equal => "=",
|
||||
ExpressionType.NotEqual => "<>",
|
||||
ExpressionType.GreaterThan => ">",
|
||||
ExpressionType.GreaterThanOrEqual => ">=",
|
||||
ExpressionType.LessThan => "<",
|
||||
ExpressionType.LessThanOrEqual => "<=",
|
||||
ExpressionType.AndAlso => "AND",
|
||||
ExpressionType.OrElse => "OR",
|
||||
_ => throw new NotSupportedException($"Operador no soportado: {b.NodeType}")
|
||||
};
|
||||
|
||||
return $"({left} {op} {right})";
|
||||
}
|
||||
|
||||
private static string GetColumnName(MemberExpression m)
|
||||
{
|
||||
var prop = (PropertyInfo)m.Member;
|
||||
return OrmMetadata.GetColumnName(prop);
|
||||
}
|
||||
|
||||
private static string AddParameter(ConstantExpression c, DynamicParameters parameters)
|
||||
{
|
||||
var name = $"@p{parameters.ParameterNames.Count()}";
|
||||
parameters.Add(name, c.Value);
|
||||
return name;
|
||||
}
|
||||
|
||||
public static string GetOrderByColumn<T>(Expression<Func<T, object>> exp)
|
||||
{
|
||||
var body = exp.Body is UnaryExpression u ? u.Operand : exp.Body;
|
||||
var member = (MemberExpression)body;
|
||||
return OrmMetadata.GetColumnName((PropertyInfo)member.Member);
|
||||
}
|
||||
}
|
||||
20
MicroORM/Interfaces/IRepository.cs
Normal file
20
MicroORM/Interfaces/IRepository.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
using System.Data;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace MicroORM.Interfaces;
|
||||
|
||||
public interface IRepository<T>
|
||||
{
|
||||
Task<int> InsertAsync<T>(T entity, IDbConnection? connection = null, IDbTransaction? transaction = null);
|
||||
Task<bool> UpdateAsync<T>(T entity, IDbConnection? connection = null, IDbTransaction? transaction = null);
|
||||
Task<T?> GetByIdAsync(object id);
|
||||
|
||||
Task<IEnumerable<T>> GetAllAsync(Expression<Func<T, bool>>? filter = null,
|
||||
Expression<Func<T, object>>? orderBy = null, bool descending = false);
|
||||
Task<bool> DeleteAsync(object id);
|
||||
Task<int> SaveAsync<T>(T entity);
|
||||
Task<IReadOnlyList<int>> SaveAsyncList<T>(IEnumerable<T> entities);
|
||||
Task<T?> QuerySingleAsync<T>(string sql, object? parameters = null);
|
||||
Task<IEnumerable<T>> QueryAsync<T>(string sql, object? parameters = null);
|
||||
Task<T?> ExecuteScalarAsync<T>(string sql, object? parameters = null);
|
||||
}
|
||||
7
MicroORM/MicroORM.cs
Normal file
7
MicroORM/MicroORM.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace MicroORM;
|
||||
|
||||
public class MicroORM
|
||||
{
|
||||
|
||||
|
||||
}
|
||||
15
MicroORM/MicroORM.csproj
Normal file
15
MicroORM/MicroORM.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
<PackageReference Include="Dapper.Contrib" Version="2.0.78" />
|
||||
<PackageReference Include="Dapper.SqlBuilder" Version="2.1.66" />
|
||||
<PackageReference Include="Npgsql" Version="10.0.1" />
|
||||
<PackageReference Include="Npgsql.Json.NET" Version="10.0.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
63
MicroORM/OrmMetadata.cs
Normal file
63
MicroORM/OrmMetadata.cs
Normal file
@@ -0,0 +1,63 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.ComponentModel.DataAnnotations.Schema;
|
||||
using System.Reflection;
|
||||
|
||||
namespace MicroORM;
|
||||
|
||||
public class OrmMetadata
|
||||
{
|
||||
public static string GetTableName<T>()
|
||||
{
|
||||
var table = typeof(T).GetCustomAttribute<TableAttribute>();
|
||||
if (table == null)
|
||||
throw new InvalidOperationException("La entidad no tiene [Table]");
|
||||
|
||||
return string.IsNullOrEmpty(table.Schema)
|
||||
? table.Name
|
||||
: $"{table.Schema}.{table.Name}";
|
||||
}
|
||||
|
||||
public static PropertyInfo GetKeyProperty<T>()
|
||||
{
|
||||
var key = typeof(T)
|
||||
.GetProperties()
|
||||
.FirstOrDefault(p => p.GetCustomAttribute<KeyAttribute>() != null);
|
||||
|
||||
if (key == null)
|
||||
throw new InvalidOperationException("La entidad no tiene [Key]");
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
public static string GetColumnName(PropertyInfo prop)
|
||||
{
|
||||
var columnAttr = prop.GetCustomAttribute<ColumnAttribute>();
|
||||
return columnAttr?.Name ?? prop.Name;
|
||||
}
|
||||
|
||||
public static bool IsIdentity(PropertyInfo prop)
|
||||
{
|
||||
var key = prop.GetCustomAttribute<KeyAttribute>() != null;
|
||||
var dbGenerated = prop.GetCustomAttribute<DatabaseGeneratedAttribute>();
|
||||
|
||||
return key &&
|
||||
dbGenerated?.DatabaseGeneratedOption == DatabaseGeneratedOption.Identity;
|
||||
}
|
||||
|
||||
public static IEnumerable<PropertyInfo> GetScaffoldProperties<T>()
|
||||
{
|
||||
var type = typeof(T);
|
||||
var key = GetKeyProperty<T>();
|
||||
|
||||
return type.GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p =>
|
||||
p.CanWrite && // tiene setter
|
||||
p.GetCustomAttribute<NotMappedAttribute>() == null &&
|
||||
(
|
||||
p.GetCustomAttribute<ColumnAttribute>() != null ||
|
||||
p == key
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
610
MicroORM/Repository.cs
Normal file
610
MicroORM/Repository.cs
Normal file
@@ -0,0 +1,610 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user