330 lines
12 KiB
C#
330 lines
12 KiB
C#
using Dapper;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using MySqlConnector;
|
|
using System.ComponentModel.DataAnnotations.Schema;
|
|
using System.Data;
|
|
using System.Globalization;
|
|
using System.Linq.Expressions;
|
|
using System.Reflection;
|
|
using System.Runtime.Intrinsics.Arm;
|
|
using System.Text;
|
|
|
|
namespace TWASys_App.Dapper.AExtentions
|
|
{
|
|
public class DataValue
|
|
{
|
|
public object? Data { set; get; }
|
|
|
|
public string Name { set; get; } = "";
|
|
|
|
public string PKName { set; get; } = "";
|
|
}
|
|
|
|
public class BatchInsert
|
|
{
|
|
private IDbConnection _connect;
|
|
private IDbTransaction _transaction;
|
|
private Dictionary<string, DataValue> _dataKey;
|
|
private List<DataValue> _list;
|
|
private StringBuilder queryBatch = new();
|
|
public int IdTable = 100;
|
|
public BatchInsert(IDbConnection connect, IDbTransaction transaction)
|
|
{
|
|
_connect = connect;
|
|
_transaction = transaction;
|
|
_dataKey = new Dictionary<string, DataValue>();
|
|
_list = new List<DataValue>();
|
|
}
|
|
|
|
public void AddRow<T>(T obj)
|
|
{
|
|
var dt = new DataValue
|
|
{
|
|
Name = "",
|
|
PKName = "",
|
|
Data = obj
|
|
};
|
|
_list.Add(dt);
|
|
|
|
var p = DapperCommand.QueryInsert(obj, _dataKey, IdTable);
|
|
IdTable++;
|
|
dt.Name = p.Name;
|
|
dt.PKName = p.KeyVar;
|
|
if (p.IsKeyAI)
|
|
{
|
|
_dataKey.TryAdd(p.KeyVar, dt);
|
|
}
|
|
queryBatch.AppendLine(p.Query);
|
|
}
|
|
|
|
public string GetQuery()
|
|
{
|
|
string query = string.Empty;
|
|
foreach (var (key, value) in _dataKey)
|
|
{
|
|
query += $"Select @tb{value.Name}_{key} As {key}; \n";
|
|
}
|
|
queryBatch.AppendLine(query);
|
|
return queryBatch.ToString();
|
|
}
|
|
|
|
public async Task ExcuteQuery()
|
|
{
|
|
|
|
var dynamic = ToDynamicParametersSeqPrefix(_list);
|
|
var str = GetQuery();
|
|
Console.WriteLine(str);
|
|
using var grid = await _connect.QueryMultipleAsync(str, dynamic, _transaction);
|
|
await MapAutoIncrementIdsAsync(grid, _dataKey.Values);
|
|
|
|
}
|
|
|
|
public static async Task MapAutoIncrementIdsAsync(SqlMapper.GridReader grid,IEnumerable<DataValue> items)
|
|
{
|
|
foreach (var dv in items)
|
|
{
|
|
var obj = dv?.Data;
|
|
if (obj is null) continue;
|
|
|
|
var (pk, isAuto) = FindPrimaryKey(obj.GetType());
|
|
if (pk is null || !isAuto) continue; // không phải auto-inc → không đọc scalar
|
|
|
|
// nếu object đã có ID (không rỗng) thì bỏ qua để không lệch thứ tự result set
|
|
var cur = pk.GetValue(obj);
|
|
if (!IsEmptyId(cur)) continue;
|
|
|
|
// đọc scalar từ SELECT kế tiếp và gán lại cho property khóa
|
|
var raw = await grid.ReadSingleAsync<object>();
|
|
var targetType = Nullable.GetUnderlyingType(pk.PropertyType) ?? pk.PropertyType;
|
|
|
|
if (raw is IDictionary<string, object> row)
|
|
{
|
|
if (!row.TryGetValue(dv?.PKName, out raw))
|
|
throw new InvalidOperationException($"Missing column {dv?.PKName}");
|
|
}
|
|
|
|
// MySqlDecimal → decimal
|
|
if (raw?.GetType().FullName == "MySql.Data.Types.MySqlDecimal")
|
|
{
|
|
var t = raw.GetType();
|
|
raw = (decimal)t.GetProperty("Value")!.GetValue(raw)!;
|
|
}
|
|
|
|
// Giờ o phải là số: long/ulong/int/decimal/…
|
|
var target = Nullable.GetUnderlyingType(pk.PropertyType) ?? pk.PropertyType;
|
|
pk.SetValue(obj, ConvertNumericTo(target, raw));
|
|
|
|
}
|
|
}
|
|
static object ConvertNumericTo(Type target, object? x)
|
|
{
|
|
// ưu tiên xử lý 2 nhánh chính
|
|
if (target == typeof(ulong) || target == typeof(uint) || target == typeof(ushort) || target == typeof(byte))
|
|
{
|
|
ulong u = x switch
|
|
{
|
|
ulong v => v,
|
|
long v => v < 0 ? throw new OverflowException() : (ulong)v,
|
|
int v => v < 0 ? throw new OverflowException() : (ulong)v,
|
|
decimal v => v < 0 ? throw new OverflowException() : checked((ulong)v),
|
|
_ => Convert.ToUInt64(x, System.Globalization.CultureInfo.InvariantCulture)
|
|
};
|
|
return target == typeof(ulong) ? u :
|
|
target == typeof(uint) ? checked((uint)u) :
|
|
target == typeof(ushort) ? checked((ushort)u) :
|
|
checked((byte)u);
|
|
}
|
|
else
|
|
{
|
|
long s = x switch
|
|
{
|
|
long v => v,
|
|
ulong v => v > (ulong)long.MaxValue ? throw new OverflowException() : (long)v,
|
|
int v => v,
|
|
decimal v => checked((long)v),
|
|
_ => Convert.ToInt64(x, System.Globalization.CultureInfo.InvariantCulture)
|
|
};
|
|
return target == typeof(long) ? s :
|
|
target == typeof(int) ? checked((int)s) :
|
|
target == typeof(short) ? checked((short)s) :
|
|
checked((sbyte)s);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
public static DynamicParameters ToDynamicParametersSeqPrefix(
|
|
IEnumerable<DataValue> inputs // 0 coi là "trống" (trừ property tên Status)
|
|
)
|
|
{
|
|
var dp = new DynamicParameters();
|
|
var seen = new HashSet<string>(StringComparer.Ordinal);
|
|
|
|
foreach (var src in inputs)
|
|
{
|
|
var t = src.Data?.GetType();
|
|
|
|
var props = t?.GetProperties(BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
foreach (var p in props)
|
|
{
|
|
if (!p.CanRead) continue;
|
|
if (IsNotMapped(p)) continue;
|
|
if (IsKeyOrForeignKey(p, src.Data))
|
|
continue;
|
|
|
|
|
|
var paramName = "tb" + src.Name + "_" + p.Name;
|
|
var val = p.GetValue(src.Data);
|
|
dp.Add("@" + paramName, val);
|
|
}
|
|
}
|
|
return dp;
|
|
}
|
|
private static (PropertyInfo? Key, bool IsAutoInc) FindPrimaryKey(Type t)
|
|
{
|
|
PropertyInfo? key = null; bool auto = false;
|
|
|
|
foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
|
|
{
|
|
if (HasAttr(p, "System.ComponentModel.DataAnnotations.KeyAttribute", "KeyAttribute",
|
|
"Dapper.Contrib.Extensions.KeyAttribute"))
|
|
{ key = p; auto = IsDatabaseGeneratedIdentity(p); break; }
|
|
|
|
if (HasAttr(p, "Dapper.Contrib.Extensions.ExplicitKeyAttribute", "ExplicitKeyAttribute"))
|
|
{ key = p; auto = false; break; }
|
|
}
|
|
|
|
// fallback theo tên "Id" nếu không có attribute
|
|
key ??= t.GetProperty("Id", BindingFlags.Public | BindingFlags.Instance);
|
|
|
|
// nếu không tìm được attribute, đoán auto-inc cho kiểu số
|
|
if (key != null && !auto)
|
|
{
|
|
var k = Nullable.GetUnderlyingType(key.PropertyType) ?? key.PropertyType;
|
|
auto = k == typeof(int) || k == typeof(long) || k == typeof(uint) || k == typeof(ulong);
|
|
}
|
|
return (key, auto);
|
|
}
|
|
|
|
private static bool IsDatabaseGeneratedIdentity(PropertyInfo p)
|
|
{
|
|
// [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
foreach (var ca in p.CustomAttributes)
|
|
{
|
|
var full = ca.AttributeType.FullName ?? ca.AttributeType.Name;
|
|
if (full is "System.ComponentModel.DataAnnotations.Schema.DatabaseGeneratedAttribute"
|
|
or "DatabaseGeneratedAttribute")
|
|
{
|
|
if (ca.ConstructorArguments.Count == 1 && ca.ConstructorArguments[0].ArgumentType.IsEnum)
|
|
return Convert.ToInt32(ca.ConstructorArguments[0].Value) == 1; // Identity = 1
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
private static bool IsNotMapped(PropertyInfo p)
|
|
{
|
|
return HasAttr(p, "System.ComponentModel.DataAnnotations.Schema.NotMappedAttribute", "NotMappedAttribute");
|
|
}
|
|
private static bool IsKey(PropertyInfo p, object? obj)
|
|
{
|
|
if( HasAttr(p,
|
|
"System.ComponentModel.DataAnnotations.KeyAttribute", "KeyAttribute",
|
|
"Dapper.Contrib.Extensions.KeyAttribute"))
|
|
{
|
|
var val = p.GetValue(obj);
|
|
ulong t = 0;
|
|
if (val == null && string.IsNullOrWhiteSpace(val?.ToString()))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if (ulong.TryParse(val?.ToString(), out t) && t == 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
private static bool IsKeyOrForeignKey(PropertyInfo p, object? obj)
|
|
{
|
|
return IsKey(p, obj) || IsForeignKeyProperty(p, obj);
|
|
}
|
|
|
|
private static bool HasAttr(MemberInfo m, params string[] names)
|
|
{
|
|
return m.CustomAttributes.Any(ca =>
|
|
{
|
|
var full = ca.AttributeType.FullName ?? ca.AttributeType.Name;
|
|
var shortName = ca.AttributeType.Name;
|
|
return names.Contains(full) || names.Contains(shortName);
|
|
});
|
|
}
|
|
|
|
private static bool IsForeignKeyProperty(PropertyInfo p, object? obj)
|
|
{
|
|
// [ForeignKey] ngay trên property
|
|
if (HasAttr(p, "System.ComponentModel.DataAnnotations.Schema.ForeignKeyAttribute", "ForeignKeyAttribute"))
|
|
{
|
|
var val = p.GetValue(obj);
|
|
ulong t = 0;
|
|
if (val == null && string.IsNullOrWhiteSpace(val?.ToString()))
|
|
{
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
if (ulong.TryParse(val?.ToString(), out t) && t == 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
//// [ForeignKey("PropName")] đặt trên navigation khác, trỏ tới property này
|
|
//var my = p.Name;
|
|
//foreach (var nav in allProps)
|
|
//{
|
|
// if (ReferenceEquals(nav, p)) continue;
|
|
// var fkAttr = nav.CustomAttributes.FirstOrDefault(ca =>
|
|
// (ca.AttributeType.FullName ?? ca.AttributeType.Name) is
|
|
// "System.ComponentModel.DataAnnotations.Schema.ForeignKeyAttribute" or "ForeignKeyAttribute");
|
|
// if (fkAttr == default) continue;
|
|
|
|
// var nameArg = fkAttr.ConstructorArguments.FirstOrDefault().Value?.ToString();
|
|
// if (string.IsNullOrWhiteSpace(nameArg))
|
|
// nameArg = fkAttr.NamedArguments.FirstOrDefault(a => a.MemberName == "Name").TypedValue.Value?.ToString();
|
|
|
|
// if (string.IsNullOrWhiteSpace(nameArg)) continue;
|
|
|
|
// var names = nameArg.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
|
|
// .Select(s => s.Trim());
|
|
// if (names.Any(n => string.Equals(n, my, StringComparison.OrdinalIgnoreCase)))
|
|
// return true;
|
|
//}
|
|
return false;
|
|
}
|
|
private static bool IsEmptyId(object? v)
|
|
{
|
|
if (v is null) return true;
|
|
var t = Nullable.GetUnderlyingType(v.GetType()) ?? v.GetType();
|
|
if (!t.IsValueType) return false;
|
|
try { return v.Equals(Activator.CreateInstance(t)); } catch { return false; }
|
|
}
|
|
}
|
|
public sealed class PropMeta
|
|
{
|
|
public required PropertyInfo Prop { get; init; }
|
|
public required Func<object, object?> Getter { get; init; }
|
|
public required Type UnderlyingType { get; init; }
|
|
public required bool IsNotMapped { get; init; }
|
|
public required bool IsKeyOrForeignKey { get; init; }
|
|
}
|
|
}
|