C# 遍历未知对象类型的图并改变某些对象属性
具体要求是,如果用户没有特定权限,则替换某些MVC模型属性中的某些值 该解决方案应该是通用的,适用于任何模型的任何图形,并且也相当有效,因为它将用于屏蔽大型对象列表中的值 这些假设是:C# 遍历未知对象类型的图并改变某些对象属性,c#,asp.net-core,graph-traversal,C#,Asp.net Core,Graph Traversal,具体要求是,如果用户没有特定权限,则替换某些MVC模型属性中的某些值 该解决方案应该是通用的,适用于任何模型的任何图形,并且也相当有效,因为它将用于屏蔽大型对象列表中的值 这些假设是: 该图由未知类型的自定义对象组成(只知道这些对象是C#类) 所有感兴趣的对象都有公共属性,只应检查公共属性(可能使用自定义[SensitiveData]属性作为过滤器,主要是出于性能原因,忽略大多数属性) 这些对象可能有其他子对象,需要为[SensitiveData]属性遍历这些子对象 对象可能具有需要遍历的不同
- 该图由未知类型的自定义对象组成(只知道这些对象是C#类)
- 所有感兴趣的对象都有公共属性,只应检查公共属性(可能使用自定义
属性作为过滤器,主要是出于性能原因,忽略大多数属性)[SensitiveData]
- 这些对象可能有其他子对象,需要为
属性遍历这些子对象[SensitiveData]
- 对象可能具有需要遍历的不同类型集合的属性(IEnumerables、ILists、通用IEnumerables和带有自定义对象的ILists)
- 这些对象还可能包含.NET核心对象的集合,例如
,IEnumerable
,我对此不感兴趣,也不应该遍历这些对象IEnumerable
- 我不想遍历.NET核心对象的属性(不需要检查DateTime的Date属性)
- 我不想遍历.NET核心对象(不需要检查字符串的每个字符,它是IEnumerable)
- 在大多数情况下,它将是一棵树,而不是一个图形。尽管如此,对于具有防止陷入无休止循环的保护的图来说,实现它会更安全
另外,我听说反射SetValue和GetValue方法很慢,我应该更好地将setter和getter缓存为委托,并在再次遇到相同类型时重用它们。我将再次遇到相同的类型,因为它是ASP.NET核心web应用程序。因此,如果我缓存每个感兴趣的setter/getter以备将来重用,就有可能比单纯的反射解决方案获得显著的性能提升。这需要一些努力,但我有一些行之有效的方法:
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
public class SensitiveDataAttribute : Attribute
{
}
public abstract class PocoGraphPropertyWalker
{
private enum TypeKind
{
Primitive,
IterablePrimitive,
Poco,
IterablePoco
}
private class TypeAccessDescriptor
{
public TypeAccessor accessor;
public List<PropertyInfo> primitives;
public List<PropertyInfo> iterables;
public List<PropertyInfo> singles;
}
private static ConcurrentDictionary<Type, TypeAccessDescriptor> _accessorCache =
new ConcurrentDictionary<Type, TypeAccessDescriptor>();
public IEnumerable<object> TraversePocoList(IEnumerable<object> pocos)
{
if (pocos == null)
return null;
foreach (var poco in pocos)
TraversePoco(poco);
return pocos;
}
public object TraversePoco(object poco)
{
var unwound = Traversal(poco, ChildrenSelector).ToList();
foreach(var unw in unwound)
VisitPoco(unw);
return poco;
}
public object VisitPoco(object poco)
{
if (poco == null)
return poco;
var t = poco.GetType();
// the registry ignores types that are not POCOs
var typeDesc = TryGetOrRegisterForType(t);
if (typeDesc == null)
return poco;
// do not attempt to parse Keys and Values as primitives,
// even if they were specified as such
if (IsKeyValuePair(t))
return poco;
foreach (var prop in typeDesc.primitives)
{
var oldValue = typeDesc.accessor[poco, prop.Name];
var newValue = VisitProperty(poco, oldValue, prop);
typeDesc.accessor[poco, prop.Name] = newValue;
}
return poco;
}
protected virtual object VisitProperty(object model,
object currentValue, PropertyInfo prop)
{
return currentValue;
}
private IEnumerable<object> Traversal(
object item,
Func<object, IEnumerable<object>> children)
{
var seen = new HashSet<object>();
var stack = new Stack<object>();
seen.Add(item);
stack.Push(item);
yield return item;
while (stack.Count > 0)
{
object current = stack.Pop();
foreach (object newItem in children(current))
{
// protect against cyclic refs
if (!seen.Contains(newItem))
{
seen.Add(newItem);
stack.Push(newItem);
yield return newItem;
}
}
}
}
private IEnumerable<object> ChildrenSelector(object poco)
{
if (poco == null)
yield break;
var t = poco.GetType();
// the registry ignores types that are not POCOs
var typeDesc = TryGetOrRegisterForType(t);
if (typeDesc == null)
yield break;
// special hack for KeyValuePair - FastMember fails to access its Key and Value
// maybe because it's a struct, not class?
// and now we have prop accessors stored in singles / primitives
// so we extract it manually
if (IsKeyValuePair(t))
{
// reverting to good old slow reflection
var k = t.GetProperty("Key").GetValue(poco, null);
var v = t.GetProperty("Value").GetValue(poco, null);
if (k != null)
{
foreach (var yp in YieldIfPoco(k))
yield return yp;
}
if (v != null)
{
foreach(var yp in YieldIfPoco(v))
yield return yp;
}
yield break;
}
// registration method should have registered correct singles
foreach (var single in typeDesc.singles)
{
yield return typeDesc.accessor[poco, single.Name];
}
// registration method should have registered correct IEnumerables
// to skip strings as enums and primitives as enums
foreach (var iterable in typeDesc.iterables)
{
if (!(typeDesc.accessor[poco, iterable.Name] is IEnumerable iterVals))
continue;
foreach (var iterval in iterVals)
yield return iterval;
}
}
private IEnumerable<object> YieldIfPoco(object v)
{
var myKind = GetKindOfType(v.GetType());
if (myKind == TypeKind.Poco)
{
foreach (var d in YieldDeeper(v))
yield return d;
}
else if (myKind == TypeKind.IterablePoco && v is IEnumerable iterVals)
{
foreach (var i in iterVals)
foreach (var d in YieldDeeper(i))
yield return d;
}
}
private IEnumerable<object> YieldDeeper(object o)
{
yield return o;
// going slightly recursive here - might have IEnumerable<IEnumerable<IEnumerable<POCO>>>...
var chs = Traversal(o, ChildrenSelector);
foreach (var c in chs)
yield return c;
}
private TypeAccessDescriptor TryGetOrRegisterForType(Type t)
{
if (!_accessorCache.TryGetValue(t, out var typeAccessorsDescriptor))
{
// blacklist - cannot process dictionary KeyValues
if (IsBlacklisted(t))
return null;
// check if I myself am a real Poco before registering my properties
var myKind = GetKindOfType(t);
if (myKind != TypeKind.Poco)
return null;
var properties = t.GetProperties(BindingFlags.Public | BindingFlags.Instance);
var accessor = TypeAccessor.Create(t);
var primitiveProps = new List<PropertyInfo>();
var singlePocos = new List<PropertyInfo>();
var iterablePocos = new List<PropertyInfo>();
// now sort all props in subtypes:
// 1) a primitive value or nullable primitive or string
// 2) an iterable with primitives (including strings and nullable primitives)
// 3) a subpoco
// 4) an iterable with subpocos
// for our purposes, 1 and 2 are the same - just properties,
// not needing traversion
// ignoring non-generic IEnumerable - can't know its inner types
// and it is not expected to be used in our POCOs anyway
foreach (var prop in properties)
{
var pt = prop.PropertyType;
var propKind = GetKindOfType(pt);
// 1) and 2)
if (propKind == TypeKind.Primitive || propKind == TypeKind.IterablePrimitive)
primitiveProps.Add(prop);
else
if (propKind == TypeKind.IterablePoco)
iterablePocos.Add(prop); //4)
else
singlePocos.Add(prop); // 3)
}
typeAccessorsDescriptor = new TypeAccessDescriptor {
accessor = accessor,
primitives = primitiveProps,
singles = singlePocos,
iterables = iterablePocos
};
if (!_accessorCache.TryAdd(t, typeAccessorsDescriptor))
{
// if failed add, a parallel process added it, just get it back
if (!_accessorCache.TryGetValue(t, out typeAccessorsDescriptor))
throw new Exception("Failed to get a type descriptor that should exist");
}
}
return typeAccessorsDescriptor;
}
private static TypeKind GetKindOfType(Type type)
{
// 1) a primitive value or nullable primitive or string
// 2) an iterable with primitives (including strings and nullable primitives)
// 3) a subpoco
// 4) an iterable with subpocos
// ignoring non-generic IEnumerable - can't know its inner types
// and it is not expected to be used in our POCOs anyway
// 1)
if (IsSimpleType(type))
return TypeKind.Primitive;
var ienumerableInterfaces = type.GetInterfaces()
.Where(x => x.IsGenericType && x.GetGenericTypeDefinition() ==
typeof(IEnumerable<>)).ToList();
// add itself, if the property is defined as IEnumerable<x>
if (type.IsGenericType && type.GetGenericTypeDefinition() ==
typeof(IEnumerable<>))
ienumerableInterfaces.Add(type);
if (ienumerableInterfaces.Any(x =>
IsSimpleType(x.GenericTypeArguments[0])))
return TypeKind.IterablePrimitive;
if (ienumerableInterfaces.Count() != 0)
// 4) - it was enumerable, but not primitive - maybe POCOs
return TypeKind.IterablePoco;
return TypeKind.Poco;
}
private static bool IsBlacklisted(Type type)
{
return false;
}
public static bool IsKeyValuePair(Type type)
{
return type.IsGenericType &&
type.GetGenericTypeDefinition() == typeof(KeyValuePair<,>);
}
public static bool IsSimpleType(Type type)
{
return
type.IsPrimitive ||
new Type[] {
typeof(string),
typeof(decimal),
typeof(DateTime),
typeof(DateTimeOffset),
typeof(TimeSpan),
typeof(Guid)
}.Contains(type) ||
type.IsEnum ||
Convert.GetTypeCode(type) != TypeCode.Object ||
(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>) && IsSimpleType(type.GetGenericArguments()[0]))
;
}
}
public class ProjectSpecificDataFilter : PocoGraphPropertyWalker
{
const string MASK = "******";
protected override object VisitProperty(object model,
object currentValue, PropertyInfo prop)
{
if (prop.GetCustomAttributes<SensitiveDataAttribute>().FirstOrDefault() == null)
return currentValue;
if (currentValue == null || (currentValue is string &&
string.IsNullOrWhiteSpace((string)currentValue)))
return currentValue;
return MASK;
}
}
[AttributeUsage(AttributeTargets.Property,AllowMultiple=false,Inherited=true)]
公共类敏感数据属性:属性
{
}
公共抽象类PocGraphPropertyWalker
{
私有枚举类型
{
原始的,
伊特雷伯原始,
波科,
伊特拉伯科
}
私有类TypeAccessDescriptor
{
公共类型存取器;
公共列表原语;
公共物品清单;
公开单打名单;
}
私有静态ConcurrentDictionary\u accessorCache=
新的ConcurrentDictionary();
公共IEnumerable TraverseColist(IEnumerable pocos)
{
如果(pocos==null)
返回null;
foreach(poco中的var poco)
TraversePoco(poco);
返回POCO;
}
公共对象遍历EPOCO(对象poco)
{
var unbound=遍历(poco,ChildrenSelector).ToList();
foreach(未缠绕时的var unw)
参观奥林匹克运动会(unw);
返回poco;
}
公共对象访问poco(对象poco)
{
if(poco==null)
返回poco;
var t=poco.GetType();
//注册表忽略不是POCO的类型
var typeDesc=TryGetOrRegisterForType(t);
if(typeDesc==null)
返回poco;
//不要尝试将键和值作为原语进行分析,
//即使它们被指定为
如果(IsKeyValuePair(t))
返回poco;
foreach(typeDesc.primitives中的变量prop)
{
var oldValue=typeDesc.accessor[poco,prop.Name];
var newValue=VisitProperty(poco、oldValue、prop);
typeDesc.accessor[poco,prop.Name]=newValue;
}
返回poco;
}
受保护的虚拟对象访问属性(对象模型,
对象currentValue、PropertyInfo属性)
{
返回当前值;
}
私有IEnumerable遍历(
对象项,
(儿童)
{
var seen=newhashset();
var stack=新堆栈();
见.增加(项目);
堆栈。推送(项目);
收益回报项目;
而(stack.Count>0)
{
当前对象=stack.Pop();
foreach(子对象中的对象newItem(当前))
{
//防止循环引用
如果(!seen.Contains(newItem))
{
参见。添加(新项);
stack.Push(newItem);
收益返回新项目;
}
}
}
}
私有IEnumerable子对象选择器(对象poco)
{
if(poco==null)
屈服断裂;
var t=poco.GetType();
//注册表忽略不是POCO的类型
var typeDesc=TryGetOrRegisterForType(t);
if(typeDesc==null)
屈服断裂;
//KeyValuePair的特殊攻击-FastMember无法访问其密钥和值
//也许是因为它是一个结构,而不是类?
//现在我们有了存储在单体/原语中的道具访问器
//所以我们把它提取出来
enum MyEnum
{
One = 1,
Two = 2
}
class A
{
[SensitiveData]
public string S { get; set; }
public int I { get; set; }
public int? I2 { get; set; }
public MyEnum Enm { get; set; }
public MyEnum? Enm1 { get; set; }
public List<MyEnum> Enm2 { get; set; }
public List<int> IL1 { get; set; }
public int?[] IL2 { get; set; }
public decimal Dc { get; set; }
public decimal? Dc1 { get; set; }
public IEnumerable<decimal> Dc3 { get; set; }
public IEnumerable<decimal?> Dc4 { get; set; }
public IList<decimal> Dc5 { get; set; }
public DateTime D { get; set; }
public DateTime? D2 { get; set; }
public B Child { get; set; }
public B[] Children { get; set; }
public List<B> Children2 { get; set; }
public IEnumerable<B> Children3 { get; set; }
public IDictionary<int, int?> PrimDict { get; set; }
public Dictionary<int, B> PocoDict { get; set; }
public IDictionary<B, int?> PocoKeyDict { get; set; }
public Dictionary<int, IEnumerable<B>> PocoDeepDict { get; set; }
}
class B
{
[SensitiveData]
public string S { get; set; }
public int I { get; set; }
public int? I2 { get; set; }
public DateTime D { get; set; }
public DateTime? D2 { get; set; }
public A Parent { get; set; }
}
class Program
{
static A root;
static void Main(string[] args)
{
root = new A
{
D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy",
Child = new B
{
D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy"
},
Children = new B[] {
new B {
D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy" },
new B {
D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy" },
},
Children2 = new List<B> {
new B {
D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy" },
new B {
D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy" },
},
PrimDict = new Dictionary<int, int?> {
{ 1, 2 },
{ 3, 4 }
},
PocoDict = new Dictionary<int, B> {
{ 1, new B {
D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy" } },
{ 3, new B {
D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy" } }
},
PocoKeyDict = new Dictionary<B, int?> {
{ new B {
D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy" }, 1 },
{ new B {
D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy" }, 3 }
},
PocoDeepDict = new Dictionary<int, IEnumerable<B>>
{
{ 1, new [] { new B {D = DateTime.Now,
D2 = DateTime.Now,
I = 10,
I2 = 20,
S = "stringy" } } }
}
};
// add cyclic ref for test
root.Child.Parent = root;
var f = new VtuaSensitiveDataFilter();
var r = f.TraversePoco(root);
}
}