Как стать автором
Обновить
564.32
OTUS
Цифровые навыки от ведущих экспертов

Реверсивный Proxy в C#

Время на прочтение7 мин
Количество просмотров3.8K

Привет, Хабр!

Сегодня расскажу о паттерне «Реверсивный Proxy» на примере онлайн‑магазина кормов для котиков. Суть паттерна проста: вместо обычного прокси, который просто передаёт вызовы, мы можем динамически менять логику выполнения методов. Под катом — теория, примеры и объяснение кода.

Теоретические основы паттерна «Реверсивный Proxy»

В классическом варианте Proxy объект‑заместитель принимает вызовы и делегирует их реальному объекту. Такой подход удобен для кэширования, логирования или контроля доступа. Но он статичен — логику менять на лету нельзя. Проблема: при обновлении функционала приходится останавливать сервер или вносить изменения во все компоненты системы.

Реверсивный Proxy перехватывает вызовы и позволяет модифицировать их до передачи реальному объекту. Это может быть:

  • Добавление проверки параметров (например, замена товара в заказе).

  • Внедрение нового алгоритма обработки.

  • Откат к резервной реализации при ошибках.

Главное — всё происходит динамически, без остановки работы системы.

Для реализации динамики используются:

  • Expression Trees: позволяют формировать и компилировать выражения на ходу.

  • Reflection.Emit: даёт контроль над генерируемым IL‑кодом.

Пример: магазин кормов для котиков

Предположим, есть интерфейс сервиса, обрабатывающего заказы:

public interface ICatFoodService
{
    void ProcessOrder(string orderId, string catFoodType);
}

Стандартная реализация:

public class CatFoodService : ICatFoodService
{
    public void ProcessOrder(string orderId, string catFoodType)
    {
        Console.WriteLine($"[CatFoodService] Заказ {orderId} на корм '{catFoodType}' обработан.");
    }
}

Теперь добавим реверсивный прокси, который, например, заменяет заказ «DeluxeCatFood» на «PremiumCatFood».

Реализация с Expression Trees и DispatchProxy

Используем DispatchProxy для перехвата вызовов. Добавим динамическую проверку и логирование:

using System;
using System.Linq.Expressions;
using System.Reflection;

public static class ReverseProxy
{
    public static T Create<T>(T instance) where T : class
    {
        if (!typeof(T).IsInterface)
            throw new ArgumentException("T должен быть интерфейсом");

        return DispatchProxyGenerator.CreateProxy<T>(instance, (method, args) =>
        {
            // Логирование вызова
            Console.WriteLine($"[ReverseProxy] Метод: {method.Name}, параметры: {string.Join(", ", args)}");

            // Если заказ на корм "DeluxeCatFood" - меняем на "PremiumCatFood"
            if (method.Name == nameof(ICatFoodService.ProcessOrder) &&
                args.Length >= 2 && args[1] is string catFoodType &&
                catFoodType.Equals("DeluxeCatFood", StringComparison.OrdinalIgnoreCase))
            {
                Console.WriteLine("[ReverseProxy] Замена товара: DeluxeCatFood -> PremiumCatFood");
                args[1] = "PremiumCatFood";
            }
        });
    }
}

public class DispatchProxyGenerator : DispatchProxy
{
    private object _target;
    private Action<MethodInfo, object[]> _interceptor;

    public static T CreateProxy<T>(T target, Action<MethodInfo, object[]> interceptor) where T : class
    {
        object proxy = Create<T, DispatchProxyGenerator>();
        ((DispatchProxyGenerator)proxy)._target = target;
        ((DispatchProxyGenerator)proxy)._interceptor = interceptor;
        return proxy as T;
    }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        _interceptor?.Invoke(targetMethod, args);
        return targetMethod.Invoke(_target, args);
    }
}

Метод Create: проверяет, что тип — интерфейс, и вызываем генератор прокси. Interceptor: перед вызовом реального метода выполняется функция‑перехватчик, где можно изменить параметры (например, заменить тип корма) или добавить логирование. DispatchProxyGenerator наследник DispatchProxy, в котором переопределён метод Invoke, позволяющий вставить перехват вызова.

Реализация с Reflection.Emit

Для тех, кто хочет максимальной гибкости, предлагаю пример с Reflection.Emit:

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class ReverseProxyEmit
{
    public static T Create<T>(T instance) where T : class
    {
        if (!typeof(T).IsInterface)
            throw new ArgumentException("T должен быть интерфейсом");

        var assemblyName = new AssemblyName("DynamicProxyAssembly");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
        var typeBuilder = moduleBuilder.DefineType(typeof(T).Name + "Proxy",
            TypeAttributes.Public, typeof(object), new[] { typeof(T) });

        // Поле для хранения реального объекта
        var targetField = typeBuilder.DefineField("_target", typeof(T), FieldAttributes.Private);

        // Создание конструктора
        var ctor = typeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard, new[] { typeof(T) });
        var ilCtor = ctor.GetILGenerator();
        ilCtor.Emit(OpCodes.Ldarg_0);
        ilCtor.Emit(OpCodes.Call, typeof(object).GetConstructor(Type.EmptyTypes));
        ilCtor.Emit(OpCodes.Ldarg_0);
        ilCtor.Emit(OpCodes.Ldarg_1);
        ilCtor.Emit(OpCodes.Stfld, targetField);
        ilCtor.Emit(OpCodes.Ret);

        // Создание метода для каждого метода интерфейса
        foreach (var method in typeof(T).GetMethods())
        {
            var parameters = method.GetParameters();
            var paramTypes = Array.ConvertAll(parameters, p => p.ParameterType);
            var methodBuilder = typeBuilder.DefineMethod(
                method.Name,
                MethodAttributes.Public | MethodAttributes.Virtual,
                method.ReturnType,
                paramTypes);

            var il = methodBuilder.GetILGenerator();

            // Логирование вызова
            il.Emit(OpCodes.Ldstr, $"[ReverseProxyEmit] Вызов метода: {method.Name}");
            il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", new[] { typeof(string) }));

            // Пример: проверка и замена параметра, если метод ProcessOrder и второй параметр равен "DeluxeCatFood"
            if (method.Name == nameof(ICatFoodService.ProcessOrder) && paramTypes.Length >= 2)
            {
                il.Emit(OpCodes.Ldarg_2); // второй аргумент
                il.Emit(OpCodes.Ldstr, "DeluxeCatFood");
                il.Emit(OpCodes.Call, typeof(string).GetMethod("Equals", new[] { typeof(string) }));
                Label labelContinue = il.DefineLabel();
                il.Emit(OpCodes.Brfalse_S, labelContinue);

                // Если равно, то заменяем на "PremiumCatFood"
                il.Emit(OpCodes.Ldarg_0);
                il.Emit(OpCodes.Ldstr, "PremiumCatFood");
                il.Emit(OpCodes.Starg_S, (byte)2);
                il.MarkLabel(labelContinue);
            }

            // Вызов реального метода
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, targetField);
            for (int i = 0; i < paramTypes.Length; i++)
            {
                il.Emit(OpCodes.Ldarg, i + 1);
            }
            il.Emit(OpCodes.Callvirt, method);
            il.Emit(OpCodes.Ret);
            typeBuilder.DefineMethodOverride(methodBuilder, method);
        }

        var proxyType = typeBuilder.CreateType();
        return Activator.CreateInstance(proxyType, instance) as T;
    }
}

Создаём сборку и модуль, определяем новый тип, реализующий интерфейс, далее инициализируем поле _target для хранения реального объекта.

Для каждого метода генерируем IL‑код:

  1. Выводим строку логирования.

  2. Если метод равен ProcessOrder, проверяем второй аргумент и, если он равен «DeluxeCatFood», заменяем его на «PremiumCatFood».

  3. Загружаем аргументы, вызываем реальный метод и возвращаем результат.

Дополнительные примеры кейсы

Иногда может потребоваться переключаться между двумя реализациями. Приведем пример прокси, который переключается на резервную логику при возникновении исключения:

public class ResilientProxy<T> : DispatchProxy
{
    private T _primary;
    private T _fallback;

    public void Configure(T primary, T fallback)
    {
        _primary = primary;
        _fallback = fallback;
    }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        try
        {
            Console.WriteLine($"[ResilientProxy] Вызов {targetMethod.Name} с основной логикой.");
            return targetMethod.Invoke(_primary, args);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"[ResilientProxy] Ошибка: {ex.Message}. Переключение на резервную логику.");
            return targetMethod.Invoke(_fallback, args);
        }
    }
}

Применение:

// Основная реализация
public class CatFoodServicePrimary : ICatFoodService
{
    public void ProcessOrder(string orderId, string catFoodType)
    {
        // Симулируем ошибку для демонстрации
        if (catFoodType == "FaultyCatFood")
            throw new Exception("Ошибка в логике заказа");
        Console.WriteLine($"[Primary] Заказ {orderId} на '{catFoodType}' обработан.");
    }
}

// Резервная реализация
public class CatFoodServiceFallback : ICatFoodService
{
    public void ProcessOrder(string orderId, string catFoodType)
    {
        Console.WriteLine($"[Fallback] Заказ {orderId} на '{catFoodType}' обработан резервной логикой.");
    }
}

// Конфигурация прокси
var primaryService = new CatFoodServicePrimary();
var fallbackService = new CatFoodServiceFallback();
var proxy = DispatchProxy.Create<ICatFoodService, ResilientProxy<ICatFoodService>>();
((ResilientProxy<ICatFoodService>)proxy).Configure(primaryService, fallbackService);

// Тестируем: вызовется резервная логика, если основная падает
proxy.ProcessOrder("123", "FaultyCatFood");

Можно создать прокси, который динамически корректирует входные данные в зависимости от бизнес‑правил:

public static class OrderCorrectionProxy
{
    public static ICatFoodService Create(ICatFoodService instance)
    {
        return DispatchProxy.Create<ICatFoodService, OrderCorrectionProxyImpl>()
            .Setup(instance);
    }
}

public class OrderCorrectionProxyImpl : DispatchProxy
{
    private ICatFoodService _target;

    public OrderCorrectionProxyImpl Setup(ICatFoodService target)
    {
        _target = target;
        return this;
    }

    protected override object Invoke(MethodInfo targetMethod, object[] args)
    {
        // Если второй параметр (тип корма) содержит ошибку в написании, исправляем
        if (targetMethod.Name == nameof(ICatFoodService.ProcessOrder) &&
            args.Length >= 2 && args[1] is string type)
        {
            if (type.Contains("Delux"))
            {
                Console.WriteLine("[OrderCorrection] Исправление типа корма с 'Delux' на 'DeluxeCatFood'");
                args[1] = "DeluxeCatFood";
            }
        }
        return targetMethod.Invoke(_target, args);
    }
}

Применение:

var realService = new CatFoodService();
var correctedService = OrderCorrectionProxy.Create(realService);
correctedService.ProcessOrder("456", "DeluxCatFood"); // Выведет исправление и затем вызов метода

А вы применяли этот паттерн? Пишите в комментариях. Удачи и до новых встреч!

P.S. Рекомендую обратить внимание на открытые уроки, которые скоро пройдут в Otus:

  • 5 марта: «Clean code и связь с архитектурными паттернами в C#». Подробнее

  • 18 марта: «Создание высоконагруженных систем на C#: инструменты и техники». Подробнее

Теги:
Хабы:
Всего голосов 10: ↑4 и ↓6-1
Комментарии13

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS