I am using Linq-to-SQL with Unity in a Repository pattern. I am trying to add an in开发者_开发知识库terceptor for object security on the repository method [Securable]IQueryable<TEntity> List<TEntity>()
that intercepts the call and returns only the entities that the user has rights to.
public class SecurableAttribute : HandlerAttribute
{...}
public class SecurableHandler : ICallHandler
{
...
IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
var message = getNext()(input, getNext);
var returnType = message.ReturnValue.GetType();
if (typeof(IQueryable).IsAssignableFrom(returnType))
{
var entityType = returnType.GetGenericArguments().Single();
var securableAttribute = entityType.GetAttribute<SecurableTypeAttribute>();
if(securableAttribute != null)
{
//Build expression to filter the list from the attribute and primary key of the entity
//Return the new IQueryable
}
}
return message;
}
}
I have built an expression, but I can't do message.ReturnValue.Where(expression)
since the message.ReturnValue
is object
(message.ReturnValue
is actually a System.Data.Linq.Table<TEntity>
, but I don't want to be too tied to L2S), and it is at runtime so I can't cast it back to a generic and replace message.ReturnValue
.
Alternatively, I tried
public interface ISecurable<TKey>
{
TKey SecurityId { get; }
}
on the entity, which locks me in a bit, but I am OK with that if I could separate the remaining security aspects. This allows me to do the following in IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
where I build the expression above:
if(typeof(ISecurableType).IsAssignableFrom(entityType))
{
var secured = ((IQueryable<ISecurable>)message.ReturnValue).Where(expression);
//Need to return secured as IQueryably<TEntity>
}
I now have to cast secured
to IQueryable<ISecurable>
but typeof(IQueryable<TEntity>).IsAssignableFrom(secured.GetType())
returns false, and swapping out the return value throws an exception, but it does seem to work with delayed execution as far as I can tell. (Also, I don't know TEntity at design time in SecurableHandler
, but I do know the reflected type - but I have tried using the class declaration that I know it is in testing.)
Is there any way to modify the return results somehow? I am stuck needing to return a generic that I don't know at design time, thus making that impossible, but I also can't modify the expression (((IQueryable)message.ReturnType).Expression
is declared as Expression Expression { get; }
).
Is there any brilliance out there that could point me in a way that works?
tl;dr Need to return an IQueryable<TEntity>
at runtime from an object
that is a Table<TEntity> : IQueryable<TEntity>
with an additional .Where(expression)
.
You can try creating a dynamic expression at runtime. You shouldn't have to explicitly cast the IQueryable back to it's generic type, as long as you don't change the element types with a "Select".
Example:
public class SecurityHandler : ICallHandler
{
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
var message = getNext()(input, getNext);
var returnType = message.ReturnValue.GetType();
if (typeof(IQueryable).IsAssignableFrom(returnType))
{
var entityType = returnType.GetGenericArguments().Single();
var securableAttribute = entityType.GetAttribute<SecurableTypeAttribute>();
if (securableAttribute != null)
{
//Build expression to filter the list from the attribute and primary key of the entity
//Return the new IQueryable
message.ReturnValue = AddWhereExpression(
(IQueryable)message.ReturnValue,
securableAttribute.FilterValues,
securableAttribute.FilterPropertyName);
}
}
return message;
}
public int Order { get; set; }
private static IQueryable AddWhereExpression(IQueryable query, IEnumerable ids, string filterPropertyName)
{
// Build this expression:
// item => ids.Contains(item.[PrimaryKeyPropertyName])
var itemParameter = Expression.Parameter(query.ElementType, "item");
var itemParameterProperty = Expression.Property(itemParameter, filterPropertyName);
var listParameter = Expression.Constant(ids);
var containsExpression = Expression.Call(
typeof(System.Linq.Enumerable),
"Contains",
new[] { typeof(int) },
listParameter,
itemParameterProperty);
var delegateTypeExpression = Expression.GetFuncType(new[] { query.ElementType, typeof(bool) });
var whereExpression = Expression.Lambda(
delegateTypeExpression,
containsExpression,
new[] { itemParameter }
);
Expression callWhere = Expression.Call(
typeof(Queryable),
"Where",
new Type[] { query.ElementType }, // type args for Where<T>()
query.Expression,
whereExpression
);
return query.Provider.CreateQuery(callWhere);
}
}
I am assuming your attribute will provide some array of allowable values.
Here are some extension methods that will help with this process:
public static class TypeExtensions
{
public static TAttribute GetAttribute<TAttribute>(this Type type)
{
var attributes = type.GetCustomAttributes(typeof(TAttribute), true);
if (attributes.Length == 0) return default(TAttribute);
return (TAttribute)attributes[0];
}
public static PropertyInfo GetPropertyWithAttributeValue<TAttribute>(
this IEnumerable<PropertyInfo> properties,
Func<TAttribute, bool> findPredicate)
where TAttribute : Attribute
{
var property = from p in properties
where p.HasAttribute<TAttribute>() &&
findPredicate.Invoke(p.GetAttribute<TAttribute>())
select p;
return property.FirstOrDefault();
}
public static bool HasAttribute<TAttribute>(this PropertyInfo propertyInfo)
{
return propertyInfo.GetCustomAttributes(typeof(TAttribute), true).Any();
}
public static TAttribute GetAttribute<TAttribute>(this PropertyInfo propertyInfo)
{
var attributes = propertyInfo.GetCustomAttributes(typeof(TAttribute), true);
if (attributes.Length == 0) return default(TAttribute);
return (TAttribute)attributes[0];
}
}
I haven't tried running this myself, but hopefully it's enough to get you started.
精彩评论