samedi 10 novembre 2018

C#: Filter method return using Generics and Interception

keywords: Linq, LambdaExpressions, AOP

This article is based on a very specific requirement I found once, I think the result was very nice and deserve the share with the public.

Requirement:

We have several service connectors retrieving data from external sources. Data is then used by business layer.
The problem is we are receiving a large amount of data that we don't need.
We can't filter data on source, the services implementation are out of our reach and don't provide the kind of filter we need.



Solution:
Apply filter on data received after service return and before arrive to BL (business layer).
Data is expressed as List<ofDTOs>
Filter should be Linq Expression.

Declaration should be as Strong Typed as possible.

Pseudo code to express the solution could be like this:

 AddServiceFilter(Service.TargetMethod(), methodResult.Items.Property == "MyValue");  

first parameter: The method to be filtered.
second parameter: the expression to apply on items returning from method

Constraints:

  • Can't apply code changes on existing service implementation.
  • Avoid add explicit data filter after each method call (use generic implementation instead)



Implementation Description:

Use AOP and intercept all method calls.
Allow declaration (by code) of filters to apply by method.
When AOP intercept a target method, then apply the filter in the output of the method.



Implementation Steps:


- Register Interceptors on container.
The application use Dependency Injection with Unity. There is already a UnityContainer in place so we add AOP on it over the target service registrations:

IUnityContainer container;

container.RegisterServiceInterceptor<IServiceInterface, ServiceImplementation>();

public static IUnityContainer RegisterServiceInterceptor<T1, T2>(this IUnityContainer container) 
            where T1 : class
            where T2 : T1
        {
            // create the interceptor for interface T1
            var srvInt = new ServiceInterceptor<T1>();

            // register the interceptor, with named registration
            container.RegisterInstance(srvInt.SrvName, srvInt)

            // register the service (of type T2, implementing T1) to intercept
            // and attach it the previous created interceptor
                     .RegisterType<T1, T2>(new Interceptor<VirtualMethodInterceptor>(),
                         new InterceptionBehavior<ServiceInterceptor<T1>>(srvInt.SrvName))
                     ;
            return container;
        }


Note: use named instance registration when you want to register multiple instances of same type.

- Allow filter configuration, this is just simple a method to allow declaring by code what method to filter and the rule (Linq expression) to apply:

public class MethodFilter
    {
        public Expression TargetMethod { get; internal set; }
        public Delegate Filter { get; internal set; }
    }

private List<MethodFilter> MethodFilters = new List<MethodFilter>();

public void AddFilter<T, TResult>(Expression<Func<T, TResult>> targetMethod, Func<TResult, TResult> filter)
    {
        // keep a list of all method and filters declarations for later use
        MethodFilters.Add(new MethodFilter { TargetMethod = targetMethod, Filter = filter });
    }


- Implement the AOP Interceptor and apply the filter if we are in a target method:

public class ServiceInterceptor<T> : MethodInterceptor where T : class
{ 
    protected Type ServiceType;
    public string ServiceName
    {
        get
        {
            return ServiceType.Name;
        }
    }

    public ServiceInterceptor()
    {
        ServiceType = typeof(T);
    }

    protected override void PostProceed(IMethodInvocation invocation, IMethodReturn result)
    {
        // match by method name
        var methodFilter = MethodFilters.FirstOrDefault(m => GetMethodCallName(m.TargetMethod) == methodName);
        if (methodFilter != null)
        {
            // apply Lambda expression to results
            result.ReturnValue = MethodFilters[0].Filter.DynamicInvoke(result.ReturnValue); 
        }
    }

    string GetMethodCallName(LambdaExpression expression)
    {
        var body = (MethodCallExpression)expression.Body;
        var memberInfo = (MemberInfo)body.Method;
        return memberInfo.Name;
    }
}

How to use it:

AddFilter((DataService m) => m.GetValues(), m => m.Where(s => s.Contains("1")).ToList());

Means: for all calls of method GetValues() in class DataService, apply the Where clause to method result, and return this result filtered by the Where clause.

Aucun commentaire:

Enregistrer un commentaire