Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
В устойчивом примере показано, как настроить среду выполнения Windows Communication Foundation (WCF) для включения контекстов долговременных экземпляров. В качестве резервного хранилища в этом примере используется SQL Server 2005, а именно SQL Server 2005 Express. Этот сервер также предоставляет возможность доступа к пользовательским механизмам хранения.
Примечание.
Процедура настройки и инструкции по построению для данного примера приведены в конце этого раздела.
Этот пример включает расширение уровня канала и уровня модели службы WCF. Поэтому прежде чем перейти к реализации, необходимо ознакомиться с основными понятиями.
Устойчивые контексты экземпляров довольно часто встречаются в реальных сценариях. Например, приложение корзины покупок может приостановить покупки на полпути и продолжить его в другой день. Таким образом, при обращении к покупательской корзине на следующий день восстанавливается исходный контекст. Важно отметить, что приложение (на сервере) не хранит экземпляр покупательской корзины, когда пользователь отключен от приложения. Вместо этого оно сохраняет его состояние на устойчивом носителе, который затем используется, чтобы создать новый экземпляр для восстановленного контекста. Поэтому экземпляр службы, используемый для того же контекста, отличается от предыдущего экземпляра (т. е. имеет другой адрес памяти).
Контекст экземпляра может быть устойчивым благодаря небольшому протоколу, посредством которого осуществляется обмен идентификатором контекста между клиентом и службой. Этот ID контекста создается на клиенте и передается сервису. При создании экземпляра службы среда выполнения службы пытается загрузить сохраненное состояние, соответствующее данному ИД контекста, из постоянного хранилища (которым по умолчанию является база данных SQL Server 2005). Если состояние недоступно, новый экземпляр имеет состояние по умолчанию. Реализация службы использует пользовательский атрибут для отметки операций, изменяющих состояние реализации службы, чтобы среда выполнения могла сохранять экземпляр службы после выполнения этих операций.
Из предыдущего описания можно легко выделить два шага для достижения цели:
- изменить сообщение, передаваемое по линии связи, включив в него ИД контекста;
- изменить локальное поведение службы для реализации пользовательской логики создания экземпляров.
Поскольку первый в списке влияет на сообщения в канале связи, он должен быть реализован как настраиваемый канал и интегрирован в уровень канала. Последнее действие влияет только на локальное поведение службы и поэтому может быть реализовано путем расширения нескольких точек расширяемости службы. Каждое из таких расширений рассматривается в следующих разделах.
Устойчивый канал InstanceContext
В первую очередь следует обратить внимание на расширение уровня канального слоя. Первый этап создания пользовательского канала - определение структуры взаимодействия канала. По мере внедрения нового протокола провода канал должен работать практически с любым другим каналом в стеке каналов. Поэтому он должен поддерживать все шаблоны обмена сообщениями. Однако основная функциональность канала остается прежней несмотря на его структуру взаимодействия. Конкретнее, на стороне клиента он должен записывать ИД контекста в сообщения, а на стороне службы он должен считывать этот ИД контекста из сообщений и передавать его на верхние уровни. Поэтому создается класс DurableInstanceContextChannelBase, являющийся абстрактным базовым классом для всех реализаций канала устойчивого контекста экземпляра. Этот класс содержит функции управления общим конечным автоматом и два защищенных члена для применения информации о контексте к сообщениям и чтения этой информации из них.
class DurableInstanceContextChannelBase
{
//…
protected void ApplyContext(Message message)
{
//…
}
protected string ReadContextId(Message message)
{
//…
}
}
Эти два метода используют реализации IContextManager для записи ИД контекста в сообщения и чтения его из них. (IContextManager — это пользовательский интерфейс, используемый для определения контракта для всех диспетчеров контекстов.) Канал может включать идентификатор контекста в пользовательский заголовок SOAP или в заголовок файла cookie HTTP. Каждая реализация диспетчера контекста наследуется от класса ContextManagerBase, в котором содержатся общие функции для всех диспетчеров контекста. Метод GetContextId в этом классе используется для получения ИД контекста от клиента. При первом получении ИД контекста этот метод сохраняет его в текстовый файл, имя которого создается на основе адреса удаленной конечной точки (недопустимые символы имени файла в типичных URI заменяются символами "@").
Позднее, когда требуется ИД контекста для той же удаленной конечной точки, выполняется проверка на наличие соответствующего файла. Если да, то считывается ID контекста и возвращается. В противном случае он возвращает вновь созданный ИД контекста и сохраняет его в файл. С конфигурацией по умолчанию эти файлы помещаются в каталог с именем ContextStore, который находится в временном каталоге текущего пользователя. Однако это расположение настраивается с помощью элемента привязки.
Механизм, используемый для передачи ИД контекста, является настраиваемым. Он может быть записан в заголовок файла cookie HTTP или в пользовательский заголовок SOAP. В случае подхода на основе пользовательского заголовка SOAP можно использовать этот протокол с протоколами, отличными от HTTP (например, с протоколом TCP или протоколом именованных каналов). Эти две возможности реализуют два класса - MessageHeaderContextManager и HttpCookieContextManager.
Оба они правильно записывают идентификатор контекста в сообщение. Например, класс MessageHeaderContextManager позволяет записать его в заголовок SOAP с помощью метода WriteContext.
public override void WriteContext(Message message)
{
string contextId = this.GetContextId();
MessageHeader contextHeader =
MessageHeader.CreateHeader(DurableInstanceContextUtility.HeaderName,
DurableInstanceContextUtility.HeaderNamespace,
contextId,
true);
message.Headers.Add(contextHeader);
}
Методы ApplyContext и ReadContextId класса DurableInstanceContextChannelBase вызывают IContextManager.ReadContext и IContextManager.WriteContext соответственно. Однако эти диспетчеры контекста не создаются непосредственно с помощью класса DurableInstanceContextChannelBase. Для выполнения этой задачи используется класс ContextManagerFactory.
IContextManager contextManager =
ContextManagerFactory.CreateContextManager(contextType,
this.contextStoreLocation,
this.endpointAddress);
Метод ApplyContext вызывается отправляющими каналами. Он вставляет ИД контекста в исходящие сообщения. Метод ReadContextId вызывается получающими каналами. Этот метод обеспечивает доступность ИД контекста во входящих сообщениях и добавляет его в коллекцию Properties класса Message. Кроме того, в случае ошибки чтения ИД контекста возникает исключение CommunicationException, что может привести к прерыванию работы канала.
message.Properties.Add(DurableInstanceContextUtility.ContextIdProperty, contextId);
Прежде чем продолжить, важно понять принцип использования коллекции Properties в классе Message. Как правило, коллекция Properties используется при передаче данных с нижнего на верхние уровни канального слоя. Таким образом требуемые данные можно согласованно передавать на верхние уровни независимо от подробностей работы протокола. Другими словами, слой канала может отправлять и получать идентификатор контекста в виде заголовка SOAP или заголовка HTTP cookie. Однако знание этих подробностей на верхних уровнях не требуется, поскольку уровень канала делает эту информацию доступной в коллекции Properties.
Теперь, когда класс DurableInstanceContextChannelBase создан, необходимо реализовать все десять требуемых интерфейсов (IOutputChannel, IInputChannel, IOutputSessionChannel, IInputSessionChannel, IRequestChannel, IReplyChannel, IRequestSessionChannel, IReplySessionChannel, IDuplexChannel, IDuplexSessionChannel). Они напоминают каждый доступный шаблон обмена сообщениями (датаграмма, симплекс, дуплекс и их варианты сессий). Каждая из этих реализаций наследует ранее описанный базовый класс и вызывает ApplyContext и ReadContextId соответствующим образом. Например, канал DurableInstanceContextOutputChannel, реализующий интерфейс IOutputChannel, вызывает метод ApplyContext для каждого метода, который отправляет сообщения.
public void Send(Message message, TimeSpan timeout)
{
// Apply the context information before sending the message.
this.ApplyContext(message);
//…
}
С другой стороны, DurableInstanceContextInputChannel, который реализует интерфейс IInputChannel, вызывает метод ReadContextId в каждом методе, который обрабатывает сообщения.
public Message Receive(TimeSpan timeout)
{
//…
ReadContextId(message);
return message;
}
Кроме того, эти реализации каналов делегируют вызовы методов каналу, расположенному ниже них в стеке каналов. Однако у связанных с сеансами разновидностей имеется базовая логика, обеспечивающая отправку и чтение ИД контекста только для первого сообщения, которое приводит к созданию сеанса.
if (isFirstMessage)
{
//…
this.ApplyContext(message);
isFirstMessage = false;
}
Затем эти реализации каналов добавляются в среду выполнения канала WCF с помощью классов DurableInstanceContextBindingElement и DurableInstanceContextBindingElementSection соответствующим образом. Дополнительные сведения о элементах привязки и разделах элементов привязки см. в примере документации по каналу HttpCookieSession .
Расширения уровня модели служб
Теперь, когда идентификатор контекста прошел через слой канала, можно реализовать поведение службы для настройки инстанцирования. В этом образце диспетчер хранилища используется для загрузки состояния из постоянного хранилища, а также сохранения состояния в него. Как отмечалось ранее, этот пример предоставляет диспетчер хранилища, который использует SQL Server 2005 в качестве основного хранилища. Однако также можно добавить к этому расширению пользовательские механизмы хранения. Для этого объявляется открытый интерфейс, который должен быть реализован всеми диспетчерами хранилища.
public interface IStorageManager
{
object GetInstance(string contextId, Type type);
void SaveInstance(string contextId, object state);
}
Класс SqlServerStorageManager содержит реализацию IStorageManager по умолчанию. В методе SaveInstance данный объект сериализуется с помощью XmlSerializer и сохраняется в базе данных SQL Server.
XmlSerializer serializer = new XmlSerializer(state.GetType());
string data;
using (StringWriter writer = new StringWriter(CultureInfo.InvariantCulture))
{
serializer.Serialize(writer, state);
data = writer.ToString();
}
using (SqlConnection connection = new SqlConnection(GetConnectionString()))
{
connection.Open();
string update = @"UPDATE Instances SET Instance = @instance WHERE ContextId = @contextId";
using (SqlCommand command = new SqlCommand(update, connection))
{
command.Parameters.Add("@instance", SqlDbType.VarChar, 2147483647).Value = data;
command.Parameters.Add("@contextId", SqlDbType.VarChar, 256).Value = contextId;
int rows = command.ExecuteNonQuery();
if (rows == 0)
{
string insert = @"INSERT INTO Instances(ContextId, Instance) VALUES(@contextId, @instance)";
command.CommandText = insert;
command.ExecuteNonQuery();
}
}
}
В методе GetInstance сериализованные данные считываются для заданного идентификатора контекста и объект, созданный из него, возвращается вызывающему объекту.
object data;
using (SqlConnection connection = new SqlConnection(GetConnectionString()))
{
connection.Open();
string select = "SELECT Instance FROM Instances WHERE ContextId = @contextId";
using (SqlCommand command = new SqlCommand(select, connection))
{
command.Parameters.Add("@contextId", SqlDbType.VarChar, 256).Value = contextId;
data = command.ExecuteScalar();
}
}
if (data != null)
{
XmlSerializer serializer = new XmlSerializer(type);
using (StringReader reader = new StringReader((string)data))
{
object instance = serializer.Deserialize(reader);
return instance;
}
}
Пользователи этих менеджеров хранилища не должны самостоятельно создавать их экземпляры. Они используют класс StorageManagerFactory, абстрагирующий от подробностей создания диспетчера хранилища. В этом классе имеется один статический член, GetStorageManager, который создает экземпляр заданного типа диспетчера хранилища. Если параметр типа имеет значение null, этот метод создает и возвращает экземпляр класса по умолчанию SqlServerStorageManager. Он также проверяет, что заданный тип реализует интерфейс IStorageManager.
public static IStorageManager GetStorageManager(Type storageManagerType)
{
IStorageManager storageManager = null;
if (storageManagerType == null)
{
return new SqlServerStorageManager();
}
else
{
object obj = Activator.CreateInstance(storageManagerType);
// Throw if the specified storage manager type does not
// implement IStorageManager.
if (obj is IStorageManager)
{
storageManager = (IStorageManager)obj;
}
else
{
throw new InvalidOperationException(
ResourceHelper.GetString("ExInvalidStorageManager"));
}
return storageManager;
}
}
Инфраструктура, необходимая для чтения и записи экземпляров из постоянного хранилища, реализована. Теперь следует выполнить необходимые шаги по изменению поведения службы.
На первом шаге этого процесса нужно сохранить ИД контекста, поступивший через уровень канала в текущий контекст InstanceContext. InstanceContext — это компонент среды выполнения, который служит связующим звеном между диспетчером WCF и экземпляром службы. Его можно использовать для расширения состояния и поведения экземпляра службы. Это важно, поскольку в связанном с сеансами коммуникации идентификатор контекста отправляется только с первым сообщением.
WCF позволяет расширить компонент среды выполнения InstanceContext, добавив новое состояние и поведение с помощью его расширяемого шаблона объекта. Шаблон расширяемого объекта используется в WCF для расширения существующих классов среды выполнения с новыми функциями или добавления новых функций состояния в объект. В шаблоне расширяемых объектов есть три интерфейса : IExtensibleObject<T, IExtension T>< и IExtensionCollection><T>:
Интерфейс IExtensibleObject<T> реализуется объектами, которые позволяют с помощью расширений настраивать их функциональные возможности.
Интерфейс IExtension<T> реализуется объектами, которые являются расширениями классов типа T.
Интерфейс IExtensionCollection<T> — это коллекция IExtensions, которая позволяет получать IExtensions по их типу.
Таким образом, необходимо создать класс InstanceContextExtension, реализующий интерфейс IExtension и определяющий требуемое состояние для сохранения идентификатора контекста. Этот класс также предоставляет состояние для удержания используемого в данный момент диспетчера хранилища. После сохранения нового состояния его должно быть невозможно изменить. Поэтому состояние предоставляется и сохраняется в экземпляре в момент его создания, после чего доступ к нему осуществляется только с помощью свойств, доступных только для чтения.
// Constructor
public DurableInstanceContextExtension(string contextId,
IStorageManager storageManager)
{
this.contextId = contextId;
this.storageManager = storageManager;
}
// Read only properties
public string ContextId
{
get { return this.contextId; }
}
public IStorageManager StorageManager
{
get { return this.storageManager; }
}
Класс InstanceContextInitializer реализует интерфейс IInstanceContextInitializer и добавляет расширение контекста экземпляра в коллекцию расширений создаваемого контекста InstanceContext.
public void Initialize(InstanceContext instanceContext, Message message)
{
string contextId =
(string)message.Properties[DurableInstanceContextUtility.ContextIdProperty];
DurableInstanceContextExtension extension =
new DurableInstanceContextExtension(contextId,
storageManager);
instanceContext.Extensions.Add(extension);
}
Как отмечалось ранее, ИД контекста считывается из коллекции Properties класса Message и передается конструктору класса расширения. Это демонстрирует, как можно согласованно обмениваться информацией между уровнями.
Следующий важный шаг - переопределение процесса создания экземпляра службы. WCF позволяет реализовывать пользовательские поведения инстанцирования и интегрировать их со средой выполнения с помощью интерфейса IInstanceProvider. Для выполнения этой задачи реализуется новый класс InstanceProvider. Тип службы, ожидаемый от поставщика экземпляра, принимается в конструкторе. Затем он используется для создания новых экземпляров.
GetInstance В реализации создается экземпляр диспетчера хранилища, который ищет сохраненный экземпляр. Если он возвращается null, создаётся новый экземпляр типа службы и возвращается вызывающему.
public object GetInstance(InstanceContext instanceContext, Message message)
{
object instance = null;
DurableInstanceContextExtension extension =
instanceContext.Extensions.Find<DurableInstanceContextExtension>();
string contextId = extension.ContextId;
IStorageManager storageManager = extension.StorageManager;
instance = storageManager.GetInstance(contextId, serviceType);
instance ??= Activator.CreateInstance(serviceType);
return instance;
}
Следующим важным шагом является установка классов InstanceContextExtension, InstanceContextInitializer и InstanceProvider в среду выполнения модели службы. С помощью пользовательского атрибута можно отметить классы реализации службы для установки поведения.
DurableInstanceContextAttribute содержит реализацию этого атрибута и реализует интерфейс IServiceBehavior для расширения всей среды выполнения службы.
В этом классе имеется свойство, принимающее тип используемого диспетчера хранилища. Таким образом, реализация позволяет пользователям указывать собственную IStorageManager реализацию в качестве параметра этого атрибута.
ApplyDispatchBehavior В реализации InstanceContextMode проверяется текущий ServiceBehavior атрибут. Если это свойство имеет значение Singleton, устойчивое создание экземпляров невозможно, и чтобы уведомить хост, генерируется исключение InvalidOperationException.
ServiceBehaviorAttribute serviceBehavior =
serviceDescription.Behaviors.Find<ServiceBehaviorAttribute>();
if (serviceBehavior != null &&
serviceBehavior.InstanceContextMode == InstanceContextMode.Single)
{
throw new InvalidOperationException(
ResourceHelper.GetString("ExSingletonInstancingNotSupported"));
}
После этого экземпляры диспетчера хранилища, инициализатора контекста экземпляра и поставщика экземпляров создаются и устанавливаются в объекте DispatchRuntime, созданном для каждой конечной точки.
IStorageManager storageManager =
StorageManagerFactory.GetStorageManager(storageManagerType);
InstanceContextInitializer contextInitializer =
new InstanceContextInitializer(storageManager);
InstanceProvider instanceProvider =
new InstanceProvider(description.ServiceType);
foreach (ChannelDispatcherBase cdb in serviceHostBase.ChannelDispatchers)
{
ChannelDispatcher cd = cdb as ChannelDispatcher;
if (cd != null)
{
foreach (EndpointDispatcher ed in cd.Endpoints)
{
ed.DispatchRuntime.InstanceContextInitializers.Add(contextInitializer);
ed.DispatchRuntime.InstanceProvider = instanceProvider;
}
}
}
Итак, на данный момент создан канал, который предоставляет пользовательский проводной протокол для обмена пользовательским идентификатором контекста и также переопределяет поведение создания экземпляров по умолчанию для загрузки экземпляров из постоянного хранилища.
Остается способ сохранить экземпляр службы в постоянное хранилище. Как отмечалось ранее, уже имеется требуемая функциональность для сохранения состояния в реализации IStorageManager. Теперь мы должны интегрировать это с средой выполнения WCF. Требуется другой атрибут, применимый к методам в классе реализации службы. Этот атрибут должен применяться к методам, изменяющим состояние экземпляра службы.
Класс SaveStateAttribute реализует данную функциональность. Он также реализует IOperationBehavior класс для изменения среды выполнения WCF для каждой операции. Если метод помечается этим атрибутом, среда выполнения WCF вызывает ApplyBehavior во время создания соответствующего DispatchOperation. В этой реализации метода существует одна строка кода:
dispatch.Invoker = new OperationInvoker(dispatch.Invoker);
Эта инструкция создает экземпляр типа OperationInvoker и присваивает его свойству Invoker создаваемого объекта DispatchOperation. Класс OperationInvoker является оболочкой для средства вызова операции по умолчанию, созданного для DispatchOperation. Этот класс реализует интерфейс IOperationInvoker. В реализации метода Invoke фактический вызов метода передается внутреннему вызывающему операции. Однако перед возвратом результатов используется диспетчер хранилища в InstanceContext для сохранения экземпляра службы.
object result = innerOperationInvoker.Invoke(instance,
inputs, out outputs);
// Save the instance using the storage manager saved in the
// current InstanceContext.
InstanceContextExtension extension =
OperationContext.Current.InstanceContext.Extensions.Find<InstanceContextExtension>();
extension.StorageManager.SaveInstance(extension.ContextId, instance);
return result;
Использование расширения
Выполняются расширения уровня канала и модели службы, и теперь их можно использовать в приложениях WCF. Службы должны добавить канал в стек каналов с помощью настраиваемой привязки, а затем пометить классы реализации службы соответствующими атрибутами.
[DurableInstanceContext]
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class ShoppingCart : IShoppingCart
{
//…
[SaveState]
public int AddItem(string item)
{
//…
}
//…
}
Клиентским приложениям необходимо добавить канал DurableInstanceContextChannel в стек каналов с помощью пользовательской привязки. Чтобы настроить канал декларативно в файле конфигурации, необходимо добавить раздел элемента привязки в коллекцию расширений элемента привязки.
<system.serviceModel>
<extensions>
<bindingElementExtensions>
<add name="durableInstanceContext"
type="Microsoft.ServiceModel.Samples.DurableInstanceContextBindingElementSection, DurableInstanceContextExtension, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</bindingElementExtensions>
</extensions>
</system.serviceModel>
Теперь элемент привязки можно использовать с пользовательской привязкой так же, как и с другими стандартными элементами привязки.
<bindings>
<customBinding>
<binding name="TextOverHttp">
<durableInstanceContext contextType="HttpCookie"/>
<reliableSession />
<textMessageEncoding />
<httpTransport />
</binding>
</customBinding>
</bindings>
Заключение
В этом образце показано, как создать пользовательский канал протокола и настроить поведение службы, чтобы задействовать этот канал.
Расширение можно усовершенствовать, позволив пользователям указывать реализацию IStorageManager с помощью раздела конфигурации. Это делает возможным изменение резервного хранилища без повторной компиляции кода службы.
Более того, можно попытаться реализовать класс (например, StateBag), инкапсулирующий состояние экземпляра. Этот класс отвечает за сохранение состояния при каждом его изменении. Таким образом можно избегать использования атрибута SaveState и более точно выполнять операции сохранения (например, можно сохранять состояние при его фактическом изменении, а не при каждом вызове метода с атрибутом SaveState).
При запуске примера отображается следующий результат. Клиент добавляет две единицы продукции в свою покупательскую корзину и получает список имеющихся в ней единиц продукции от службы. Нажмите клавишу ВВОД в каждом окне консоли, чтобы закрыть службу и клиент.
Enter the name of the product: apples
Enter the name of the product: bananas
Shopping cart currently contains the following items.
apples
bananas
Press ENTER to shut down client
Примечание.
Повторное построение службы приводит к перезаписи файла базы данных. Для просмотра состояния, сохраняемого между несколькими запусками образца, не следует выполнять повторная сборка образца между запусками.
Установка, сборка и выполнение примера
Убедитесь, что вы выполнили процедуру однократной настройки для образцов Windows Communication Foundation.
Чтобы создать решение, следуйте инструкциям в разделе Building the Windows Communication Foundation Samples.
Чтобы запустить пример в конфигурации с одним или несколькими компьютерами, следуйте инструкциям в разделе "Примеры Windows Communication Foundation".
Примечание.
Для выполнения этого образца необходимо использовать SQL Server 2005 или SQL Express 2005. При использовании SQL Server 2005 необходимо изменить конфигурацию строки подключения службы. При выполнении на разных устройствах, SQL Server требуется только на компьютере-сервере.