在项目开发中经常会遇到这样的场景:查询一个复杂实体,其中一部分字段数据从数据库中直接查出,另一部字段数据从缓存中取出。这里通过AutoMapper和特性,提供一种优雅的编码方法。
这种方法的大概思路是:在成员的特性中配置好[缓存字典的key]、[与缓存字典关联的外键名称]和[缓存字典里目标字段的名称]。然后根据上述参数从缓存里取出需要的数据,最后通过配置AutoMapper的Profile来将数据映射到要查询的list里。
可能这样说会让人有点摸不着头脑,接下来就开始一步一步讲解如何编码。
1.建立一个Attribute并在Property中标记以获取我们需要的参数
/// <summary> /// 使用映射 /// </summary> [AttributeUsage(AttributeTargets.Property)] public class MapperPropertyAttribute: Attribute { /// <summary> /// 缓存的Key /// </summary> public string CacheTableKey { get; set; } /// <summary> /// 与缓存字典关联的外键 /// </summary> public string SearchKey { get; set; } /// <summary> /// 缓存字典里的目标字段 /// </summary> public string SearchName { get; set; } }
有了上面的Attribute,就可以用它来标记需要Mapper的Property了。如:
[MapperProperty(CacheTableKey = CacheKey.SYS_USER,SearchKey ="sysID",SearchName ="RealName")] public string CreatedUserName { set; get; }
2.根据参数从缓存里取出数据
首先建立一个Wrapper来存放从缓存中取出的数据
/// <summary> /// 缓存包装层,用于封装从缓存读取的数据 /// </summary> public class CacheDictInfoWrapper { public CacheDictInfoWrapper() { TempDict = new Dictionary<MapperKey, MapperResult>(); } public Dictionary<MapperKey, MapperResult> TempDict { get; set; } } /// <summary> /// 映射的主键 /// </summary> public class MapperKey { public MapperKey(){} public MapperKey(string keyValue, string fieldName) { KeyValue = keyValue; FieldName = fieldName; } /// <summary> /// 要对比的主键值 /// </summary> public string KeyValue { get; set; } /// <summary> /// 要匹配的字段名 /// </summary> public string FieldName { get; set; } public override bool Equals(object obj) { var entity = obj as MapperKey; return entity.KeyValue.Equals(KeyValue) && entity.FieldName.Equals(FieldName); } public override int GetHashCode() { return $"{KeyValue},{FieldName}".GetHashCode(); } } public class MapperResult { public string Key { get; set; } public string Result { get; set; } }
Wrapper最终会被用于Mapper进我们要查询的对象里。具体是如何操作的稍后再进行讲解。
有了Wrapper类,接下来的工作就是把缓存数据查出来并放入new好的Wrapper。
思路在注释里写得比较详细了,这里就不再赘述。
public class MapperConvert { public static IEnumerable<T> Pure<T>(IEnumerable<T> srclist) { if (srclist == null || srclist.Count() < 1) return srclist; //通过反射获取T的Properties var props = srclist.FirstOrDefault().GetType().GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(p => p.CustomAttributes.Any()).ToList(); if (props.Any()) { Dictionary<string, CacheDictInfoWrapper> totalDict = GetCacheDictInfoWrapperDict(props); if (totalDict.Keys.Count > 0) { foreach (var entity in srclist) { //使用AutoMapper来将CacheDictInfoWrapper里的数据Mapper进entity AutoMapperBootstrap.Mapper.Map<Dictionary<string, CacheDictInfoWrapper>, T>(totalDict, entity); } } } return srclist; } /// <summary> /// 获取CacheDictInfoWrapper /// </summary> /// <param name="props"></param> /// <returns></returns> private static Dictionary<string, CacheDictInfoWrapper> GetCacheDictInfoWrapperDict(List<PropertyInfo> props) { //一个类存在多个字段使用同一缓存字典时直接从这里拿缓存,不用重复从缓存中间件取 Dictionary<string, List<JObject>> historyDict = new Dictionary<string, List<JObject>>(); //Dictionary的key就是缓存字典的Key,用于区分缓存的来源 Dictionary<string, CacheDictInfoWrapper> totalDict = new Dictionary<string, CacheDictInfoWrapper>(); //这一句是我这里获取CacheProvider实例的方法,各位需要根据自己的代码修改 ICacheProvider cacheProvider = CacheContainer.Resolve<ICacheProvider>(); //遍历Properties,只有标记了MapperPropertyAttribute的property才进行缓存操作 foreach (var property in props) { var attr = property.GetCustomAttribute(typeof(MapperPropertyAttribute), true); if (attr != null) { //将Attribute里配置好的参数取出 var mapper = (attr as MapperPropertyAttribute); var key = mapper.CacheTableKey; var keyName = property.Name; var searchKey = mapper.SearchKey; var searchName = mapper.SearchName; try { if (!historyDict.TryGetValue(key, out List<JObject> objList)) { objList = new List<JObject>(); //从CacheProvider取出缓存 dynamic list = cacheProvider.Get(key); if (list != null) { //这里把它转成JObject是因为只有JObject才能通过[xxx]匹配,如果有更好的方法欢迎提出 foreach (var item in list) { objList.Add(JObject.FromObject(item)); } historyDict.Add(key, objList); totalDict.Add(key, new CacheDictInfoWrapper()); } } //从缓存取出的数据不为空时,把数据放入Wrapper if (objList != null && objList.Any()) { var dictWrapper = GenerateDictInfoWrapper(objList, searchKey, searchName, keyName); foreach (var kevValue in dictWrapper.TempDict) { totalDict[key].TempDict.Add(kevValue.Key, kevValue.Value); } } } catch (Exception ex) { logger.Erroe(ex); } } } return totalDict; } /// <summary> /// 根据Jobject组装Wrapper /// </summary> /// <param name="listObj"></param> /// <param name="mapperInfo"></param> /// <returns></returns> private static CacheDictInfoWrapper GenerateDictInfoWrapper(List<JObject> listObj, string searchKey, string searchName, string keyName) { CacheDictInfoWrapper cacheDictInfoWrapper = new CacheDictInfoWrapper(); foreach (var jObject in listObj) { MapperKey mapperKey; MapperResult mapperResult; try { //上面提到的,只有JObject能这样匹配 var key = jObject[searchKey].ToString(); mapperKey = new MapperKey { KeyValue = key, FieldName = keyName }; //这里把结果全部转成String了,AutoMapper会自动根据property的类型映射 mapperResult = new MapperResult { Key = key, Result = jObject[searchName].ToString() }; cacheDictInfoWrapper.TempDict.TryAdd(mapperKey, mapperResult); } catch (Exception ex) { logger.Error(ex); } } return cacheDictInfoWrapper; } }
这样一来,执行Pure<T>()时,就能得到装满缓存数据的Wrapper了。
3.配置AutoMapper的Profile
有了Wrapper,我们需要配置AutoMapper的Profile来让AutoMapper正常工作。对这方面不熟悉的朋友可以百度一下AutoMapper和Profile。
每个需要Mapper的实体类都需要配置一个Profile。
public class CacheProfile : Profile { public CacheProfile() { //创建automapper映射关系 CreateMap<Dictionary<string,CacheDictInfoWrapper>, BaseDto>(MemberList.None) .ForMember(dest => dest.UpdatedUserName, opts => opts.MapFrom((src1, dest1, res1) => { var result = string.Empty;//非String的类型这里需要 = null或defult() MapperResult tr; MapperKey mapperKey = new MapperKey(dest1.UpdatedBy, nameof(dest1.UpdatedUserName)); if (src1.TryGetValue(CacheKey.SYS_USER, out CacheDictInfoWrapper dic)) { if ((!string.IsNullOrEmpty(dest1.UpdatedBy)) && dic.TempDict.TryGetValue(mapperKey, out tr)) { result = tr.Result; } } return result; })) .ForMember(dest => dest.CreatedUserName, opts => opts.MapFrom((src1, dest1, res1) => { var result = string.Empty; MapperResult tr; MapperKey mapperKey = new MapperKey(dest1.UpdatedBy, nameof(dest1.CreatedUserName)); if (src1.TryGetValue(CacheKey.SYS_USER, out CacheDictInfoWrapper dic)) { if ((!string.IsNullOrEmpty(dest1.UpdatedBy)) && dic.TempDict.TryGetValue(mapperKey, out tr)) { result = tr.Result; } } return result; })).IncludeAllDerived(); //添加此方法,用于子类有重复映射时,不会覆盖该映射,导致该映射失效。 } }
配置了Profile,AutoMaperr就可以把查出的缓存mapper进我们想要查询的列表里了。最后我们可以写一个扩展方法来让操作更优雅:
public static void Pure<T>(this IEnumerable<T> srcList) where T: class//BaseDto { MapperConvert.Pure<T>(srcList); }
这样一来,只需要一句 list.Pure(); 就可以优雅的把缓存的数据mapper到查询的列表里了。