update TWA Management v0.0.1

This commit is contained in:
2025-11-05 10:08:06 +07:00
parent d4e91c7960
commit b4b191f829
73 changed files with 2249 additions and 418 deletions

View File

@ -0,0 +1,329 @@
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; }
}
}

View File

@ -1,12 +1,80 @@
using Dapper;
using Newtonsoft.Json.Linq;
using System.Data;
using System.Data.Common;
using System.Net.WebSockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml.Linq;
namespace TWASys_App.Dapper.AExtentions
{
public static class DapperCommand
{
public static async Task Insert<T>(this DbConnection connection, T obj, bool identityInsert = true)
public static dynamic QueryInsert<T>(T obj)
{
return QueryInsert(obj, new Dictionary<string, DataValue>());
}
public static dynamic QueryInsert<T>(T obj, Dictionary<string, DataValue> data, int IdTable = 100)
{
var type = typeof(T);
var tableName = TableMapper.GetTableName(type);
var allProperties = PropertiesCache.TypePropertiesCache(type);
var keyProperties = PropertiesCache.KeyPropertiesCache(type);
var fkProperties = PropertiesCache.FKPropertiesCache(type);
var computedProperties = PropertiesCache.ComputedPropertiesCache(type);
var columns = PropertiesCache.GetColumnNamesCache(type);
var insertProperties = allProperties.Except(computedProperties).Except(fkProperties).ToList();
var insertPropertiesString = string.Empty;
var stringKeyProperty = string.Empty;
var keyAI = string.Empty;
var flag = false;
var q = string.Empty;
if (keyProperties.Count > 0)
{
flag = true;
insertProperties = insertProperties.Except(keyProperties).ToList();
stringKeyProperty = GetColumnsString(keyProperties.ToList(), columns);
keyAI = $@"SET @{GetColumnsString(keyProperties.ToList(), columns, IdTable.ToString(), "")} := LAST_INSERT_ID();";
}
insertPropertiesString = GetColumnsString(insertProperties, columns);
var fkPropertiesString = string.Empty;
var fkValuesString = string.Empty;
if (data.Count > 0)
{
fkPropertiesString = GetColumnsString(fkProperties, columns);
fkValuesString = string.Join(", ", fkProperties.Select(u => {
var ok = data.TryGetValue(u.Name, out var v);
if (ok)
{
return $"@tb{v?.Name}_{u.Name}";
}
else
{
return $"@tb{IdTable}_{u.Name}";
}
}));
fkPropertiesString += (string.IsNullOrEmpty(fkPropertiesString))?"": ", ";
fkValuesString += (string.IsNullOrEmpty(fkValuesString)) ? "" : ", ";
}
q = $@"INSERT INTO {FormatTableName(tableName)} ({fkPropertiesString + insertPropertiesString}) VALUES ({fkValuesString + GetColumnsString(insertProperties, columns, IdTable.ToString(), "@")});
{keyAI}";
return new
{
IsKeyAI = flag,
Query = q,
Name = IdTable.ToString(),
KeyVar = stringKeyProperty
};
}
public static async Task Insert<T>(this DbConnection connection, T obj, IDbTransaction? tx = null)
{
var type = typeof(T);
var tableName = TableMapper.GetTableName(type);
@ -17,13 +85,14 @@ namespace TWASys_App.Dapper.AExtentions
var insertProperties = allProperties.Except(computedProperties).ToList();
var insertPropertiesString = string.Empty;
if (identityInsert)
if (keyProperties.Count > 0)
{
insertProperties = insertProperties.Except(keyProperties).ToList();
insertPropertiesString = GetColumnsString(insertProperties, columns);
var insertedId = await connection.QueryFirstAsync<int>($@"
INSERT INTO {FormatTableName(tableName)} ({insertPropertiesString}) VALUES ({GetColumnsString(insertProperties, columns, "@")});
SELECT LAST_INSERT_ID()", obj);
SELECT LAST_INSERT_ID()", obj, tx);
keyProperties[0].SetValue(obj, insertedId);
}
@ -32,7 +101,7 @@ namespace TWASys_App.Dapper.AExtentions
insertPropertiesString = GetColumnsString(insertProperties, columns);
await connection.QueryFirstAsync($@"
INSERT INTO {FormatTableName(tableName)}({insertPropertiesString})
VALUES ({GetColumnsString(insertProperties, columns, "@")})", obj);
VALUES ({GetColumnsString(insertProperties, columns, "@")})", obj, tx);
}
}
@ -71,17 +140,24 @@ namespace TWASys_App.Dapper.AExtentions
{
return string.Join(prefix, properties.Select(p => $"{columnNames[p.Name]} = @{p.Name}"));
}
private static string GetColumnsString(IEnumerable<PropertyInfo> properties, IReadOnlyDictionary<string, string> columnNames, string tablePrefix = null)
private static string GetColumnsString(IEnumerable<PropertyInfo> properties, IReadOnlyDictionary<string, string> columnNames, string tbName, string tablePrefix = "")
{
if (tbName != string.Empty) tbName = "tb" + tbName + "_";
if (tablePrefix == "target.")
{
return string.Join(", ", properties.Select(property => $"{tablePrefix}{columnNames[property.Name]} as {property.Name}"));
}
else if(tablePrefix == "@")
{
return string.Join(", ", properties.Select(property => $"{tablePrefix}{property.Name}"));
return string.Join(", ", properties.Select(property => $"{tablePrefix}{tbName + property.Name}"));
}
return string.Join(", ", properties.Select(property => $"{tablePrefix}{columnNames[property.Name]}"));
return string.Join(", ", properties.Select(property => $"{tablePrefix}{tbName + columnNames[property.Name].Replace("__", "")}"));
}
private static string GetColumnsString(IEnumerable<PropertyInfo> properties, IReadOnlyDictionary<string, string> columnNames, string tablePrefix = null)
{
return GetColumnsString(properties, columnNames, "", tablePrefix);
}
private static string FormatTableName(string table)
{

View File

@ -1,4 +1,6 @@
using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Reflection;
@ -7,10 +9,12 @@ namespace TWASys_App.Dapper.AExtentions
public class PropertiesCache
{
private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> KeyProperties = new();
private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> FKProperties = new();
private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> TypeProperties = new();
private static readonly ConcurrentDictionary<RuntimeTypeHandle, IEnumerable<PropertyInfo>> ComputedProperties = new();
private static readonly ConcurrentDictionary<RuntimeTypeHandle, IReadOnlyDictionary<string, string>> ColumnNames = new();
public static List<PropertyInfo> TypePropertiesCache(Type type)
{
if (TypeProperties.TryGetValue(type.TypeHandle, out var cachedProps))
@ -60,6 +64,28 @@ namespace TWASys_App.Dapper.AExtentions
return result;
}
public static bool IsPkAutoIncrement(PropertyInfo p)
{
// ===== Fallback by-name (nếu không thể tham chiếu typed)
var attrs = p.GetCustomAttributes(true);
bool hasKeyByName = attrs.Any(a => a.GetType().Name is "KeyAttribute");
var dbgenByName = attrs.FirstOrDefault(a => a.GetType().Name is "DatabaseGeneratedAttribute");
bool isIdentityByName = false;
if (dbgenByName != null)
{
// Tìm property "DatabaseGeneratedOption" (enum) và đọc giá trị "Identity"
var prop = dbgenByName.GetType().GetProperty("DatabaseGeneratedOption");
var val = prop?.GetValue(dbgenByName)?.ToString(); // ví dụ "Identity"
isIdentityByName = string.Equals(val, "Identity", StringComparison.OrdinalIgnoreCase);
}
if (hasKeyByName && isIdentityByName) return true;
return false;
}
public static List<PropertyInfo> KeyPropertiesCache(Type type)
{
if (KeyProperties.TryGetValue(type.TypeHandle, out var cachedProps))
@ -68,19 +94,42 @@ namespace TWASys_App.Dapper.AExtentions
}
var allProperties = TypePropertiesCache(type);
var keyProperties = allProperties.Where(p => p.GetCustomAttributes(true).Any(a => a.GetType().Name == "KeyAttribute")).ToList();
var keyProperties = allProperties.Where(IsPkAutoIncrement).ToList();
KeyProperties[type.TypeHandle] = keyProperties; // ghi cache khi miss
return keyProperties;
}
if (keyProperties.Count == 0)
public static List<PropertyInfo> FKPropertiesCache(Type type)
{
if (FKProperties.TryGetValue(type.TypeHandle, out var cachedProps))
{
var idProp = allProperties.Find(p => string.Equals(p.Name, "id", StringComparison.CurrentCultureIgnoreCase));
if (idProp != null)
return cachedProps.ToList();
}
var allProperties = TypePropertiesCache(type);
var keyProperties = allProperties.Where(HasForeignKeyAttribute).ToList();
FKProperties[type.TypeHandle] = keyProperties;
return keyProperties;
}
static bool HasForeignKeyAttribute(PropertyInfo p)
{
if (p.GetCustomAttribute<ForeignKeyAttribute>() != null) return true;
// FK đặt trên navigation và chỉ ra tên prop FK
var t = p.DeclaringType!;
foreach (var nav in t.GetProperties(BindingFlags.Public | BindingFlags.Instance))
{
// Fallback by-name nếu không tham chiếu DataAnnotations (tùy dự án)
var attr = nav.GetCustomAttributes(true).FirstOrDefault(a => a.GetType().Name == "ForeignKeyAttribute");
if (attr != null)
{
keyProperties.Add(idProp);
var val = attr.GetType().GetProperty("Name")?.GetValue(attr);
if (val == null && string.IsNullOrWhiteSpace(val?.ToString()))
return true;
}
}
KeyProperties[type.TypeHandle] = keyProperties;
return keyProperties;
return false;
}
public static List<PropertyInfo> ComputedPropertiesCache(Type type)
@ -95,7 +144,7 @@ namespace TWASys_App.Dapper.AExtentions
return computedProperties;
}
private static IReadOnlyDictionary<string, string> GetColumnNames(IEnumerable<PropertyInfo> props)
public static IReadOnlyDictionary<string, string> GetColumnNames(IEnumerable<PropertyInfo> props)
{
var ret = new Dictionary<string, string>();
foreach (var prop in props)