Сообщений 10    Оценка 860 [+1/-0]         Оценить  
Система Orphus

Механизм контекстов в .NET

Автор: Тимофей Казаков
The RSDN Group

Источник: RSDN Magazine #3-2003
Опубликовано: 20.12.2003
Исправлено: 10.12.2016
Версия текста: 1.0.1
Что такое контексты и зачем они нужны?
Активизация CBO в серверном контексте
Активация CBO в клиентском контексте
Вызов метода CBO в клиентском контексте
Вызов метода CBO объекта в серверном контексте
Что осталось еще?
Примеры использования контекстов
Автоматические транзакции средствами контекстов
Заключение.

Код к статье

Вместе с появлением .NET нам была предложено масса полезных и интересных технологий и одна из них – контексты. Естественно, что контексты не являются чем-то принципиально новым. В том же COM они были доступны, начиная с Windows2000, но популярными они не были. Возможно, с появлением .NET контексты будут использоваться более активно. Почему «возможно», а не «наверняка»? Да потому, что большая часть документации пространства имен System.Runtime.Remoting.Contexts содержит только «This type supports the .NET Framework infrastructure and is not intended to be used directly from your code», то есть классы, входящие в это пространство имён, не документированы. Конечно, нельзя сказать, что за все время существования .NET ничего не изменилось. Например, MSDN Magazine уже напечатал две статьи в мартовских выпусках за 2002 и 2003 годы, и очень вероятно, что в марте 2004 выйдет еще одна статья, посвященная ведению лога вызовов на более высоком уровне.

Что такое контексты и зачем они нужны?

Вольный перевод определения контекста из MSDN говорит примерно следующее: "Контекст – это упорядоченный набор свойств, определяющих окружение для объектов, которые исполняются в нем." Создается контекст на этапе активации объекта, для работы которого нужны те или иные службы (JIT-активация, поддержка транзакций, синхронизация, безопасность и многое другое). Естественно, что в рамках одного контекста может существовать несколько объектов.

Ядром инфраструктуры контекстов является класс Context (пространство имен System.Runtime.Remoting.Contexts). Рассмотрим его основные методы:

Context Constructor  Конструктор класса. Обычно контексты создаются в процессе активации ContextBoundObject (CBO), но иногда может потребоваться явно создать новый контекст.
SetProperty Установить свойство контекста.
GetProperty Получить свойство контекста.
Freeze Заморозить контекст. «Заморозка» контекста подразумевает, что все нужные свойства контекста уже добавлены, и мы готовы начать его использование.
DoCallBack Сделать обратный вызов в контекст. Этот метод аналогичен методу AppDomain.DoCallBack с единственным отличием, что вызов будет обрабатываться в другом контексте.
RegisterDynamicProperty Регистрация динамического свойства.
UnregisterDynamicProperty Удалить динамическое свойство.

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

Как уже упоминалось, использование контекстов тесно связано с классом ContextBoundObject. Это единственный класс (не считая его наследников), для которого всегда создается контекст исполнения. Естественно, встает вопрос – как управлять созданием контекста и наполнением его свойствами?

Наверное, все знают о механизме атрибутов в .NET Framework и о том, как их можно использовать для расширения метаинформации о классе. Контексты в этом случае не исключение. Здесь также активно используются атрибуты для описания дополнительных возможностей. И основным атрибутом здесь выступает ContextAttribute. Обычно этот атрибут используется для управления созданием нового контекста и определения свойств создаваемого контекста. К атрибутам и свойствам мы вернемся чуть позже, а пока рассмотрим, как обеспечивается взаимодействие с CBO.

Работа с CBO в .NET Framework построена на использовании связки TransparentProxy и RealProxy. И здесь программист может столкнуться с уже упоминавшейся трудностью – отсутствием документации. Если RealProxy еще худо-бедно документирован, то с TransparentProxy дела обстоят хуже. В MSDN есть несколько незначительных упоминаний о том, что он существует, и что обычному программисту он не нужен. TP трудно назвать полноценным классом – это, скорее, некий мост между средой исполнения и управляемым кодом. Поэтому легко можно обойтись без документации, так как единственное, что необходимо помнить – это то, что в нем хранится ссылка на RealProxy и некий объект под названием StubData, который используется для принятия решения о том, какой из механизмов вызова методов CBO-объекта будет использоваться (рисунок 1).


Рисунок 1

Естественно, что это создает один из побочных эффектов – все (sic!) обращения к CBO идут через TransparentProxy (TP), что вносит накладные расходы.

Но это все относится к механизмам работы с ContextBoundObject, а пока мы вернемся к самому началу и рассмотрим механизм создания CBO. Поговорим и о том, на основе какой информации принимается решение о создании для объекта нового контекста или использовании существующего контекста.

Каждый раз, когда исполняемая среда создаёт объект, требующий управляемой активации, она проверяет наличие у этого объекта атрибутов контекста. У этих атрибутов запрашивается два специальных интерфейса – IContextAttribute и IСontextProperty (ContextBoundAttribute реализует оба этих интерфейса). Интерфейс IContextAtrtribute используется для принятия решения о создании нового контекста и наполнении его свойствами, а IContextProperty используется для описания свойств контекста. Активация любого ContextBound-объекта производится в несколько этапов (рисунок 2).


Рисунок 2

IContextAttribute.IsContextOK – каждый атрибут проверяет пригодность текущего контекста для активации. Если хоть один атрибут возвращает false, то принимается решение о создании нового контекста;

IContextAttribute.GetPropertiesForNewContext – если было принято решение о создании нового контекста, каждый атрибут получает возможность добавить в новый контекст свой список свойств;

IContextProperty.Freeze – каждое свойство уведомляется о том, что инициализация контекста завершена, и оно должно зафиксировать свое состояние;

IContextProperty.IsNewContextOK – после фиксации состояния контекста каждое свойство получает возможность проверить корректность созданного контекста и если, все в порядке, то дальше начинается процесс активации объекта.

Обычно интерфейсы IContextAttribute и IContextProperty напрямую не используются, а используется класс ContextAttribute реализующий оба этих интерфейса.

СОВЕТ

Естественно, что использование ContextBoundAttribute при объявлении класса не является обязательным требованием. Мы всегда можем воспользоваться классом Activator и указать нужные атрибуты в методе CreateInstance, или самостоятельно создать новый контекст и форсировать активацию CBO именно в нем.


Рисунок 3

Кроме интерфейса IContextProperty каждое свойство может реализовать и несколько дополнительных интерфейсов, каждый из которых так или иначе может влиять на процесс взаимодействия с ContextBoundObject:

IContributeClientContextSink Интерфейс IContributeClientContextSink дает возможность свойству принять участие в создании клиентского приемника (IMessageSink) контекста. Клиентский приемник сообщений создается в момент первого вызова из-за границы контекста и может использоваться для перехвата всех исходящих вызовов
IContributeServerContextSink Интерфейс IContributeServerContextSink дает возможность свойству принять участие в создании серверного приемника. Серверный приемник сообщений создается в момент первого входа в контекст и может использоваться для перехвата всех входящих в контекст вызовов.
IContributeEnvoySink Интерфейс IContributeEnvoySink дает возможность свойству принять участие в создании «дипломатического» приемника. «Дипломатический» приемник создается в серверном контексте, передается на сторону клиента (именно поэтому необходимо, чтобы класс, реализующий данный приемник, поддерживал сериализацию) и позволяет воздействовать на цепочку обработки сообщений еще до того, как произойдет переход из клиентского контекста в серверный. Одно из возможных назначений EnvoySink – это контроль входных параметров объекта, интеллектуальное кэширование вызовов и т.д. EnvoySink привязывается к конкретному объекту и служит для обработки сообщений конкретного объекта.
IContributeObjectSink Интерфейс IContributeObjectSink дает возможность свойству принять участие в создании приемника «объекта». Можно сказать, что этот приемник является серверным аналогом EnvoySink. Он так же, как и EnvoySink, привязывается к конкретному объекту и служит для обработки сообщений конкретного объекта.
IContextPropertyActivator Интерфейс IContextPropertyActivator дает возможность свойству влиять на процесс активации объекта.

На процесс взаимодействия с ContextBoundObject могут оказывать свое влияние и динамические свойства (IDynamicProperty). В отличие от обычных свойств, динамические свойства могут подключаться и отключаться в процессе существования контекста (методы Context.RegisterDynamicProperty и Context.UnregisterDynamicProperty):

IContributeDynamicSink Интерфейс IContributeDynamicSink дает возможность динамическому свойству создать IDynamicMessageSink. В целом возможности, предоставляемые посредством динамического приемника, схожи с теми, что можно получить через IContextPropertyActivator. Основное отличие между ними заключается в том, что IContextPropertyActivator действует только на этапе активации объекта и имеет более высокий приоритет.

Теперь рассмотрим использование свойств, входящих в состав контекста (для простоты будем рассматривать только синхронные вызовы).

Начнем мы это делать с серверного контекста. Почему с серверного? Конечно, можно было бы начать и с клиентского, но именно свойства серверного контекста обычно задействуются первыми. Отчего это происходит именно так – достаточно вспомнить с чего начинается исполнение большинства программ:

      static
      void Main(string[] args)
{
//  Какой-то код здесь
}

Что получается, когда управление попадает в Main? Естественно, создаётся контекст, используемый по умолчанию, у которого клиентские свойства не определены. Когда будет создан первый ContextBoundObject, то первыми начнут работать свойства именно его (т.е. серверного контекста).

Активизация CBO в серверном контексте

Будем считать, что для своего первого CBO мы не поскупились на всевозможные свойства и решили использовать все доступные нам возможности. Что при этом будет происходить, изображено на рисунке 4. Процесс активации объекта начинается в методе DoCrossContextActivation класса ActivationServices (к сожалению, данный класс объявлен как internal, и поэтому его использование сильно затруднено). Первым делом в этом методе создается и инициализируется новый контекст. После этого контекст замораживается (IContextProperty.Freeze), и каждое свойство получает возможность проверить состояние нового контекста (блок IContextProperty на рисунке 2). После проверки контекста инициируется процесс создания «серверного приемника». Для этого каждое свойство проверяется на предмет поддержки интерфейса IContributeServerContextSink и, если такая поддержка обнаруживается, то создается цепочка «приемников», которая завершается специальным приемником ServerContextTerminatorSink.


Рисунок 4

После формирования цепочки «серверных приемников» вызов переходит к первому приемнику в цепочке и, следуя по ней, достигает последнего – ServerContextTerminatorSink, который уведомляет свойства контекста, поддерживающие интерфейс IСontextPropertyActivator, и передает вызов активатору объекта - IConstructionCallMessage.Activator.Activate. Обычно (если никто ничего не поменял на предыдущем шаге) активатором выступает внутренний класс ConstructionLevelActivator, обеспечивающий конструирование объекта и создание его «дипломатических приемников». Так же, как и для «серверных приемников», это происходит через опрос свойств контекста, только в данном случае используется интерфейс IContributeEnvoySink.

Что является здесь наиболее важным? Во-первых, получая вызов IContributeServerContextSink.GetServerContextSink, мы можем быть уверены, что создаваемый объект является в этом контексте «первым». Во вторых, именно на этапе активации создается EnvoySink объекта и, хотя вызова SyncProcessMessage для EnvoySink в серверном контексте не будет, именно на этапе активации объект, реализующий EnvoySink, будет отправлен в клиентский контекст (вполне возможно, что его даже придется сериализовать).

Естественно, что в процессе серверной активации некоторые свойства оказались незадействованными. Разумеется, не все можно сделать в серверном контексте, и иногда более удобно выполнить часть работы в своем «родном» контексте. Поэтому вернемся и рассмотрим, что же происходило в клиентском контексте до того, как мы начали активацию в серверном.

Активация CBO в клиентском контексте

Активация в клиентском контексте (рисунок 5) начинается после того, как было принято решение о создании нового контекста. Это происходит непосредственно после того, как были опрошены все атрибуты, и один из них (IContextAttribute.IsContextOK) вернул false. Естественно, что еще до начала активации была создана «парочка» TransparentProxy и RealProxy, и именно благодаря действиям RealProxy запускается механизм создания нового контекста (вообще-то, RealProxy - это абстрактный класс, в реальности используется недокументированный класс RemotingProxy)


Рисунок 5

Так или иначе, будем считать, что активация CBO в клиентском контексте начинается с создания цепочки «клиентских приемников». Для этого используются свойства контекста с интерфейсом IContributeClientSink (стоит отметить, что IContributeClientSink используется только один раз, в тот момент, когда управление передается за границу клиентского контекста). После обработки сообщения в клиентском приемнике мы попадаем в ClientContextTerminatorSink, который «уведомляет» динамические свойства контекста и IContextPropertyActivator о вызове. Последним этапом обработки сообщения в клиентском контексте является специальный объект ContextLevelActivator. Именно на этом этапе осуществляется проверка свойств (рисунок 2) вновь созданного серверного контекста – IContextProperty.Freeze & IContextProperty.IsNewContextOK.

Но на этом работа со свойствами контекста еще не завершена. Часть свойств обрабатывается при первом вызове метода CBO.

Вызов метода CBO в клиентском контексте

Когда рассматривался процесс активизации объекта в серверном контексте, был упомянут специальный приемник – EnvoySink. Как уже говорилось – этот приемник был создан в процессе «серверной» активации и именно он получит шанс первым обработать сообщение на стороне клиента. Такое «поведение» делает “дипломатические” приемники очень удобным инструментом для контроля/модификации входных параметров. Наиболее полезным это может оказаться в распределенных приложениях. В этом случае неверные данные даже не будут переданы на сервер (нужно учитывать, что есть возможность обойти вызов EnvoySink, поэтому наиболее эффективным будет дублирование проверяющего кода как в EnvoySink, так и в ObjectSink).

Вызов метода CBO объекта в серверном контексте

Выйдя за пределы клиентского контекста, вызов проходит через уже созданную цепочку серверных приемников и попадает в ServerContextTerminatorSink (рисунок 7). Этот стандартный приемник инициирует создание цепочки «объектных приемников», которая будет завершена приемником ServerObjectTerminatorSink (как и во всех остальных случаях, приемники создаются с помощью свойств контекста, только в данном случае будет использоваться IContributeObjectSink).


Рисунок 7

Что было пропущено на этой диаграмме? Для упрощения можно считать, что вызов методов CBO-объекта осуществляется напрямую из ServerObjectTerminatorSink. В реальности же есть еще один дополнительный приемник – это StackBuilderSink, и именно он отвечает за доставку вызова к методам объекта. Также была пропущена одна интересная возможность – CBO сам может реализовать IMessageSink, и тогда реализация из ServerObjectTerminatorSink не будет использовать StackBuilderSink, а вызовет метод SyncProcessMessage у CBO-объекта.

СОВЕТ

Может возникнуть вопрос – если нет возможности вызвать StackBuilderSink (например, если CBO-объект реализовал IMessageSink), то как обратиться к объекту, имея на руках только IMessage? В этом случае все просто – метод RemotingServices.ExecuteMessage обеспечивает практически «прямой» вызов StackBuilderSink.

Естественно, что может возникнуть вопрос, для чего нужны TerminatorSink и с чем связано такое название. TerminatorSink – это специальный приемник, который завершает обработку сообщений на определенном уровне и служит неким гарантом того, что он на этом «уровне» всегда будет последним (именно поэтому его свойство IMessageSink.NextSink всегда возвращает null. Таким образом, его просто невозможно пропустить).

Что осталось еще?

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

Также заслуживает отдельного упоминания специальный атрибут ProxyAttribute. Использование этого атрибута позволяет перехватить момент создания класса специального класса RealProxy и при необходимости использовать вместо стандартной реализации собственную. Бесспорно, что это очень интересная возможность, единственная причина, по которой мы не будем ее рассматривать, заключается в том, что к контекстам она не имеет никакого отношения. Достаточно посмотреть, с чего начинается работа с контекстом (рисунок 5). Если мы создаем собственную реализацию RealProxy, то мы теряем практически всю инфраструктуру (очень сомнительно, что кто-то сможет написать собственную реализацию того, что было показано на рисунках 4 – 7).

Примеры использования контекстов

В любом случае описание возможностей контекстов не так интересно, если нет примеров их использования (ведение лога вызов не в счет). И, как сейчас будет показано, предоставляемые контекстами возможности достаточно широки.

Первую задачу которую мы попробуем решить, используя контексты, – это задача «перехвата» создания MarshalByRefObject. Учитывая, что это всего лишь пример, упростим себе задачу и ограничимся простой подменой типа. Основой для теста будут классы Source и Derived

      /// <summary>
      /// Базовый класс - его мы будем перехватывать
      /// </summary>
      public
      class Source : MarshalByRefObject
{
  public Source()
  {
  }

  publicvirtualvoid InvokeTest()
  {
    Console.WriteLine("Source.InvokeTest");
  }
}
/// <summary>/// Класс-перехватчик. Все попытки создания класса Source будут /// приводить к созданию класса Derived/// </summary>publicclass Derived : Source
{
  public Derived()
  {
  }

  publicoverridevoid InvokeTest()
  {
    Console.WriteLine("Derived.InvokeTest");
  }
}

Перед решением этой задачи нужно будет ответить на вопрос – а только ли на наследников ContextBoundObject распространяется действие свойств контекста? Нет, не только. Действие контекста распространяется и на наследников класса MarshalByRefObject (естественно, что только для случая, когда класс зарегистрирован как remote-класс). Разумеется, не стоит надеяться, что будут доступны все возможности контекстов, но и тех, которые доступны, нам хватит.

С чего стоит начать? Для этого нужно посмотреть на рисунок 5. ClientContextSink получает управление в клиентском контексте первым. Единственное, свойства контекста по умолчанию мы изменить не можем. Ничего не поделаешь, придется создавать свой контекст.

Первым, что понадобится, – это реализация свойства контекста с интерфейсом IContributeClientContextSink, который создаст нам нужную реализацию ClientContextSink

      /// <summary>
      /// Специальный атрибут контекста, который создаст нам нужный приемник
      /// </summary>
[AttributeUsage(AttributeTargets.Class), Serializable]
internalclass InterceptionAttribute : ContextAttribute,  
               IContributeClientContextSink
{
  public InterceptionAttribute() : base ("Interception")
  {
  }

  IMessageSink IContributeClientContextSink.GetClientContextSink(ImessageSink
                                                                 nextSink)
  {
    returnnew InterceptionSink(nextSink);
  }

/// <summary>/// Атрибут будет всегда требовать создания нового контекста /// </summary>publicoverridebool IsContextOK(Context ctx,
                                   IConstructionCallMessage ctorMsg) 
  {
    returnfalse;
  }
}

Помимо атрибута, придется реализовать и ClientContextSink, специальный «приемник», который возьмет на себя подмену объектов. Именно он проделает всю основную работу по перехвату.

      /// <summary>
      /// Реализация ClientContextSink
      /// </summary>
      internal
      class InterceptionSink : IMessageSink 
{
  IMessageSink _nextSink;

  public InterceptionSink(IMessageSink nextSink)
  {
    _nextSink = nextSink;
  }

  public IMessage SyncProcessMessage(IMessage msg)
  {
IConstructionCallMessage ccm = msg as IConstructionCallMessage;
    if(ccm != null)
    {
            ActivatedClientTypeEntry clientTypeEntry =   
        RemotingConfiguration.IsRemotelyActivatedClientType(ccm.ActivationType);
      if (clientTypeEntry.ApplicationUrl.StartsWith("dummy:"))
      {
        MarshalByRefObject instance =  
                      InterceptionContext.CurrentContext.CreateInstance(ccm);
        return MagicHook(ccm, instance);
      }
    }
    return _nextSink.SyncProcessMessage(msg);
  }

  public IMessageSink NextSink
  {
    get { return _nextSink; }
  }

  public IMessageCtrl AsyncProcessMessage(IMessage msg,
                                          IMessageSink replySink)
  {
    IMessage retMsg = SyncProcessMessage(msg);

    if(replySink != null)
    {
      replySink.SyncProcessMessage(retMsg);
    }

    returnnull;
  }

/// <summary>/// Кое-какие манипуляции с сообщением и объектом. /// Как обычно, самое важное-то и не документировано :)/// </summary>privatestatic IConstructionReturnMessage  
                 MagicHook(IConstructionCallMessage ccm,
                           MarshalByRefObject instance)
  {
    IMessageSink os = new ObjectSink(instance);
    MethodInfo mi = ccm.GetType().GetMethod("GetThisPtr");
    MarshalByRefObject tp = (MarshalByRefObject) mi.Invoke((object) ccm,
                                                           newobject[] {});

    Type rs = Type.GetType("System.Runtime.Remoting.RemotingServices",
                            true, true);
    RealProxy rp = RemotingServices.GetRealProxy(tp);
    Type rpt = typeof(RealProxy);

    MethodInfo asm = rpt.GetMethod("AttachServer", BindingFlags.Instance | 
                                   BindingFlags.NonPublic, null,  
                                   CallingConventions.Any, new Type[]
                                 { typeof (MarshalByRefObject) }, null);
    FieldInfo idf = rpt.GetField("_identity", BindingFlags.Instance | 
                                 BindingFlags.NonPublic);
    MethodInfo sid = rs.GetMethod("SetEnvoyAndChannelSinks", 
                                  BindingFlags.NonPublic | 
                                  BindingFlags.Static);

    asm.Invoke(rp, newobject[] { instance });
    object id = idf.GetValue(rp);      
    sid.Invoke(null, newobject[] { id, os, os });      

    return EnterpriseServicesHelper.CreateConstructionReturnMessage(ccm, tp);
  }

/// <summary>/// Тоже не документировали/// </summary>privateclass ObjectSink : IMessageSink
  {
    MarshalByRefObject _instance;

    public ObjectSink(MarshalByRefObject instance) 
    {
      _instance = instance;
    }

    public IMessage SyncProcessMessage(IMessage msg)
    {
      return RemotingServices.ExecuteMessage(_instance,
                                             (IMethodCallMessage) msg);
    }

    public IMessageSink NextSink
    {
      get { returnnull; }
    }

    public IMessageCtrl AsyncProcessMessage(IMessage msg,
                                            IMessageSink replySink)
    {
      IMessage retMsg = SyncProcessMessage(SyncProcessMessage(msg));
      if (replySink != null)
      {
        replySink.SyncProcessMessage(retMsg);
      }

      returnnull;
    }
  }
}

/// <summary>/// ContextBoundObject – “гвоздь” программы/// </summary>
[Interception]
publicclass InterceptionContext : ContextBoundObject
{
  [ContextStatic]
  static InterceptionContext _currentContext;

  internalstatic InterceptionContext CurrentContext
  {
    get { return _currentContext; }
  }

  public InterceptionContext()
  {
    _currentContext = this;
  }

/// <summary>/// Конструктор для "перехваченных" типов/// </summary>protectedinternalvirtual MarshalByRefObject 
                             CreateInstance(IConstructionCallMessage ccm)
  {
    Type activationType = ccm.ActivationType;
    if (activationType == typeof (Source))
    {
      activationType = typeof(Derived);
    }      
    
    MarshalByRefObject mbr = (MarshalByRefObject) 
                    FormatterServices.GetUninitializedObject(activationType);
    RemotingServices.ExecuteMessage(mbr, ccm);
    return mbr;
  }
  
/// <summary>/// Просто тестовый метод. Здесь мы проверим, что вместо Source /// у нас будет создаваться объект типа Derived/// </summary>publicvirtualvoid Run()
  {
    Source src = new Source();
    src.InvokeTest();
  }
}

class TestConsole
{
  staticvoid Main(string[] args)
  {
////// Регистрация типа в Remoting инфраструктуре необходима для задействия managed/// активации. /// Естественно, что никаких каналов мы писать не собираемся т.к. объект нам нужен /// "локально"///  
    RemotingConfiguration.RegisterActivatedClientType(typeof(Source),  
                                                      String.Concat("dummy:", 
                                                      typeof(Source).FullName));
    
    InterceptionContext ctx = new InterceptionContext();
    ctx.Run();
  }
}

Не будем вдаваться в подробности того, как осуществляется подключение экземпляра объекта к Proxy. Вместо этого приведем диаграмму связей TransparentProxy, RealProxy и MBR объекта (рисунок 3).

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

Автоматические транзакции средствами контекстов

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

В .NET уже встроена поддержка автоматических транзакций, эту задачу решает класс System.EnterpriseServices.ServicedComponent. К сожалению, единственное, что мешает его полноценно использовать – тесная интеграция с COM, и, как следствие, необходимость регистрации компонентов в каталоге COM+,что, разумеется, сильно ограничивает возможности распространения подобных приложений. И что особенно огорчает – использование ServicedComponent – это практически единственный способ получить поддержку автоматических транзакций в .NET

ПРИМЕЧАНИЕ

Примечание: Поддержку автоматических транзакций можно также получить через ASP.NET или Web-сервис, но это не всегда удобно. Еще один вариант – начиная с .NET Framework 1.1 появилось несколько специальных классов, позволяющих использовать возможности COM+ 1.5, но по каким-то причинам эти классы доступны только под Windows Server 2003.

Итак, мы можем получить поддержку транзакций для своего объекта, воспользовавшись для этого сервисами, предоставляемыми COM+. Для этого можно использовать COM+ 1.5 и его интерфейсы (CoCreateActivity, IServiceCall, IServiceActivity и т.д.), но в этом случае мы вынуждены будем работать в WinXP или в Win2003, что само по себе является достаточно серьёзным ограничением. Можно также написать специальный библиотечный COM+-компонент, который «поделится» с нами своими сервисами. Правда, может показаться, что в этом случае мы от чего ушли, к тому и пришли, – устанавливать компонент в каталог COM+ все равно нужно. Но это не совсем так, ибо – установка такого компонента сродни установке .NET Framework её нужно будет произвести только единожды, в дальнейшем мы сможем ее использовать из любого приложения. Да и возможность работы в Win2000 будет тоже неплохим плюсом.

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

  [Transaction(TransactionOption.Required)]
  publicclass MyComponent : ApplicationComponent
  {
    publicvoid DoPrepare()
    {
      ////  Код требующий транзакции://  Работа с БД и т.п.//      
    }

    publicvoid DoJob()
    {
      ////  Код требующий транзакции://  Работа с БД и т.п.//      
    }

    publicvoid Abort()
    {
      ContextUtil.SetAbort();
    }

    publicvoid Complete()
    {
      ContextUtil.SetComplete();
    }
  }

  class TestConsole
  {
    staticvoid Main(string[] args)
    {
      using(MyComponent myComp = new MyComponent())
      {
        try
        {
          //// DoPrepare и DoJob исполняются в рамках одной транзакции//
          myComp.DoPrepare();    
          myComp.DoJob();

          ////  Зафиксировать данные в БД//
          myComp.Complete();
        }
        catch (Exception)
        {
          ////  Откатить все сделанные изменения//
          myComp.Abort();
        }
      }
    }
  }


Рисунок 8

Первое, что нам нужно будет решить, - это на каком этапе будут добавляться службы контекста. В данном случае нужно предоставлять службы транзакций для конкретного экземпляра объекта. Ответив на этот вопрос можно нарисовать примерную схему (рисунок 8) того, как должны осуществляться вызовы методов нашего класса. А теперь, сама реализация:

  [AttributeUsage(AttributeTargets.Class), Serializable]
  publicclass ApplicationComponentAttribute : ContextAttribute, IContributeObjectSink
  {  
    public ApplicationComponentAttribute()
      : base ("ApplicationComponent")
    {    
    }

    publicoverridebool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg)
    {
      returnfalse;
    }
  
    public IMessageSink GetObjectSink(MarshalByRefObject obj, IMessageSink nextSink)
    {
      returnnew TransactionSink((ContextBoundObject)obj, nextSink);
    }
  }
/// <summary>/// Реализация приемника объекта. Основная задача /// приемника это принять решение - выполнять вызов напрямую или /// выполнить его в рамках транзакции. /// </summary>internalclass TransactionSink : IMessageSink
  {  
    IMessageSink _nextSink;
    ApplicationContextTx _txContext;
    
    public TransactionSink(ContextBoundObject instance, IMessageSink nextSink)
    {
      _nextSink = nextSink;      

      TransactionAttribute ta = Attribute.GetCustomAttribute(instance.GetType(), 
                                                        typeof (TransactionAttribute), 
                                                        true) as TransactionAttribute;
      if (ta != null && ta.TransactionOption != TransactionOption.Disabled)
      {
        txContext = new ApplicationContextTx(instance, ta.TransactionOption);
      }
    }

    public IMessageCtrl AsyncProcessMessage(IMessage msg, IMessageSink replySink)
    {
      IMessage retMsg = _nextSink.SyncProcessMessage(msg);

      if (replySink != null) {
        replySink.SyncProcessMessage(retMsg);
      }

      returnnull;
    }

/// <summary>/// Мы специальным образом обрабатываем вызов IDisposable.Dispose/// основная задача метода Dispose - освободить используемые ресурсы и /// именно поэтому мы выполняем их «локально», а не в контексте COM+/// </summary>  bool IsDispose(IMessage msg) 
    {
      MethodBase dispose =
        RemotingServices.GetMethodBaseFromMethodMessage((IMethodMessage) msg);
      return dispose.DeclaringType == typeof (IDisposable);
    }

/// <summary>/// Обработка сообщения. /// </summary>/// <remarks>/// Все вызовы к IDisposable.Dispose мы будем выполнять "локально"/// </remarks>public IMessage SyncProcessMessage(IMessage msg)
    {
      if (_txContext == null || _txContext.DoContextMatch() || IsDispose(msg)) {
        return _nextSink.SyncProcessMessage(msg);
      }
      else {
        return _txContext.TransactProcessMessage(_nextSink,
                                                 (IMethodCallMessage) msg);
      }      
    }
  
    public IMessageSink NextSink
    {
      get { return _nextSink; }
    }
  }
/// <summary>/// Специальный класс, который будет служить "мостом" /// между ContextBound-объектом и COM+-компонентом, который "поделится" /// своими сервисами по работе с транзакциями/// </summary>internalclass ApplicationContextTx : IObjectControlTx, IDisposable
  {
    ContextBoundObject _object;
    IObjectContextTx _objectContext;    
    bool _activated;
    int _contextToken;

    public ApplicationContextTx(ContextBoundObject instance,
                                TransactionOption transactionOption)
    {
      _object = instance;  
      _activated = false;
	ApplicationComponent ac = _object as ApplicationComponent;
      if (ac != null)
      {
        ac.SetTxContext(this);
      }

      _objectContext = AppUtility.CreateInstance(transactionOption);
      _contextToken = _objectContext.AttachObject(this);
    }

    ~ApplicationContextTx()
    {
      Dispose(false);
    }
  
    void IObjectControlTx.OnDeactivate()
    {
      _activated = false;
      ApplicationComponent ac = _object as ApplicationComponent;
      if (ac != null) {
        ac.Deactivate();
      }
    }

    void IObjectControlTx.OnActivate()
    {
      if (ContextUtil.IsInTransaction) {
        ContextUtil.MyTransactionVote = TransactionVote.Abort;
      }
      _activated = true;
      
	
      ApplicationComponent ac = _object as ApplicationComponent;
      if (ac != null) {
        ac.Activate();
      }
    }
    
    void AttachToContext()
    {
      if (!_activated) 
      {
        _objectContext.AttachObject(this);
      }
    }

/// <summary>/// Проверка того, что вызов уже выполняется в рамках транзакции/// Такая проверка нужна для случаев, когда методы ContextBound-объекта/// будут вызываться через CallBack-механизмы./// </summary>publicbool DoContextMatch()
    {
      return _contextToken == AppUtility.GetContextToken();
    }

/// <summary>/// Обработка сообщения в контексте транзакции/// </summary>public IMethodReturnMessage TransactProcessMessage(IMessageSink sink, 
                                                       IMethodCallMessage mcm) 
    {
      if (_contextToken == -1)
      {
        thrownew ObjectDisposedException("_objectContext");
      }
      AttachToContext();

//////  Передаем вызов в COM+, используя callback-интерфейс (собственный аналог ///  IServiceCall)///
      ApplicationWorkItem workItem = new ApplicationWorkItem(sink, mcm);
      objectContext.SynchronousCall(workItem);
      return workItem.ReturnMessage;
    }

    protectedvoid Dispose(bool disposing) 
    {
      if (_objectContext != null)
      {
        Marshal.ReleaseComObject(_objectContext);
        _contextToken = -1;
        _objectContext = null;
        _object = null;
      }

      if(disposing)
      {
        GC.SuppressFinalize(this);
      }
    }

    publicvoid Dispose()
    {
      Dispose(true);
    }
  }
/// <summary>/// Базовый класс для всех Transacted-компонентов. /// Мы могли бы обойтись и обычным ContextBoundObject, но /// наличие IDisposable и методов Activate/Deactivate достаточно важно/// </summary>
  [ApplicationComponent]
  publicclass ApplicationComponent : ContextBoundObject, IDisposable
  {
    ApplicationContextTx _txContext;
    
    internal ApplicationContextTx TxContext
    {
      get { return _txContext; }    
    }

    internalvoid SetTxContext(ApplicationContextTx txContext)
    {
      if (_txContext != null || txContext == null)
      {
        thrownew InvalidOperationException();
      }

      _txContext = txContext;
    }

    public ApplicationComponent()
    {
    }

    protectedinternalvirtualvoid Activate()
    {
    }

    protectedinternalvirtualvoid Deactivate()
    {
    }
  
    protectedvirtualvoid Dispose(bool disposing) 
    {
      if(_txContext != null) 
      {
        _txContext.Dispose();
        _txContext = null;
      }
    }

    publicvirtualvoid Dispose()
    {
      Dispose(true);
    }
  }
/// <summary>/// Реализация callback-интерфейса (наш собственный аналог IServiceCall)./// </summary>internalclass ApplicationWorkItem : IObjectActivityTx
  {
    IMessageSink _sink;
    IMessage _msg;
    IMethodReturnMessage _retMsg;

    public ApplicationWorkItem(IMessageSink sink, IMessage msg)
    {
      _sink = sink;
      _msg = msg;
    }
  
    /// <summary>/// Этот метод будет вызван из COM+-компонента и будет /// работать в транзакционном контексте./// </summary>publicvoid OnCall()
    {
      _retMsg = (IMethodReturnMessage) _sink.SyncProcessMessage(_msg);

      if (Attribute.GetCustomAttribute(((IMethodCallMessage)_msg).MethodBase, 
                                       typeof(AutoCompleteAttribute)) != null)
      {
        if (_retMsg.Exception != null)
        {
          ContextUtil.SetAbort();
        }  
        else {
          ContextUtil.SetComplete();
        }
      }
    }

    public IMethodReturnMessage ReturnMessage
    {
      get { return _retMsg; }
    }
  }
  [
    ComImport,
    Guid("EE1EA35D-C237-4d5e-8AFC-31B071A97D96"),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
  ]
  internalinterface IObjectControlTx
  {
    void OnActivate();
    void OnDeactivate();
  }
  [
    ComImport,
    Guid("A04995AF-5058-4A6B-87A7-0A06E1B628B7"),
    InterfaceType(ComInterfaceType.InterfaceIsDual)
  ]
  internalinterface IObjectContextTx 
  {    
    int AttachObject(IObjectControlTx pObjControl);
    void SynchronousCall(IObjectActivityTx pActivity);
    int GetContextToken();
  };
  [
    ComImport,
    Guid("B8A53454-4186-41ee-8BCF-6D8E9E5F02AA"),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
  ]
  internalinterface IObjectActivityTx
  {
    void OnCall();
  }

Это, естественно, не весь код, некоторые незначительные классы и реализация COM+-компонента не приведены (весь код целиком можно посмотреть в прилагаемом архиве). Но основная идея проста – это перехват сообщений о вызове методов ContextBoundObject и обработка их уже в контексте COM+.

Заключение.

Контексты в .NET – это очень интересная технология, грамотное использование которой может существенно облегчить жизнь разработчику. И, естественно, возможности контекстов не ограничиваются ведением логов и обработкой транзакций, каждый разработчик может найти для себя интересное решение.


Эта статья опубликована в журнале RSDN Magazine #3-2003. Информацию о журнале можно найти здесь
    Сообщений 10    Оценка 860 [+1/-0]         Оценить