項目中常常遇到的一個場景,根據當前登陸用戶權限,僅返回權限內可見的內容。參考了不少開源框架,更多的是在
ViewModel
層面硬編碼實現。這種方式太過繁瑣,每一個須要相應邏輯的地方都要寫一遍。通過研究,筆者提供另一種實現,目前已經應用到項目中。這裏記錄一下,也但願能給須要的人提供一個參考。html
PermissionAttribute.Permissions
保存了被受權的權限列表(假設權限類型是string
)。構造函數要求permissions
不能爲空,你能夠選擇不在Property
上使用此屬性(對全部權限可見),或者傳遞一個空數組(對全部權限隱藏)。express
///<summary> /// 訪問許可屬性 ///</summary> [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property)] public class PermissionAttribute : Attribute { public readonly IEnumerable<string> Permissions; public PermissionAttribute([NotNull] params string[] permissions) { this.Permissions = permissions.Distinct(); } }
Name
屬性的訪問權限受權給三、4
權限,Cities
受權給1
權限,Id
屬性對全部權限隱藏,Code
屬性對全部權限都是可見的。api
///<summary> /// 省份實體 ///</summary> [Table("Province")] public class Province { /// <summary> /// 自增主鍵 /// </summary> [Key, Permission(new string[0])] public int Id { get; set; } /// <summary> /// 省份編碼 /// </summary> [StringLength(10)] public string Code { get; set; } /// <summary> /// 省份名稱 /// </summary> [StringLength(64), Permission("3", "4")] public string Name { get; set; } /// <summary> /// 城市列表 /// </summary> [Permission("1")] public List<object> Cities { get; set; } }
ExpressionExtensions
類提供了根據受權列表IEnumerable<string> permissions
構建表達式的方法,並擴展一個SelectPermissionDynamic
方法把sources
映射爲表達式返回的結果類型——動態構建的類型。數組
public static class ExpressionExtensions { /// <summary> /// 根據權限動態查找 /// </summary> /// <typeparam name="TSource"></typeparam> /// <param name="sources"></param> /// <param name="permissions"></param> /// <returns></returns> public static IQueryable<object> SelectPermissionDynamic<TSource>(this IQueryable<TSource> sources, IEnumerable<string> permissions) { var selector = BuildExpression<TSource>(permissions); return sources.Select(selector); } /// <summary> /// 構建表達式 /// </summary> /// <param name="sources"></param> /// <param name="permissions"></param> /// <returns></returns> public static Expression<Func<TSource, object>> BuildExpression<TSource>(IEnumerable<string> permissions) { Type sourceType = typeof(TSource); Dictionary<string, PropertyInfo> sourceProperties = sourceType.GetProperties(BindingFlags.Public | BindingFlags.Instance).Where(prop => { if (!prop.CanRead) { return false; } var perms = prop.GetCustomAttribute<PermissionAttribute>(); return (perms == null || perms.Permissions.Intersect(permissions).Any()); }).ToDictionary(p => p.Name, p => p); Type dynamicType = LinqRuntimeTypeBuilder.GetDynamicType(sourceProperties.Values); ParameterExpression sourceItem = Expression.Parameter(sourceType, "t"); IEnumerable<MemberBinding> bindings = dynamicType.GetRuntimeProperties().Select(p => Expression.Bind(p, Expression.Property(sourceItem, sourceProperties[p.Name]))).OfType<MemberBinding>(); return Expression.Lambda<Func<TSource, object>>(Expression.MemberInit( Expression.New(dynamicType.GetConstructor(Type.EmptyTypes)), bindings), sourceItem); } }
上述代碼片斷調用了LinqRuntimeTypeBuilder.GetDynamicType
方法構建動態類型,下面給出LinqRuntimeTypeBuilder
的源碼。框架
public static class LinqRuntimeTypeBuilder { private static readonly AssemblyName AssemblyName = new AssemblyName() { Name = "LinqRuntimeTypes4iTheoChan" }; private static readonly ModuleBuilder ModuleBuilder; private static readonly Dictionary<string, Type> BuiltTypes = new Dictionary<string, Type>(); static LinqRuntimeTypeBuilder() { ModuleBuilder = AssemblyBuilder.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess.Run).DefineDynamicModule(AssemblyName.Name); } private static string GetTypeKey(Dictionary<string, Type> fields) { //TODO: optimize the type caching -- if fields are simply reordered, that doesn't mean that they're actually different types, so this needs to be smarter string key = string.Empty; foreach (var field in fields) key += field.Key + ";" + field.Value.Name + ";"; return key; } private const MethodAttributes RuntimeGetSetAttrs = MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig; public static Type BuildDynamicType([NotNull] Dictionary<string, Type> properties) { if (null == properties) throw new ArgumentNullException(nameof(properties)); if (0 == properties.Count) throw new ArgumentOutOfRangeException(nameof(properties), "fields must have at least 1 field definition"); try { // Acquires an exclusive lock on the specified object. Monitor.Enter(BuiltTypes); string className = GetTypeKey(properties); if (BuiltTypes.ContainsKey(className)) return BuiltTypes[className]; TypeBuilder typeBdr = ModuleBuilder.DefineType(className, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable); foreach (var prop in properties) { var propertyBdr = typeBdr.DefineProperty(name: prop.Key, attributes: PropertyAttributes.None, returnType: prop.Value, parameterTypes: null); var fieldBdr = typeBdr.DefineField("itheofield_" + prop.Key, prop.Value, FieldAttributes.Private); MethodBuilder getMethodBdr = typeBdr.DefineMethod("get_" + prop.Key, RuntimeGetSetAttrs, prop.Value, Type.EmptyTypes); ILGenerator getIL = getMethodBdr.GetILGenerator(); getIL.Emit(OpCodes.Ldarg_0); getIL.Emit(OpCodes.Ldfld, fieldBdr); getIL.Emit(OpCodes.Ret); MethodBuilder setMethodBdr = typeBdr.DefineMethod("set_" + prop.Key, RuntimeGetSetAttrs, null, new Type[] { prop.Value }); ILGenerator setIL = setMethodBdr.GetILGenerator(); setIL.Emit(OpCodes.Ldarg_0); setIL.Emit(OpCodes.Ldarg_1); setIL.Emit(OpCodes.Stfld, fieldBdr); setIL.Emit(OpCodes.Ret); propertyBdr.SetGetMethod(getMethodBdr); propertyBdr.SetSetMethod(setMethodBdr); } BuiltTypes[className] = typeBdr.CreateType(); return BuiltTypes[className]; } catch { throw; } finally { Monitor.Exit(BuiltTypes); } } private static string GetTypeKey(IEnumerable<PropertyInfo> fields) { return GetTypeKey(fields.ToDictionary(f => f.Name, f => f.PropertyType)); } public static Type GetDynamicType(IEnumerable<PropertyInfo> fields) { return BuildDynamicType(fields.ToDictionary(f => f.Name, f => f.PropertyType)); } }
在Controller
中增長一個Action
,查詢DBContext.Provinces
,並用上面擴展的SelectPermissionDynamic
方法映射到動態類型返回當前用戶權限範圍內可見的內容。代碼片斷以下:ide
[HttpGet, Route(nameof(Visibility))] public IActionResult Visibility(string id) { var querable = _dbContext.Provinces.SelectPermissionDynamic(id.Split(',')).Take(2); return Json(querable.ToList()); }
測試case
:
訪問/Test/Visibility?id=2,3
,預期返回Code
和Name
屬性;
訪問/Test/Visibility?id=8,9
,預期返回Code
屬性;
以下圖所示,返回符合預期,測試經過!
函數
參考文檔:
[1] https://docs.microsoft.com/zh-cn/dotnet/api/system.reflection.emit.assemblybuilder.definedynamicassembly?view=net-5.0
[2] https://stackoverflow.com/questions/606104/how-to-create-linq-expression-tree-to-select-an-anonymous-type測試