Динамически заменять содержимое метода C #?

111

Я хочу изменить способ выполнения метода C # при его вызове, чтобы я мог написать что-то вроде этого:

[Distributed]
public DTask<bool> Solve(int n, DEvent<bool> callback)
{
    for (int m = 2; m < n - 1; m += 1)
        if (m % n == 0)
            return false;
    return true;
}

Во время выполнения мне нужно иметь возможность анализировать методы с атрибутом Distributed (что я уже могу делать), а затем вставлять код до выполнения тела функции и после ее возврата. Что еще более важно, мне нужно иметь возможность делать это, не изменяя код, в котором вызывается Solve, или в начале функции (во время компиляции; цель - сделать это во время выполнения).

На данный момент я попытался использовать этот фрагмент кода (предположим, что t - это тип, в котором хранится Solve, а m - MethodInfo для Solve) :

private void WrapMethod(Type t, MethodInfo m)
{
    // Generate ILasm for delegate.
    byte[] il = typeof(Dpm).GetMethod("ReplacedSolve").GetMethodBody().GetILAsByteArray();

    // Pin the bytes in the garbage collection.
    GCHandle h = GCHandle.Alloc((object)il, GCHandleType.Pinned);
    IntPtr addr = h.AddrOfPinnedObject();
    int size = il.Length;

    // Swap the method.
    MethodRental.SwapMethodBody(t, m.MetadataToken, addr, size, MethodRental.JitImmediate);
}

public DTask<bool> ReplacedSolve(int n, DEvent<bool> callback)
{
    Console.WriteLine("This was executed instead!");
    return true;
}

Однако MethodRental.SwapMethodBody работает только с динамическими модулями; не те, что уже были скомпилированы и сохранены в сборке.

Итак, я ищу способ эффективно использовать SwapMethodBody для метода, который уже хранится в загруженной и выполняющейся сборке. .

Обратите внимание: если мне нужно полностью скопировать метод в динамический модуль, это не проблема, но в этом случае мне нужно найти способ скопировать через IL, а также обновить все вызовы Solve (), чтобы они укажет на новую копию.

Июнь Родос
источник
3
Невозможно поменять местами уже загруженные методы. В противном случае Spring.Net не пришлось бы делать странные вещи с прокси и интерфейсами :-) Прочтите этот вопрос, он касается вашей проблемы: stackoverflow.com/questions/25803/… (если вы можете его перехватить, вы можете что-то вроде -замени ... Если вы не можете 1, то явно не можете 2).
xanatos
В этом случае есть ли способ скопировать метод в динамический модуль и обновить остальную часть сборки, чтобы вызовы этого метода указывали на новую копию?
июнь Родос,
Такой же старый-такой же старый. Если бы это можно было сделать легко, все различные контейнеры IoC, вероятно, сделали бы это. Они этого не делают-> 99% это невозможно :-) (без ужасных и бесчисленных хаков). Есть одна надежда: в C # 5.0 обещали метапрограммирование и асинхронность. Async мы видели ... Ничего не метапрограммировать ... НО это могло быть!
xanatos
1
Вы действительно не объяснили, почему хотите позволить себе что-то настолько болезненное.
DanielOfTaebl
6
Пожалуйста, смотрите мой ответ ниже. Это вполне возможно. В коде, которым вы не владеете, и во время выполнения. Я не понимаю, почему многие думают, что это невозможно.
Андреас Пардейке

Ответы:

206

Раскрытие информации: Harmony - это библиотека, написанная и поддерживаемая мной, автором этой публикации.

Гармония 2 - это библиотека с открытым исходным кодом (лицензия MIT), предназначенная для замены, украшения или изменения существующих методов C # любого типа во время выполнения. Основное внимание уделяется играм и плагинам, написанным на Mono или .NET. Он заботится о нескольких изменениях одного и того же метода - они накапливаются, а не перезаписывают друг друга.

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

Чтобы завершить процесс, он записывает простой переход на ассемблере в трамплин исходного метода, который указывает на ассемблер, созданный при компиляции динамического метода. Это работает для 32/64-битных версий Windows, macOS и любых Linux, поддерживаемых Mono.

Документацию можно найти здесь .

пример

( Источник )

Исходный код

public class SomeGameClass
{
    private bool isRunning;
    private int counter;

    private int DoSomething()
    {
        if (isRunning)
        {
            counter++;
            return counter * 10;
        }
    }
}

Патч с аннотациями Harmony

using SomeGame;
using HarmonyLib;

public class MyPatcher
{
    // make sure DoPatching() is called at start either by
    // the mod loader or by your injector

    public static void DoPatching()
    {
        var harmony = new Harmony("com.example.patch");
        harmony.PatchAll();
    }
}

[HarmonyPatch(typeof(SomeGameClass))]
[HarmonyPatch("DoSomething")]
class Patch01
{
    static FieldRef<SomeGameClass,bool> isRunningRef =
        AccessTools.FieldRefAccess<SomeGameClass, bool>("isRunning");

    static bool Prefix(SomeGameClass __instance, ref int ___counter)
    {
        isRunningRef(__instance) = true;
        if (___counter > 100)
            return false;
        ___counter = 0;
        return true;
    }

    static void Postfix(ref int __result)
    {
        __result *= 2;
    }
}

Как вариант, ручное исправление с отражением

using SomeGame;
using HarmonyLib;

public class MyPatcher
{
    // make sure DoPatching() is called at start either by
    // the mod loader or by your injector

    public static void DoPatching()
    {
        var harmony = new Harmony("com.example.patch");

        var mOriginal = typeof(SomeGameClass).GetMethod("DoSomething", BindingFlags.Instance | BindingFlags.NonPublic);
        var mPrefix = typeof(MyPatcher).GetMethod("MyPrefix", BindingFlags.Static | BindingFlags.Public);
        var mPostfix = typeof(MyPatcher).GetMethod("MyPostfix", BindingFlags.Static | BindingFlags.Public);
        // add null checks here

        harmony.Patch(mOriginal, new HarmonyMethod(mPrefix), new HarmonyMethod(mPostfix));
    }

    public static void MyPrefix()
    {
        // ...
    }

    public static void MyPostfix()
    {
        // ...
    }
}
Андреас Пардейке
источник
Посмотрел исходники, очень интересно! Можете ли вы объяснить (здесь и / или в документации), как работают конкретные инструкции, которые используются для выполнения прыжка (внутрь Memory.WriteJump)?
Том
Для того, чтобы частично ответить на мой собственный комментарий: 48 B8 <QWord>перемещает непосредственное значение QWORD к rax, то FF E0есть jmp rax- все ясно , там! Мой оставшийся вопрос касается E9 <DWord>случая (ближний прыжок): кажется, в этом случае ближний прыжок сохраняется, и модификация относится к цели прыжка; когда Mono вообще генерирует такой код и почему он получает особую обработку?
Том
1
Насколько я могу судить, он еще не поддерживает .NET Core 2, но есть некоторые исключения с AppDomain.CurrentDomain.DefineDynamicAssembly
Max
1
Мой друг 0x0ade упомянул мне, что есть менее зрелая альтернатива, которая работает на .NET Core, а именно MonoMod.RuntimeDetour на NuGet.
Андреас Пардейке
1
Обновление: включив ссылку на System.Reflection.Emit, Harmony теперь компилируется и тестирует нормально с .NET Core 3
Андреас Пардейке
182

Для .NET 4 и выше

using System;
using System.Reflection;
using System.Runtime.CompilerServices;


namespace InjectionTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Target targetInstance = new Target();

            targetInstance.test();

            Injection.install(1);
            Injection.install(2);
            Injection.install(3);
            Injection.install(4);

            targetInstance.test();

            Console.Read();
        }
    }

    public class Target
    {
        public void test()
        {
            targetMethod1();
            Console.WriteLine(targetMethod2());
            targetMethod3("Test");
            targetMethod4();
        }

        private void targetMethod1()
        {
            Console.WriteLine("Target.targetMethod1()");

        }

        private string targetMethod2()
        {
            Console.WriteLine("Target.targetMethod2()");
            return "Not injected 2";
        }

        public void targetMethod3(string text)
        {
            Console.WriteLine("Target.targetMethod3("+text+")");
        }

        private void targetMethod4()
        {
            Console.WriteLine("Target.targetMethod4()");
        }
    }

    public class Injection
    {        
        public static void install(int funcNum)
        {
            MethodInfo methodToReplace = typeof(Target).GetMethod("targetMethod"+ funcNum, BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
            MethodInfo methodToInject = typeof(Injection).GetMethod("injectionMethod"+ funcNum, BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
            RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
            RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle);

            unsafe
            {
                if (IntPtr.Size == 4)
                {
                    int* inj = (int*)methodToInject.MethodHandle.Value.ToPointer() + 2;
                    int* tar = (int*)methodToReplace.MethodHandle.Value.ToPointer() + 2;
#if DEBUG
                    Console.WriteLine("\nVersion x86 Debug\n");

                    byte* injInst = (byte*)*inj;
                    byte* tarInst = (byte*)*tar;

                    int* injSrc = (int*)(injInst + 1);
                    int* tarSrc = (int*)(tarInst + 1);

                    *tarSrc = (((int)injInst + 5) + *injSrc) - ((int)tarInst + 5);
#else
                    Console.WriteLine("\nVersion x86 Release\n");
                    *tar = *inj;
#endif
                }
                else
                {

                    long* inj = (long*)methodToInject.MethodHandle.Value.ToPointer()+1;
                    long* tar = (long*)methodToReplace.MethodHandle.Value.ToPointer()+1;
#if DEBUG
                    Console.WriteLine("\nVersion x64 Debug\n");
                    byte* injInst = (byte*)*inj;
                    byte* tarInst = (byte*)*tar;


                    int* injSrc = (int*)(injInst + 1);
                    int* tarSrc = (int*)(tarInst + 1);

                    *tarSrc = (((int)injInst + 5) + *injSrc) - ((int)tarInst + 5);
#else
                    Console.WriteLine("\nVersion x64 Release\n");
                    *tar = *inj;
#endif
                }
            }
        }

        private void injectionMethod1()
        {
            Console.WriteLine("Injection.injectionMethod1");
        }

        private string injectionMethod2()
        {
            Console.WriteLine("Injection.injectionMethod2");
            return "Injected 2";
        }

        private void injectionMethod3(string text)
        {
            Console.WriteLine("Injection.injectionMethod3 " + text);
        }

        private void injectionMethod4()
        {
            System.Diagnostics.Process.Start("calc");
        }
    }

}
Логман
источник
14
Это заслуживает еще большего количества голосов. У меня совершенно другой сценарий, но этот фрагмент - именно то, что мне нужно, чтобы направить меня в правильном направлении. Спасибо.
SC
2
@Logman отличный ответ. Но у меня вопрос: что происходит в режиме отладки? А можно ли заменить только одну инструкцию? Например, если я хочу заменить условный переход на безусловный? AFAIK вы заменяете скомпилированный метод, поэтому непросто определить, какое условие мы должны заменить ...
Алекс Жуковский
2
@AlexZhukovskiy, если вам нравится, разместите его в стеке и пришлите мне ссылку. Я изучу его и дам вам ответ после выходных. Машина Я также рассмотрю ваш вопрос после выходных.
Logman 05
2
Когда я делал это для интеграционного теста с MSTest, я заметил две вещи: (1) Когда вы используете thisвнутри, injectionMethod*()он будет ссылаться на Injectionэкземпляр во время компиляции , но на Targetэкземпляр во время выполнения (это верно для всех ссылок на члены экземпляра, которые вы используете внутри введенного метод). (2) По какой-то причине эта #DEBUGчасть работала только при отладке теста, но не при запуске теста, который был скомпилирован отладкой. В итоге я всегда использовал эту #elseдеталь. Я не понимаю, почему это работает, но это так.
Good Night Nerd Pride
2
очень хорошо. пора все сломать! @GoodNightNerdPride использование Debugger.IsAttachedвместо #if препроцессора
М.казем Ахгары
25

Вы МОЖЕТЕ изменить содержимое метода во время выполнения. Но вы не должны этого делать, и настоятельно рекомендуется сохранить это в тестовых целях.

Вы только посмотрите:

http://www.codeproject.com/Articles/463508/NET-CLR-Injection-Modify-IL-Code-during-Run-time

В принципе, вы можете:

  1. Получить содержимое метода IL через MethodInfo.GetMethodBody (). GetILAsByteArray ()
  2. Возьми эти байты.

    Если вы просто хотите добавить или добавить какой-то код, просто добавьте коды операций preprend / append, которые вы хотите (однако будьте осторожны, оставляя стек чистым)

    Вот несколько советов по "распаковке" существующего IL:

    • Возвращаемые байты представляют собой последовательность инструкций IL, за которыми следуют их аргументы (если они есть - например, '.call' имеет один аргумент: токен вызываемого метода, а '.pop' не имеет никакого)
    • Соответствие между кодами IL и байтами, которые вы найдете в возвращаемом массиве, можно найти с помощью OpCodes.YourOpCode.Value (который является реальным значением байта кода операции, сохраненным в вашей сборке)
    • Аргументы, добавленные после кодов IL, могут иметь разный размер (от одного до нескольких байтов) в зависимости от вызываемого кода операции.
    • Вы можете найти токены, на которые ссылаются эти аргументы, с помощью соответствующих методов. Например, если ваш IL содержит «.call 354354» (кодируется как 28 00 05 68 32 в шестнадцатеричной системе, 28h = 40 является кодом операции «.call» и 56832h = 354354), соответствующий вызываемый метод можно найти с помощью MethodBase.GetMethodFromHandle (354354 )
  3. После изменения ваш байтовый массив IL можно повторно ввести через InjectionHelper.UpdateILCodes (метод MethodInfo, byte [] ilCodes) - см. Ссылку, указанную выше

    Это «небезопасная» часть ... Работает хорошо, но заключается во взломе внутренних механизмов CLR ...

Оливье
источник
7
Чтобы быть педантичным, 354354 (0x00056832) не является допустимым токеном метаданных, старший байт должен быть 0x06 (MethodDef), 0x0A (MemberRef) или 0x2B (MethodSpec). Кроме того, токен метаданных должен быть записан в порядке байтов с прямым порядком байтов. Наконец, токен метаданных зависит от модуля, и MethodInfo.MetadataToken вернет токен из объявляющего модуля, что сделает его непригодным для использования, если вы хотите вызвать метод, не определенный в том же модуле, что и метод, который вы изменяете.
Брайан Райхл
13

вы можете заменить его, если метод не виртуальный, не общий, не общего типа, не встроен и находится на платформе x86:

MethodInfo methodToReplace = ...
RuntimeHelpers.PrepareMetod(methodToReplace.MethodHandle);

var getDynamicHandle = Delegate.CreateDelegate(Metadata<Func<DynamicMethod, RuntimeMethodHandle>>.Type, Metadata<DynamicMethod>.Type.GetMethod("GetMethodDescriptor", BindingFlags.Instance | BindingFlags.NonPublic)) as Func<DynamicMethod, RuntimeMethodHandle>;

var newMethod = new DynamicMethod(...);
var body = newMethod.GetILGenerator();
body.Emit(...) // do what you want.
body.Emit(OpCodes.jmp, methodToReplace);
body.Emit(OpCodes.ret);

var handle = getDynamicHandle(newMethod);
RuntimeHelpers.PrepareMethod(handle);

*((int*)new IntPtr(((int*)methodToReplace.MethodHandle.Value.ToPointer() + 2)).ToPointer()) = handle.GetFunctionPointer().ToInt32();

//all call on methodToReplace redirect to newMethod and methodToReplace is called in newMethod and you can continue to debug it, enjoy.
Тетер28
источник
Это выглядит безумно опасным. Я очень надеюсь, что никто не использует его в производственном коде.
Брайан Райхл
2
Это используется инструментами мониторинга производительности приложений (APM), а также используется в производстве.
Мартин Керстен
1
Спасибо за ответ. Я работаю над проектом, предлагающим такую ​​возможность, как API-интерфейс ориентированного на аспекты программирования. Я устранил свое ограничение на управление виртуальным и универсальным методами как на x86, так и на x64. Дайте мне знать, если вам понадобятся подробности.
Teter28,
6
Что такое метаданные класса?
Себастьян
Этот ответ является псевдокодом и устарел. Многие методы больше не существуют.
N-ate
9

Существует пара фреймворков, которые позволяют динамически изменять любой метод во время выполнения (они используют интерфейс ICLRProfiling, упомянутый пользователем 152949):

  • Приг : Бесплатно и с открытым исходным кодом!
  • Microsoft Fakes : коммерческий, включен в Visual Studio Premium и Ultimate, но не в Community и Professional
  • Telerik JustMock : коммерческая, доступна "облегченная" версия
  • Изолятор Typemock : Коммерческий

Есть также несколько фреймворков, которые издеваются над внутренними компонентами .NET, они, вероятно, более хрупкие и, вероятно, не могут изменять встроенный код, но с другой стороны они полностью автономны и не требуют использования кастомная пусковая установка.

  • Гармония : лицензия MIT. Кажется, на самом деле успешно использовался в нескольких игровых модах, поддерживает как .NET, так и Mono.
  • Deviare In Process Instrumentation Engine : GPLv3 и коммерческая. Поддержка .NET в настоящее время помечена как экспериментальная, но, с другой стороны, она имеет коммерческую поддержку.
poizan42
источник
8

Решение Логмана , но с интерфейсом для обмена телами методов. Также более простой пример.

using System;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;

namespace DynamicMojo
{
    class Program
    {
        static void Main(string[] args)
        {
            Animal kitty = new HouseCat();
            Animal lion = new Lion();
            var meow = typeof(HouseCat).GetMethod("Meow", BindingFlags.Instance | BindingFlags.NonPublic);
            var roar = typeof(Lion).GetMethod("Roar", BindingFlags.Instance | BindingFlags.NonPublic);

            Console.WriteLine("<==(Normal Run)==>");
            kitty.MakeNoise(); //HouseCat: Meow.
            lion.MakeNoise(); //Lion: Roar!

            Console.WriteLine("<==(Dynamic Mojo!)==>");
            DynamicMojo.SwapMethodBodies(meow, roar);
            kitty.MakeNoise(); //HouseCat: Roar!
            lion.MakeNoise(); //Lion: Meow.

            Console.WriteLine("<==(Normality Restored)==>");
            DynamicMojo.SwapMethodBodies(meow, roar);
            kitty.MakeNoise(); //HouseCat: Meow.
            lion.MakeNoise(); //Lion: Roar!

            Console.Read();
        }
    }

    public abstract class Animal
    {
        public void MakeNoise() => Console.WriteLine($"{this.GetType().Name}: {GetSound()}");

        protected abstract string GetSound();
    }

    public sealed class HouseCat : Animal
    {
        protected override string GetSound() => Meow();

        private string Meow() => "Meow.";
    }

    public sealed class Lion : Animal
    {
        protected override string GetSound() => Roar();

        private string Roar() => "Roar!";
    }

    public static class DynamicMojo
    {
        /// <summary>
        /// Swaps the function pointers for a and b, effectively swapping the method bodies.
        /// </summary>
        /// <exception cref="ArgumentException">
        /// a and b must have same signature
        /// </exception>
        /// <param name="a">Method to swap</param>
        /// <param name="b">Method to swap</param>
        public static void SwapMethodBodies(MethodInfo a, MethodInfo b)
        {
            if (!HasSameSignature(a, b))
            {
                throw new ArgumentException("a and b must have have same signature");
            }

            RuntimeHelpers.PrepareMethod(a.MethodHandle);
            RuntimeHelpers.PrepareMethod(b.MethodHandle);

            unsafe
            {
                if (IntPtr.Size == 4)
                {
                    int* inj = (int*)b.MethodHandle.Value.ToPointer() + 2;
                    int* tar = (int*)a.MethodHandle.Value.ToPointer() + 2;

                    byte* injInst = (byte*)*inj;
                    byte* tarInst = (byte*)*tar;

                    int* injSrc = (int*)(injInst + 1);
                    int* tarSrc = (int*)(tarInst + 1);

                    int tmp = *tarSrc;
                    *tarSrc = (((int)injInst + 5) + *injSrc) - ((int)tarInst + 5);
                    *injSrc = (((int)tarInst + 5) + tmp) - ((int)injInst + 5);
                }
                else
                {
                    throw new NotImplementedException($"{nameof(SwapMethodBodies)} doesn't yet handle IntPtr size of {IntPtr.Size}");
                }
            }
        }

        private static bool HasSameSignature(MethodInfo a, MethodInfo b)
        {
            bool sameParams = !a.GetParameters().Any(x => !b.GetParameters().Any(y => x == y));
            bool sameReturnType = a.ReturnType == b.ReturnType;
            return sameParams && sameReturnType;
        }
    }
}
К. Маккой IV
источник
1
Это дало мне: исключение типа «System.AccessViolationException» произошло в MA.ELCalc.FunctionalTests.dll, но не было обработано в пользовательском коде. Дополнительная информация: Попытка чтения или записи в защищенную память. Это часто указывает на то, что другая память повреждена. ,,, При замене геттера.
N-ate
У меня исключение: «wapMethodBodies еще не обрабатывает IntPtr размером 8»
Фонг Дао
8

Основываясь на ответах на этот и еще один вопрос, ive придумала эту упрощенную версию:

// Note: This method replaces methodToReplace with methodToInject
// Note: methodToInject will still remain pointing to the same location
public static unsafe MethodReplacementState Replace(this MethodInfo methodToReplace, MethodInfo methodToInject)
        {
//#if DEBUG
            RuntimeHelpers.PrepareMethod(methodToReplace.MethodHandle);
            RuntimeHelpers.PrepareMethod(methodToInject.MethodHandle);
//#endif
            MethodReplacementState state;

            IntPtr tar = methodToReplace.MethodHandle.Value;
            if (!methodToReplace.IsVirtual)
                tar += 8;
            else
            {
                var index = (int)(((*(long*)tar) >> 32) & 0xFF);
                var classStart = *(IntPtr*)(methodToReplace.DeclaringType.TypeHandle.Value + (IntPtr.Size == 4 ? 40 : 64));
                tar = classStart + IntPtr.Size * index;
            }
            var inj = methodToInject.MethodHandle.Value + 8;
#if DEBUG
            tar = *(IntPtr*)tar + 1;
            inj = *(IntPtr*)inj + 1;
            state.Location = tar;
            state.OriginalValue = new IntPtr(*(int*)tar);

            *(int*)tar = *(int*)inj + (int)(long)inj - (int)(long)tar;
            return state;

#else
            state.Location = tar;
            state.OriginalValue = *(IntPtr*)tar;
            * (IntPtr*)tar = *(IntPtr*)inj;
            return state;
#endif
        }
    }

    public struct MethodReplacementState : IDisposable
    {
        internal IntPtr Location;
        internal IntPtr OriginalValue;
        public void Dispose()
        {
            this.Restore();
        }

        public unsafe void Restore()
        {
#if DEBUG
            *(int*)Location = (int)OriginalValue;
#else
            *(IntPtr*)Location = OriginalValue;
#endif
        }
    }
TakeMeAsAGuest
источник
На данный момент это лучший ответ
Евгений Горбовой
было бы полезно добавить пример использования
kofifus
Удивительный! Я просто попробовал, и он работает ~ 0 ~ Но мне интересно, как это работает. Не могли бы вы мне что-нибудь об этом рассказать? ссылку или тему, по которой можно найти ответ?
юг
3

Я знаю, что это не точный ответ на ваш вопрос, но обычный способ сделать это - использовать подход фабрики / прокси.

Сначала мы объявляем базовый тип.

public class SimpleClass
{
    public virtual DTask<bool> Solve(int n, DEvent<bool> callback)
    {
        for (int m = 2; m < n - 1; m += 1)
            if (m % n == 0)
                return false;
        return true;
    }
}

Затем мы можем объявить производный тип (назовем его прокси).

public class DistributedClass
{
    public override DTask<bool> Solve(int n, DEvent<bool> callback)
    {
        CodeToExecuteBefore();
        return base.Slove(n, callback);
    }
}

// At runtime

MyClass myInstance;

if (distributed)
    myInstance = new DistributedClass();
else
    myInstance = new SimpleClass();

Производный тип также может быть создан во время выполнения.

public static class Distributeds
{
    private static readonly ConcurrentDictionary<Type, Type> pDistributedTypes = new ConcurrentDictionary<Type, Type>();

    public Type MakeDistributedType(Type type)
    {
        Type result;
        if (!pDistributedTypes.TryGetValue(type, out result))
        {
            if (there is at least one method that have [Distributed] attribute)
            {
                result = create a new dynamic type that inherits the specified type;
            }
            else
            {
                result = type;
            }

            pDistributedTypes[type] = result;
        }
        return result;
    }

    public T MakeDistributedInstance<T>()
        where T : class
    {
        Type type = MakeDistributedType(typeof(T));
        if (type != null)
        {
            // Instead of activator you can also register a constructor delegate generated at runtime if performances are important.
            return Activator.CreateInstance(type);
        }
        return null;
    }
}

// In your code...

MyClass myclass = Distributeds.MakeDistributedInstance<MyClass>();
myclass.Solve(...);

Единственная потеря производительности - во время построения производного объекта, первый раз происходит довольно медленно, потому что он будет использовать много отражений и отражений. Во всех остальных случаях это стоимость одновременного поиска в таблице и конструктора. Как уже говорилось, вы можете оптимизировать строительство, используя

ConcurrentDictionary<Type, Func<object>>.
Сальваторе Превити
источник
1
Хм ... это все еще требует работы от имени программиста, чтобы активно осознавать распределенную обработку; Я искал решение, которое полагается только на то, что они устанавливают атрибут [Распределенный] в методе (а не на подклассы или наследование от ContextBoundObject). Похоже, мне может потребоваться внести некоторые изменения после компиляции в сборки с использованием Mono.Cecil или чего-то в этом роде.
июнь Родос,
Я бы не сказал, что это обычный способ. Этот способ прост с точки зрения требуемых навыков (нет необходимости понимать CLR), но он требует повторения тех же шагов для каждого из заменяемых методов / классов. Если позже вы захотите что-то изменить (например, выполнить какой-то код после, а не только до), вам придется сделать это N раз (в отличие от небезопасного кода, который требует сделать это один раз). Итак, это N часов работы против 1 часа работы)
Евгений Горбовой