C# 动态代理简介

今天我们来介绍一下C#中的动态代理(Dynamic Proxy)技术。动态代理是一种在运行时创建代理类的技术,它允许我们在不修改原始类代码的情况下,拦截和增强方法调用。这在AOP(面向切面编程)中非常有用,可以用于日志记录、事务管理、权限验证等场景。动态代理的实现通常依赖于反射和IL生成技术。C#中有几个流行的库可以帮助我们实现动态代理,最常用的包括Castle DynamicProxy和DispatchProxy。

Castle DynamicProxy

Castle DynamicProxy是一个功能强大的动态代理库。它允许我们创建接口代理和类代理。下面是一个使用Castle DynamicProxy创建动态代理的简单示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;
using Castle.DynamicProxy;

public class LoggingInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
Console.WriteLine($"Calling method {invocation.Method.Name} with arguments {string.Join(", ", invocation.Arguments)}");
invocation.Proceed();
Console.WriteLine($"Method {invocation.Method.Name} returned {invocation.ReturnValue}");
}
}

public interface ICalculator
{
int Add(int a, int b);
}

public class Calculator : ICalculator
{
public int Add(int a, int b) => a + b;
}

class Program
{
static void Main()
{
ProxyGenerator generator = new ProxyGenerator();
ICalculator calculatorProxy = generator.CreateInterfaceProxyWithTarget<ICalculator>(
new Calculator(),
new LoggingInterceptor());

int result = calculatorProxy.Add(2, 3);
Console.WriteLine($"Result: {result}");
}
}

输出为:
Calling method Add with arguments 2, 3
Method Add returned 5
Result: 5

DispatchProxy

DispatchProxy是.NET Framework 4.6及更高版本中引入的一个内置动态代理机制。它允许我们创建接口的代理类,并通过重写Invoke方法来拦截方法调用。下面是一个使用DispatchProxy创建动态代理的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
using System;
using System.Reflection;

public class LoggingProxy<T> : DispatchProxy
{
private T _decorated;

protected override object Invoke(MethodInfo targetMethod, object[] args)
{
Console.WriteLine($"Calling method {targetMethod.Name} with arguments {string.Join(", ", args)}");
var result = targetMethod.Invoke(_decorated, args);
Console.WriteLine($"Method {targetMethod.Name} returned {result}");
return result;
}

public static T Create(T decorated)
{
object proxy = Create<T, LoggingProxy<T>>();
((LoggingProxy<T>)proxy)._decorated = decorated;
return (T)proxy;
}
}

public interface ICalculator
{
int Add(int a, int b);
}

public class Calculator : ICalculator
{
public int Add(int a, int b) => a + b;
}

class Program
{
static void Main()
{
ICalculator calculator = new Calculator();
ICalculator proxy = LoggingProxy<ICalculator>.Create(calculator);

int result = proxy.Add(2, 3);
Console.WriteLine($"Result: {result}");
}
}

输出为:
Calling method Add with arguments 2, 3
Method Add returned 5
Result: 5

通过以上两个示例,我们可以看到动态代理在C#中的强大功能。无论是使用Castle DynamicProxy还是DispatchProxy,我们都可以轻松地拦截方法调用并添加自定义逻辑,从而实现AOP的各种需求。在实际应用中,选择哪种动态代理技术取决于具体的需求和项目环境。

只有被标记为virtual的方法才能被动态代理拦截。接口的方法默认就是virtual的。具体实现类中的virtual方法也可以被拦截。

动态代理的应用场景

动态代理在许多场景中都非常有用,以下是一些常见框架及其对应的动态代理应用场景:

Moq

Moq是一个流行的.NET模拟库,广泛用于单元测试。它利用动态代理技术创建接口和类的模拟对象,使得测试代码更加简洁和易于维护。通过Moq,我们可以轻松地设置期望值、验证方法调用以及模拟复杂的行为。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using Moq;
using System.Diagnostics;

public class Customer
{
public int Id { get; set; }
}

public interface IDataService
{
Customer GetCustomer(int id);
}

var mock = new Mock<IDataService>();
mock.Setup(x => x.GetCustomer(It.IsAny<int>()))
.Returns((int id) => new Customer { Id = id })
.Callback((int id) => Console.WriteLine($"Called with id: {id}"));

var result = mock.Object.GetCustomer(1);
mock.Verify(s => s.GetCustomer(1), Times.Once);
Debug.Assert(result.Id == 1);
  1. Moq的核心是表达式树解析,将 x => x.Method(It.IsAny<int>()) 转换为匹配规则
  2. 动态代理处理所有方法调用,不需要手动实现每个接口方法
  3. 调用链是惰性计算的,只有在实际调用时才计算结果
  4. 默认值提供机制让 Mock.Of() 等特性成为可能

这就是为什么 Moq 能”自动”实现所有接口方法 - 它实际上没有实现任何方法,而是用一个动态代理拦截所有调用,然后根据 Setup 规则决定如何响应。

Entity Framework Core

Entity Framework Core使用动态代理来实现延迟加载(Lazy Loading)功能。当我们访问导航属性时,EF Core会动态创建一个代理类来拦截对该属性的访问,并在需要时从数据库加载相关数据。这种方式提高了性能,避免了不必要的数据加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using Microsoft.EntityFrameworkCore;

public class Blog
{
public int BlogId { get; set; }
public string Url { get; set; }
public virtual ICollection<Post> Posts { get; set; } // 声明导航属性为 virtual 以支持延迟加载
}

public class Post
{
public int PostId { get; set; }
public string Title { get; set; }
public int BlogId { get; set; }
public virtual Blog Blog { get; set; } // 声明导航属性为 virtual 以支持延迟加载
}

AOP框架

许多AOP框架(如PostSharp、AspectCore等)都使用动态代理来实现方法拦截和增强功能。通过动态代理,这些框架可以在不修改原始代码的情况下,添加日志记录、事务管理、缓存等功能,从而提高代码的可维护性和复用性。

1
2
3
4
5
[Log] // 使用AOP框架的日志特性
public void ProcessData()
{
// 业务逻辑
}

远程调用

动态代理也常用于远程调用场景,如WCF和gRPC等框架。通过动态代理,客户端可以透明地调用远程服务,就像调用本地方法一样。动态代理负责处理网络通信、序列化和反序列化等复杂操作,使得开发者可以专注于业务逻辑的实现。

1
2
3
4
5
// gRPC客户端示例
var channel = GrpcChannel.ForAddress("https://localhost:5001");
var client = new Greeter.GreeterClient(channel);
var reply = await client.SayHelloAsync(new HelloRequest { Name = "World" });
Console.WriteLine("Greeting: " + reply.Message);

装饰器模式

动态代理也可以用于实现装饰器模式,通过动态创建代理类来增强原始对象的功能。例如,我们可以创建一个缓存代理,当调用某个方法时,先检查缓存中是否有结果,如果有则直接返回,否则调用原始方法并将结果存入缓存。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CachingProxy<T> : DispatchProxy
{
private T _decorated;
private Dictionary<string, object> _cache = new Dictionary<string, object>();

protected override object Invoke(MethodInfo targetMethod, object[] args)
{
string cacheKey = $"{targetMethod.Name}:{string.Join(",", args)}";
if (_cache.TryGetValue(cacheKey, out var cachedResult))
{
return cachedResult;
}

var result = targetMethod.Invoke(_decorated, args);
_cache[cacheKey] = result;
return result;
}

public static T Create(T decorated)
{
object proxy = Create<T, CachingProxy<T>>();
((CachingProxy<T>)proxy)._decorated = decorated;
return (T)proxy;
}
}

// 调用示例
ICalculator calculator = new Calculator();
ICalculator proxy = CachingProxy<ICalculator>.Create(calculator);
int result1 = proxy.Add(2, 3); // 计算并缓存结果

总结

动态代理是C#中一种强大的技术,广泛应用于AOP、单元测试和远程调用等场景。通过使用动态代理,我们可以在不修改原始代码的情况下,拦截和增强方法调用,从而提高代码的灵活性和可维护性。无论是使用Castle DynamicProxy还是DispatchProxy,动态代理都为C#开发者提供了强大的工具,帮助他们更好地实现复杂的功能需求。

参考资料