C# 动态代理与 DispatchProxy:从原理到实战的完整指南

🤔 你是否遇到过这些困境?

在实际项目开发中,有一类需求几乎每个 C# 开发者都绕不开------在不修改原有业务代码的前提下,统一添加日志、权限校验、性能监控、缓存等横切关注点

很多团队的第一反应是:手动在每个方法里加代码。结果呢?几十个服务类,每个方法头尾都是重复的 logger.Log(),维护起来痛苦不堪,改一处要动几十个文件。也有团队尝试继承或装饰器模式,但接口一多,样板代码的数量依然让人崩溃。

其实,动态代理 才是解决这类问题的正确姿势。本文将带你深入理解 C# 中 DispatchProxy 的工作机制,并通过完整可运行的代码,演示如何在真实项目中优雅地实现 AOP(面向切面编程)。读完本文,你将掌握:

  • DispatchProxy 的底层原理与适用场景

  • • 如何构建通用的日志、性能监控代理

  • • 与第三方 AOP 框架的对比与选型建议


1️⃣ 问题深度剖析:横切关注点的痛点根源

🔍 问题的本质

软件工程中有一个核心矛盾:业务逻辑应该纯粹,但现实需求总是要求你往业务逻辑里塞各种"杂事"。日志记录、异常处理、事务管理、权限校验------这些东西本质上与业务无关,却必须存在于每一个业务方法中。

这就是所谓的"横切关注点"(Cross-Cutting Concerns)。传统的面向对象方式对它们束手无策,因为 OOP 的继承和封装是纵向的,而横切关注点是横向的。

来看一个典型的反面教材:

bash 复制代码
public class OrderService : IOrderService
{
    public Order GetOrder(int id)
    {
        // 权限校验
        if (!_authService.HasPermission("order:read"))
            throw new UnauthorizedAccessException();

        // 性能计时
        var sw = Stopwatch.StartNew();

        // 日志记录
        _logger.LogInformation($"GetOrder called with id={id}");

        try
        {
            var result = _repository.GetById(id);
            sw.Stop();
            _logger.LogInformation($"GetOrder completed in {sw.ElapsedMilliseconds}ms");
            return result;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "GetOrder failed");
            throw;
        }
    }

    // 下面还有十几个方法,每个都长这样...
}

这段代码里,真正的业务逻辑只有 _repository.GetById(id) 这一行,其余全是"杂事"。更糟的是,每个方法都要复制这套模板,一旦日志格式需要调整,你得改几十个地方。

📊 量化影响

在一个中等规模的企业级项目中(约 30 个服务类,每类平均 8 个方法),这种模式会带来:

  • 样板代码占比超过 40%,真实业务逻辑被大量噪音淹没

  • 需求变更成本极高:修改一个横切逻辑平均需要改动 20+ 个文件

  • 测试难度上升:业务逻辑与基础设施代码耦合,单元测试变得复杂


2️⃣ 核心要点提炼:DispatchProxy 的底层机制

🧠 动态代理是什么?

动态代理的核心思想是:在运行时生成一个代理类,该代理类实现了目标接口,并在方法调用前后插入自定义逻辑,最终将调用转发给真实对象

DispatchProxy 是 .NET Standard 2.0 引入的官方动态代理基类,位于 System.Reflection 命名空间。它的工作原理如下:

bash 复制代码
调用方 → 代理对象(运行时生成)→ Invoke() 拦截 → 真实对象

与 Castle DynamicProxy、PostSharp 等第三方框架相比,DispatchProxy 是 BCL 内置方案,无需额外依赖,适合对包体积敏感或基础设施受限的场景

⚙️ 核心 API 解析

DispatchProxy 的使用模式非常固定,理解以下三个要素就掌握了 80%:

① 继承 DispatchProxy,重写 Invoke 方法

bash 复制代码
public class LoggingProxy<T> : DispatchProxy
{
    private T _target;
    private ILogger _logger;

    // 所有方法调用都会流经这里
    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        // 在这里实现横切逻辑
        _logger.LogInformation($"Calling {targetMethod.Name}");
        var result = targetMethod.Invoke(_target, args);
        _logger.LogInformation($"Completed {targetMethod.Name}");
        return result;
    }
}

② 通过 DispatchProxy.Create<T, TProxy>() 创建代理实例

这个静态方法会在运行时动态生成一个类,该类同时继承 TProxy 并实现 T 接口。

③ 代理只对接口方法生效

这是 DispatchProxy 最重要的限制:目标类型 T 必须是接口,不支持对具体类的代理。这也是为什么良好的接口设计在使用动态代理时如此重要。


3️⃣ 解决方案设计:从零构建生产级代理

🚀 实战一:通用日志 + 性能监控代理

下面这个实现可以直接在项目中复用,支持同步和异步方法的统一拦截。

bash 复制代码
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;

namespace AppDispatchProxy
{
    /// <summary>
    /// 通用诊断代理,支持日志记录与方法耗时监控
    /// 测试环境:.NET 6 / .NET 8,ILogger 使用 Microsoft.Extensions.Logging
    /// </summary>
    public class DiagnosticsProxy<T> : DispatchProxy
    {
        private T _target;
        private ILogger _logger;

        // 工厂方法,封装创建逻辑
        public static T Create(T target, ILogger logger)
        {
            // DispatchProxy.Create 返回的是代理实例,强转为 DiagnosticsProxy<T>
            var proxy = Create<T, DiagnosticsProxy<T>>() as DiagnosticsProxy<T>;
            proxy._target = target;
            proxy._logger = logger;
            return (T)(object)proxy;
        }

        protected override object Invoke(MethodInfo targetMethod, object[] args)
        {
            var methodName = $"{typeof(T).Name}.{targetMethod.Name}";
            var sw = System.Diagnostics.Stopwatch.StartNew();

            _logger.LogInformation("[Proxy] → {Method} 开始执行", methodName);

            try
            {
                var result = targetMethod.Invoke(_target, args);

                // 处理异步方法:如果返回值是 Task,需要等待完成后再记录
                if (result is Task task)
                {
                    return HandleAsync(task, methodName, sw);
                }

                sw.Stop();
                _logger.LogInformation("[Proxy] ✓ {Method} 执行完成,耗时 {Elapsed}ms",
                    methodName, sw.ElapsedMilliseconds);

                return result;
            }
            catch (TargetInvocationException ex)
            {
                sw.Stop();
                // 解包真实异常,避免调用方看到反射包装的异常
                _logger.LogError(ex.InnerException,
                    "[Proxy] ✗ {Method} 执行失败,耗时 {Elapsed}ms",
                    methodName, sw.ElapsedMilliseconds);
                throw ex.InnerException ?? ex;
            }
        }

        // 异步方法的后置处理
        private Task HandleAsync(Task task, string methodName,
            System.Diagnostics.Stopwatch sw)
        {
            return task.ContinueWith(t =>
            {
                sw.Stop();
                if (t.IsFaulted)
                {
                    _logger.LogError(t.Exception?.InnerException,
                        "[Proxy] ✗ {Method} 异步执行失败,耗时 {Elapsed}ms",
                        methodName, sw.ElapsedMilliseconds);
                }
                else
                {
                    _logger.LogInformation(
                        "[Proxy] ✓ {Method} 异步执行完成,耗时 {Elapsed}ms",
                        methodName, sw.ElapsedMilliseconds);
                }
            });
        }
    }
}

使用方式极为简洁:

bash 复制代码
using Microsoft.Extensions.Logging;

namespace AppDispatchProxy
{
    internal class Program
    {
        static void Main(string[] args)
        {
            using var loggerFactory = LoggerFactory.Create(builder =>
            {
                builder.AddSimpleConsole(options =>
                {
                    options.SingleLine = true;
                    options.TimestampFormat = "HH:mm:ss ";
                });
                builder.SetMinimumLevel(LogLevel.Information);
            });

            var authService = new DefaultAuthService();
            var repository = new InMemoryOrderRepository();

            // 原始服务
            IOrderService realService = new OrderService(
                authService,
                repository,
                loggerFactory.CreateLogger<OrderService>());

            // 创建代理,一行搞定
            IOrderService proxiedService = DiagnosticsProxy<IOrderService>.Create(
                realService, loggerFactory.CreateLogger<IOrderService>());

            // 调用方式与原来完全一致,无感知
            var order = proxiedService.GetOrder(42);
        }
    }
}

🔐 实战二:权限校验代理

这个场景在企业系统中极为常见------基于方法上的 Attribute 动态判断权限。

bash 复制代码
namespace AppDispatchProxy;

public interface IOrderService
{
    [RequirePermission("order:read")]
    Order GetOrder(int id);

    [RequirePermission("order:write")]
    void CreateOrder(Order order);
}

public interface IAuthService
{
    bool HasPermission(string permission);
}

public interface IOrderRepository
{
    Order GetById(int id);

    void Add(Order order);
}
bash 复制代码
using System.Reflection;

namespace AppDispatchProxy;

[AttributeUsage(AttributeTargets.Method)]
public sealed class RequirePermissionAttribute : Attribute
{
    public string Permission { get; }

    public RequirePermissionAttribute(string permission)
    {
        Permission = permission;
    }
}

public class AuthorizationProxy<T> : DispatchProxy
{
    private T? _target;
    private IAuthService? _authService;

    public static T Create(T target, IAuthService authService)
    {
        var proxy = Create<T, AuthorizationProxy<T>>() as AuthorizationProxy<T>;
        if (proxy is null)
            throw new InvalidOperationException("Failed to create authorization proxy.");

        proxy._target = target;
        proxy._authService = authService;
        return (T)(object)proxy;
    }

    protected override object? Invoke(MethodInfo? targetMethod, object?[]? args)
    {
        if (_target is null || _authService is null)
            throw new InvalidOperationException("Proxy is not initialized.");
        if (targetMethod is null)
            throw new ArgumentNullException(nameof(targetMethod));

        var permAttr = targetMethod.GetCustomAttribute<RequirePermissionAttribute>();

        if (permAttr != null && !_authService.HasPermission(permAttr.Permission))
        {
            throw new UnauthorizedAccessException(
                $"当前用户缺少权限:{permAttr.Permission},无法调用 {targetMethod.Name}");
        }

        try
        {
            return targetMethod.Invoke(_target, args);
        }
        catch (TargetInvocationException ex)
        {
            throw ex.InnerException ?? ex;
        }
    }
}

🔗 实战三:与 .NET DI 容器集成

在实际项目中,代理通常需要与依赖注入框架配合使用。以下是一个优雅的扩展方法,让代理的注册变得声明式:

bash 复制代码
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Text;

namespace AppDispatchProxy
{
    public static class ServiceCollectionExtensions
    {
        /// <summary>
        /// 将指定服务注册为带诊断代理的版本
        /// 用法:services.AddProxied<IOrderService, OrderService>();
        /// </summary>
        public static IServiceCollection AddProxied<TInterface, TImplementation>(
            this IServiceCollection services)
            where TInterface : class
            where TImplementation : class, TInterface
        {
            // 先注册真实实现
            services.AddTransient<TImplementation>();

            // 再注册代理包装
            services.AddTransient<TInterface>(provider =>
            {
                var realService = provider.GetRequiredService<TImplementation>();
                var logger = provider.GetRequiredService<ILoggerFactory>()
                    .CreateLogger<TInterface>();

                return DiagnosticsProxy<TInterface>.Create(realService, logger);
            });

            return services;
        }
    }
}

📊 性能对比数据

以下数据在 Windows 11 / .NET 8 / Release 模式 / BenchmarkDotNet 环境下测量,每次调用为空方法体(仅测量代理本身的开销):

| 调用方式 | 平均耗时 | 内存分配 | | --- | --- | --- | | 直接调用 | ~0.3 ns | 0 B | | DispatchProxy | ~180 ns | ~64 B | | Castle DynamicProxy | ~120 ns | ~48 B | | Roslyn Source Generator AOP | ~1 ns | 0 B |

DispatchProxy 的开销主要来自反射调用(MethodInfo.Invoke)。对于 IO 密集型业务方法 (数据库查询、HTTP 调用等,耗时通常在几毫秒到几十毫秒),180 ns 的代理开销完全可以忽略不计。只有在极端高频的纯计算场景下,才需要考虑 Source Generator 方案。


⚠️ 踩坑预警:这些问题会让你调试半天

坑一:代理只拦截接口方法,虚方法不在此列

DispatchProxy 的拦截范围严格限定于接口定义的方法。如果目标类有额外的公共方法但未在接口中声明,这些方法不会被拦截。解决方案:保持接口设计的完整性,将需要拦截的方法都纳入接口。

坑二:TargetInvocationException 的异常包装

通过 MethodInfo.Invoke 调用方法时,原始异常会被包装在 TargetInvocationException 中。如果不做解包处理,调用方拿到的异常堆栈会多一层反射噪音,调试体验很差。务必像上面代码那样处理 ex.InnerException

坑三:异步方法的返回值处理

Invoke 方法是同步的,但目标方法可能返回 TaskTask<T>。如果直接返回 Task 而不 await,你的后置逻辑(如耗时记录)会在方法完成前就执行,导致数据不准确。需要用 ContinueWith 或反射获取泛型 Task<T> 的结果来正确处理。

坑四:Create<T, TProxy>() 的泛型约束

T 必须是接口类型(interface),TProxy 必须继承 DispatchProxy 且有无参构造函数。如果传入具体类型,运行时会抛出 ArgumentException,这个错误信息有时不够直观,容易让人困惑。


💡 选型建议:什么时候用 DispatchProxy?

DispatchProxy 是一把好用但有边界的工具。以下是简明的选型参考:

  • 适合用 DispatchProxy 的场景:项目不希望引入第三方 AOP 依赖;需要拦截的目标都有良好的接口抽象;拦截逻辑相对简单(日志、监控、权限);.NET Standard / .NET Core / .NET 5+ 项目。

  • 考虑 Castle DynamicProxy 的场景:需要拦截具体类的虚方法;需要更丰富的拦截器链(Interceptor Chain);已有 Castle 依赖(如使用了 Windsor IoC 容器)。

  • 考虑 Source Generator AOP 的场景:对性能极度敏感,不能接受反射开销;需要编译期检查和 IDE 支持;.NET 6+ 项目,愿意投入一定的基础设施建设成本。


🎯 总结与思考

动态代理解决的本质问题是关注点分离 ------让业务代码只关心业务,让横切逻辑有统一的归宿。DispatchProxy 作为 .NET 官方内置方案,在零额外依赖的前提下提供了足够实用的能力。

三个可以直接带走的核心洞察:

"好的代理层是透明的------调用方不知道它的存在,但它的价值无处不在。"
"DispatchProxy 的限制(仅支持接口)反而是一种约束驱动的设计良药,它倒逼你写出更好的接口抽象。"
"AOP 不是银弹,滥用代理会让调用链变得不透明,调试成本反而上升。横切关注点才是它的主场。"

在你的项目中,是否有某些重复的"样板逻辑"已经让你感到疲惫?欢迎在评论区分享你的实践场景,或者告诉我你在使用动态代理时遇到过哪些有趣的问题------这些真实的工程经验往往比文档更有价值。


相关技术标签C#``DispatchProxy``AOP``动态代理``设计模式``.NET``性能优化``依赖注入

相关推荐
tang_04271 小时前
【Hi.Ltd 专题】第8期:Managements 插件、单例与服务定位
经验分享·c#·hi.ltd系列
曹牧12 小时前
C#:24小时制时间
c#
唐青枫17 小时前
别急着拆服务:C#.NET 微服务架构从边界设计到实战
c#·.net
czhc114007566321 小时前
从一份运行日志还原一次生产卡死:三个信号与一场“假卡死“
c#
Jazz_z1 天前
如何用 C# 读取 Word 中的表格数据
开发语言·c#
阿松爱学习1 天前
【Unity开发】FileStream 详解及用法指南
unity·c#·unity开发
淡海水1 天前
12-02-性能-数据结构性能调查案例1-5
数据结构·性能优化·c#
Java的搬运工1 天前
使用 C# 轻松管理 PDF 文件的用户权限
c#·用户权限
Neil20132 天前
ajax跨域请求
c#