当前位置 博文首页 > Theo·Chan:通过创建动态类型 动态构建Expression Select表达式

    Theo·Chan:通过创建动态类型 动态构建Expression Select表达式

    作者:Theo·Chan 时间:2021-02-01 20:24

    通过创建动态类型 动态构建Expression Select表达式来控制Property可见性

    项目中经常遇到的一个场景,根据当前登录用户权限,仅返回权限内可见的内容。参考了很多开源框架,更多的是在ViewModel层面硬编码实现。这种方式太过繁琐,每个需要相应逻辑的地方都要写一遍。经过研究,笔者提供另外一种实现,目前已经应用到项目中。这里记录一下,也希望能给需要的人提供一个参考。

    1、定义用于Property可见性的属性PermissionAttribute

    PermissionAttribute.Permissions保存了被授权的权限列表(假设权限类型是string)。构造函数要求permissions不能为空,你可以选择不在Property上使用此属性(对所有权限可见),或者传递一个空数组(对所有权限隐藏)。

    ///<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();
        }
    }
    

    2、定义Entity,给个别Property添加PermissionAttribute属性来控制可见性

    Name属性的访问权限授权给3、4权限,Cities授权给1权限,Id属性对所有权限隐藏,Code属性对所有权限都是可见的。

    ///<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; }
    }
    

    3、构建表达式

    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));
        }
    }
    
    

    4、测试调用

    Controller中增加一个Action,查询DBContext.Provinces,并用上面扩展的SelectPermissionDynamic方法映射到动态类型返回当前用户权限范围内可见的内容。代码片段如下:

    [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,预期返回CodeName属性;
    访问/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

    原文:https://www.cnblogs.com/itheo/p/14358495.html

    作者:Theo·Chan
    版权:本文版权归作者和博客园共有
    转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任
    bk
    下一篇:没有了