前言:
承接上篇只实现了单实体的,不带层级的Orm,本篇文章小编带您实现带层级的Orm,不需要自定义Attribute,不需要sql强依赖,但是仍然写法有一点小限制(为了解决多条数据过滤问题)。
原理:
1. 将需要映射的对象的属性层级,以及type 存放在字典中;
2. for循环查询到的 DataRow ,然后将每一列数据映射到实体上,涉及到层级本质是递归。
Tip: 此扩展方法涉及到大量的反射的运用,如果不太了解,可以先去微软反射文档学习一下。
代码实现:
1.将对象映射成 Dictionary<string, Type>
#region GetEntityTiers
private static Dictionary<string, Type> GetPropertiesWithHierarchy(this Type type, string parentPrefix = "")
{
var properties = new Dictionary<string, Type>();
var props = type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
foreach (var prop in props)
{
string propertyName = string.IsNullOrEmpty(parentPrefix) ? prop.Name : $"{parentPrefix}.{prop.Name}";
properties[propertyName] = prop.PropertyType;
if (IsGenericList(prop.PropertyType))
{
var genericArgument = prop.PropertyType.GetGenericArguments().FirstOrDefault();
if (genericArgument != null && !genericArgument.IsPrimitive && genericArgument != typeof(string))
{
var nestedProperties = genericArgument.GetPropertiesWithHierarchy(propertyName);
foreach (var nestedProperty in nestedProperties)
{
properties[nestedProperty.Key] = nestedProperty.Value;
}
}
}
else if (prop.PropertyType.IsClass && prop.PropertyType != typeof(string))
{
var nestedProperties = prop.PropertyType.GetPropertiesWithHierarchy(propertyName);
foreach (var nestedProperty in nestedProperties)
{
properties[nestedProperty.Key] = nestedProperty.Value;
}
}
}
return properties;
}
private static bool IsGenericList(Type type)
{
return type.IsGenericType && (type.GetGenericTypeDefinition() == typeof(List<>));
}
#endregion
2. 将DataRow映射到实体层级上:
public static T MapToEntity<T>(this DataTable table) where T : new()
{
if (table == null || table.Rows.Count == 0)
return default(T);
//实例化对象
var entity = new T();
//拿到属性带层级的名称以及对应的type.
var properties = typeof(T).GetPropertiesWithHierarchy();
foreach (DataRow row in table.Rows)
{
MapRowToEntity(row, entity, properties);
}
return entity;
}
private static void MapRowToEntity(DataRow row, object entity, Dictionary<string, Type> properties)
{
//分层级,先拿到最外层的
var groupedProperties = properties.GroupBy(p => p.Key.Split('.')[0]);
foreach (var group in groupedProperties)
{
var immediatePropName = group.Key;
var nestedProperties = group.ToDictionary(p => string.Join('.', p.Key.Split('.').Skip(1)), p => p.Value);
var propertyInfo = entity.GetType().GetProperty(immediatePropName);
if (propertyInfo != null)
{
if (IsGenericList(propertyInfo.PropertyType))
{
var list = (IList)propertyInfo.GetValue(entity) ?? (IList)Activator.CreateInstance(propertyInfo.PropertyType)!;
var nestedEntityType = propertyInfo.PropertyType.GetGenericArguments().First();
var keyProperty = nestedEntityType.GetProperty(
nestedProperties.Keys.FirstOrDefault(k => k == nestedEntityType.Name + "Id" || k.EndsWith("Id")));
var keyValue = row[keyProperty.Name];
//判断是否存在该条记录了,这里就是为什么需要查询主键了,包括efcore的include都是必须得返回主键得。
var existingEntity =
list!.Cast<object>().FirstOrDefault(e => keyProperty.GetValue(e).Equals(keyValue));
if (existingEntity == null)
{
var nestedEntity = Activator.CreateInstance(nestedEntityType);
MapRowToEntity(row, nestedEntity, nestedProperties);
list!.Add(nestedEntity);
}
else
{
MapRowToEntity(row, existingEntity, nestedProperties);
}
propertyInfo.SetValue(entity, list);
}
else if (propertyInfo.PropertyType.IsClass && propertyInfo.PropertyType != typeof(string))
{
var nestedEntity = propertyInfo.GetValue(entity) ??
Activator.CreateInstance(propertyInfo.PropertyType);
MapRowToEntity(row, nestedEntity, nestedProperties);
propertyInfo.SetValue(entity, nestedEntity);
}
else
{
MapValueToProperty(row, entity, immediatePropName);
}
}
}
}
private static void MapValueToProperty(DataRow row, object entity, string propertyName)
{
if (row.Table.Columns.Contains(propertyName))
{
var value = row[propertyName];
if (value != DBNull.Value)
{
var propertyInfo = entity.GetType().GetProperty(propertyName);
if (propertyInfo != null)
{
propertyInfo.SetValue(entity, Convert.ChangeType(value, propertyInfo.PropertyType));
}
}
}
}
使用:
string sql =
@"select i.IncidentId,i.IncidenttName, tl.TimeLineId,tl.Detail,ns.NextStepId,ns.Step from Incident i
join TimeLine tl on tl.IncidentId=i.IncidentId
join NextStep ns on ns.IncidentId= i.IncidentId
where i.IncidentId=@IncidentId";
var dataTable = await _wrapper.ExecuteSqlWithAdo(sql, [new("@IncidentId", "2")]);
var mapToEntity = dataTable.MapToEntity<IncidentTest>();
public class IncidentTest
{
public int IncidentId { get; set; }
public string IncidenttName { get; set; }
public List<TimeLineTest> TimeLineTests { get; set; }
public List<NextStepTest> NextStepTests { get; set; }
}
public class TimeLineTest
{
public int TimeLineId { get; set; }
public string Detail { get; set; }
}
public class NextStepTest
{
public int NextStepId { get; set; }
public int Step { get; set; }
}
上面需要注意实体的属性名必须得和sql查询出来的列名一致,大家也可以自定义扩展attribute添加别名。
经测试,上面方法执行效率还行,是一个基础的实现,欢迎对其扩展,有更好的idea可以在评论区留言,互相交流学习。
以上就是文章的全部。
Austin.