經過建立動態類型 動態構建Expression Select表達式來控制Property可見性

經過建立動態類型 動態構建Expression Select表達式來控制Property可見性

項目中常常遇到的一個場景,根據當前登陸用戶權限,僅返回權限內可見的內容。參考了不少開源框架,更多的是在ViewModel層面硬編碼實現。這種方式太過繁瑣,每一個須要相應邏輯的地方都要寫一遍。通過研究,筆者提供另一種實現,目前已經應用到項目中。這裏記錄一下,也但願能給須要的人提供一個參考。html

一、定義用於Property可見性的屬性PermissionAttribute

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

二、定義Entity,給個別Property添加PermissionAttribute屬性來控制可見性

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,預期返回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.htmlui

做者: Theo·Chan 版權:本文版權歸做者和博客園共有 轉載:歡迎轉載,但未經做者贊成,必須保留此段聲明;必須在文章中給出原文鏈接;不然必究法律責任
相關文章
相關標籤/搜索