通過建立動態型別 動態構建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
,預期返回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