Sdílet prostřednictvím


Trvanlivý kontext instance

Ukázka Durable ukazuje, jak přizpůsobit modul runtime Windows Communication Foundation (WCF) tak, aby umožňoval kontexty trvalých instancí. Používá SQL Server 2005 jako záložní úložiště (v tomto případě SQL Server 2005 Express). Poskytuje ale také způsob, jak získat přístup k vlastním mechanismům úložiště.

Poznámka:

Postup nastavení a pokyny k sestavení pro tuto ukázku najdete na konci tohoto článku.

Tato ukázka zahrnuje rozšíření vrstvy kanálu i vrstvy modelu služby WCF. Proto je nutné porozumět základním konceptům před tím, než přejdete do podrobností implementace.

Kontexty trvalých instancí lze poměrně často najít ve scénářích reálného světa. Aplikace nákupního košíku má například možnost pozastavit nákup v polovině cesty a pokračovat v dalším dni. Takže když navštívíme nákupní košík následující den, obnoví se náš původní kontext. Je důležité si uvědomit, že aplikace nákupního košíku (na serveru) při odpojení neudržuje instanci nákupního košíku. Místo toho zachová svůj stav do odolného média úložiště a použije ho při vytváření nové instance pro obnovený kontext. Proto instance služby, která může obsluhovat stejný kontext, není stejná jako předchozí instance (to znamená, že nemá stejnou adresu paměti).

Trvalý kontext instance je možný malým protokolem, který vyměňuje ID kontextu mezi klientem a službou. Toto ID kontextu se vytvoří na klientovi a přenese se do služby. Po vytvoření instance služby se modul runtime služby pokusí načíst trvalý stav, který odpovídá tomuto ID kontextu z trvalého úložiště (ve výchozím nastavení se jedná o databázi SQL Serveru 2005). Pokud není k dispozici žádný stav, má nová instance výchozí stav. Implementace služby používá vlastní atribut k označení operací, které mění stav implementace služby, aby modul runtime mohl po vyvolání uložit instanci služby.

V předchozím popisu je možné snadno rozlišit dva kroky pro dosažení cíle:

  1. Změňte zprávu, která se přenese na drát, a přeneste ID kontextu.
  2. Změňte místní chování služby tak, aby implementovaly vlastní logiku vytváření instancí.

Protože první v seznamu ovlivňuje zprávy na drátě, měla by být implementována jako vlastní kanál a připojena k vrstvě kanálu. Druhá možnost ovlivňuje pouze místní chování služby, a proto je možné ji implementovat rozšířením několika bodů rozšiřitelnosti služby. V následujících několika částech jsou probíraná všechna tato rozšíření.

Kanál Durable InstanceContext

První věc, na kterou se chcete podívat, je rozšíření vrstvy kanálu. Prvním krokem při psaní vlastního kanálu je rozhodnutí o komunikační struktuře kanálu. Při zavádění nového přenosového protokolu by měl kanál fungovat s téměř jakýmkoli jiným kanálem v zásobníku kanálu. Proto by měla podporovat všechny vzory výměny zpráv. Základní funkce kanálu jsou však stejné bez ohledu na jeho komunikační strukturu. Konkrétněji z klienta by měl zapsat ID kontextu do zpráv a ze služby by měl číst toto ID kontextu ze zpráv a předat ho do horních úrovní. Z tohoto důvodu je vytvořena třída, DurableInstanceContextChannelBase která funguje jako abstraktní základní třída pro všechny implementace kanálů kontextu trvalé instance. Tato třída obsahuje společné funkce správy stavových počítačů a dva chráněné členy pro použití a čtení kontextových informací do a ze zpráv.

class DurableInstanceContextChannelBase
{
  //…
  protected void ApplyContext(Message message)
  {
    //…
  }
  protected string ReadContextId(Message message)
  {
    //…
  }
}

Tyto dvě metody používají IContextManager implementace k zápisu a čtení ID kontextu do nebo ze zprávy. (IContextManager je vlastní rozhraní sloužící k definování kontraktu pro všechny správce kontextu.) Kanál může buď obsahovat ID kontextu do vlastní hlavičky SOAP, nebo do hlavičky souboru cookie HTTP. Každá implementace správce kontextu dědí z ContextManagerBase třídy, která obsahuje společné funkce pro všechny správce kontextu. Metoda GetContextId v této třídě se používá k původu ID kontextu z klienta. Při prvním vzniku ID kontextu ji tato metoda uloží do textového souboru, jehož název je vytvořen adresou vzdáleného koncového bodu (neplatné znaky názvu souboru v typických identifikátorech URI se nahradí znaky @).

Později, když se pro stejný vzdálený koncový bod vyžaduje ID kontextu, zkontroluje, jestli existuje příslušný soubor. Pokud ano, přečte ID kontextu a vrátí se. V opačném případě vrátí nově vygenerované ID kontextu a uloží ho do souboru. Ve výchozí konfiguraci se tyto soubory umístí do adresáře s názvem ContextStore, který se nachází v dočasném adresáři aktuálního uživatele. Toto umístění je však možné konfigurovat pomocí elementu vazby.

Mechanismus použitý k přenosu ID kontextu je konfigurovatelný. Může se buď zapsat do hlavičky http cookie, nebo do vlastní hlavičky SOAP. Vlastní přístup hlavičky SOAP umožňuje použít tento protokol s protokoly bez protokolu HTTP (například TCP nebo pojmenované kanály). Existují dvě třídy, konkrétně MessageHeaderContextManager a HttpCookieContextManager, které implementují tyto dvě možnosti.

Obě zapisují ID kontextu do zprávy odpovídajícím způsobem. Třída ji například MessageHeaderContextManager zapíše do hlavičky SOAP v WriteContext metodě.

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 Obě i ReadContextId metody ve DurableInstanceContextChannelBase třídě vyvolat IContextManager.ReadContext a IContextManager.WriteContext, v uvedeném pořadí. Tito správci kontextu však nejsou přímo vytvořená DurableInstanceContextChannelBase třídou. Místo toho k této úloze používá ContextManagerFactory třídu.

IContextManager contextManager =
                ContextManagerFactory.CreateContextManager(contextType,
                this.contextStoreLocation,
                this.endpointAddress);

Metoda ApplyContext je vyvolána odesílajícími kanály. Vloží ID kontextu do odchozích zpráv. Metoda ReadContextId je vyvolána přijímajícími kanály. Tato metoda zajišťuje, že id kontextu je k dispozici v příchozích zprávách a přidá ho do Properties kolekce Message třídy. Vyvolá také CommunicationException chybu čtení ID kontextu, a proto způsobí přerušení kanálu.

message.Properties.Add(DurableInstanceContextUtility.ContextIdProperty, contextId);

Než budete pokračovat, je důležité pochopit použití Properties kolekce ve Message třídě. Tato Properties kolekce se obvykle používá při předávání dat z nižších do horních úrovní z vrstvy kanálu. Tímto způsobem lze požadovaná data poskytovat na vyšších úrovních konzistentním způsobem bez ohledu na podrobnosti protokolu. Jinými slovy, vrstva kanálu může odesílat a přijímat ID kontextu buď jako hlavičku SOAP, nebo hlavičku http cookie. Není ale nutné, aby horní úrovně o těchto podrobnostech věděly, protože vrstva kanálu tyto informace zpřístupní v kolekci Properties .

Nyní s DurableInstanceContextChannelBase třídou na místě všech deset potřebných rozhraní (IOutputChannel, IInputChannel, IOutputSessionChannel, IInputSessionChannel, IRequestChannel, IReplyChannel, IRequestSessionChannel, IReplySessionChannel, IDuplexChannel, IDuplexSessionChannel) musí být implementována. Podobají se všem dostupným vzorům výměny zpráv (datagram, simplex, duplex a jejich varianty relací). Každá z těchto implementací dědí dříve popsanou základní třídu a odpovídajícím způsobem voláApplyContext.ReadContextId DurableInstanceContextOutputChannel Například – který implementuje rozhraní IOutputChannel – volá metodu ApplyContext z každé metody, která odesílá zprávy.

public void Send(Message message, TimeSpan timeout)
{
    // Apply the context information before sending the message.
    this.ApplyContext(message);
    //…
}

Na druhou stranu – DurableInstanceContextInputChannel která implementuje IInputChannel rozhraní – volá metodu ReadContextId v každé metodě, která přijímá zprávy.

public Message Receive(TimeSpan timeout)
{
    //…
      ReadContextId(message);
      return message;
}

Kromě toho tyto implementace kanálu delegují volání metody do kanálu pod nimi v zásobníku kanálů. Varianty relace však mají základní logiku, která zajistí, že se id kontextu odešle a je jen pro první zprávu, která způsobí vytvoření relace.

if (isFirstMessage)
{
//…
    this.ApplyContext(message);
    isFirstMessage = false;
}

Tyto implementace kanálů se pak odpovídajícím způsobem přidají do modulu runtime DurableInstanceContextBindingElement kanálu WCF třídou a DurableInstanceContextBindingElementSection třídou. Další podrobnosti o prvcích vazeb a částech elementů vazeb najdete v ukázkové dokumentaci kanálu HttpCookieSession .

Rozšíření vrstvy modelu služby

Teď, když id kontextu prošlo vrstvou kanálu, je možné implementovat chování služby a přizpůsobit instanci. V této ukázce se správce úložiště používá k načtení a uložení stavu z nebo do trvalého úložiště. Jak jsme si vysvětlili dříve, tato ukázka poskytuje správce úložiště, který jako záložní úložiště používá SQL Server 2005. Do tohoto rozšíření je ale také možné přidat vlastní mechanismy úložiště. K tomu je deklarováno veřejné rozhraní, které musí implementovat všichni správci úložiště.

public interface IStorageManager
{
    object GetInstance(string contextId, Type type);
    void SaveInstance(string contextId, object state);
}

Třída SqlServerStorageManager obsahuje výchozí IStorageManager implementaci. V jeho SaveInstance metodě je daný objekt serializován pomocí XmlSerializer a je uložen do databáze SYSTÉMU 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 V metodě se serializovaná data čtou pro dané ID kontextu a objekt vytvořený z ní je vrácen volajícímu.

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;
    }
}

Uživatelé těchto správců úložiště by je neměli vytvořit přímo. Používají StorageManagerFactory třídu, která abstrahuje z podrobností o vytvoření správce úložiště. Tato třída má jeden statický člen , GetStorageManagerkterý vytvoří instanci daného typu správce úložiště. Pokud je nullparametr typu , tato metoda vytvoří instanci výchozí SqlServerStorageManager třídy a vrátí ji. Ověří také daný typ, aby se zajistilo, že implementuje IStorageManager rozhraní.

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;
}
}

Implementuje se nezbytná infrastruktura pro čtení a zápis instancí z trvalého úložiště. Teď je potřeba provést nezbytné kroky ke změně chování služby.

Jako první krok tohoto procesu musíme uložit ID kontextu, které prošlo vrstvou kanálu do aktuální instanceContext. InstanceContext je komponenta modulu runtime, která funguje jako propojení mezi dispečerem WCF a instancí služby. Dá se použít k poskytnutí dalšího stavu a chování instance služby. To je důležité, protože při relaci komunikace se ID kontextu odesílá pouze s první zprávou.

WCF umožňuje rozšířit komponentu modulu runtime InstanceContext přidáním nového stavu a chování pomocí rozšiřitelného objektového vzoru. Rozšiřitelný vzor objektu se používá ve WCF k rozšíření existujících tříd modulu runtime s novými funkcemi nebo k přidání nových stavových funkcí do objektu. V modelu rozšiřitelného objektu existují tři rozhraní – IExtensibleObject<T>, IExtension<T> a IExtensionCollection<T>:

  • IExtensibleObject<T> rozhraní je implementováno objekty, které umožňují rozšíření, která přizpůsobí jejich funkce.

  • IExtension<T> rozhraní je implementováno objekty, které jsou rozšíření třídy typu T.

  • IExtensionCollection<T> rozhraní je kolekce IExtensions, která umožňuje načítání IExtensions podle jejich typu.

Proto InstanceContextExtension třída by měla být vytvořena, která implementuje IExtension rozhraní a definuje požadovaný stav pro uložení ID kontextu. Tato třída také poskytuje stav pro uložení používaného správce úložiště. Po uložení nového stavu by nemělo být možné ho upravit. Stav je proto poskytován a uložen do instance v době, kdy je sestaven a pak přístupný pouze pomocí vlastností jen pro čtení.

// 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 třída implementuje IInstanceContextInitializer rozhraní a přidá rozšíření kontextu instance do kolekce Extensions InstanceContext je vytvářen.

public void Initialize(InstanceContext instanceContext, Message message)
{
    string contextId =
  (string)message.Properties[DurableInstanceContextUtility.ContextIdProperty];

    DurableInstanceContextExtension extension =
                new DurableInstanceContextExtension(contextId,
                     storageManager);
    instanceContext.Extensions.Add(extension);
}

Jak je popsáno dříve, ID kontextu je přečteno z Properties kolekce Message třídy a předáno konstruktoru třídy rozšíření. To ukazuje, jak se dají informace mezi vrstvami vyměňovat konzistentně.

Dalším důležitým krokem je přepsání procesu vytváření instance služby. WCF umožňuje implementaci vlastního chování vytváření instancí a jejich připojení k modulu runtime pomocí rozhraní IInstanceProvider. InstanceProvider Nová třída je implementována pro tuto úlohu. V konstruktoru je přijat typ služby očekávaný od zprostředkovatele instance. Později se použije k vytvoření nových instancí. V implementaci GetInstance se vytvoří instance správce úložiště, která hledá trvalou instanci. Pokud se vrátí null, vytvoří se instance nové instance typu služby a vrátí se volajícímu.

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;
}

Dalším důležitým krokem je instalace InstanceContextExtension, InstanceContextInitializera InstanceProvider třídy do modulu runtime modelu služby. Vlastní atribut lze použít k označení tříd implementace služby k instalaci chování. Obsahuje DurableInstanceContextAttribute implementaci tohoto atributu IServiceBehavior a implementuje rozhraní pro rozšíření celého modulu runtime služby.

Tato třída má vlastnost, která přijímá typ správce úložiště, který se má použít. Tímto způsobem implementace umožňuje uživatelům zadat vlastní IStorageManager implementaci jako parametr tohoto atributu.

V implementaci ApplyDispatchBehaviorInstanceContextMode se ověřuje aktuální ServiceBehavior atribut. Pokud je tato vlastnost nastavena na Singleton, povolení trvalé instanceng není možné a InvalidOperationException je vyvolán k upozornění hostitele.

ServiceBehaviorAttribute serviceBehavior =
    serviceDescription.Behaviors.Find<ServiceBehaviorAttribute>();

if (serviceBehavior != null &&
     serviceBehavior.InstanceContextMode == InstanceContextMode.Single)
{
    throw new InvalidOperationException(
       ResourceHelper.GetString("ExSingletonInstancingNotSupported"));
}

Potom se instance správce úložiště, inicializátor kontextu instance a zprostředkovatel instance vytvoří a nainstalují do DispatchRuntime vytvořeného pro každý koncový bod.

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;
        }
    }
}

V souhrnu zatím tato ukázka vytvořila kanál, který povolil vlastní protokol wire pro výměnu ID vlastního kontextu a také přepíše výchozí chování vytváření instancí pro načtení instancí z trvalého úložiště.

Zbývá uložit instanci služby do trvalého úložiště. Jak je popsáno dříve, už existuje požadovaná funkce pro uložení stavu v IStorageManager implementaci. Teď musíme tuto integraci integrovat s modulem runtime WCF. Je vyžadován další atribut, který se vztahuje na metody ve třídě implementace služby. Tento atribut se má použít u metod, které mění stav instance služby.

Třída SaveStateAttribute implementuje tuto funkci. Implementuje IOperationBehavior také třídu pro úpravu modulu runtime WCF pro každou operaci. Pokud je metoda označena tímto atributem, modul runtime WCF vyvolá metodu ApplyBehavior při vytváření příslušné DispatchOperation metody. V této implementaci metody je jeden řádek kódu:

dispatch.Invoker = new OperationInvoker(dispatch.Invoker);

Tato instrukce vytvoří instanci OperationInvoker typu a přiřadí ji k Invoker vlastnosti DispatchOperation vytvářené. Třída OperationInvoker je obálka pro výchozí vyvolání operace vytvořené pro DispatchOperation. Tato třída implementuje IOperationInvoker rozhraní. V implementaci Invoke metody je skutečné vyvolání metody delegováno do vnitřní operace invoker. Než ale vrátíte výsledky, které správce úložiště ve správci InstanceContext úložiště použije k uložení instance služby.

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;

Použití rozšíření

Rozšíření vrstvy kanálu i vrstvy modelu služby jsou hotová a dají se teď použít v aplikacích WCF. Služby musí přidat kanál do zásobníku kanálů pomocí vlastní vazby a poté označit třídy implementace služby příslušnými atributy.

[DurableInstanceContext]
[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerSession)]
public class ShoppingCart : IShoppingCart
{
//…
     [SaveState]
     public int AddItem(string item)
     {
         //…
     }
//…
 }

Klientské aplikace musí přidat DurableInstanceContextChannel do zásobníku kanálu pomocí vlastní vazby. Pokud chcete kanál nakonfigurovat deklarativní v konfiguračním souboru, musí být do kolekce rozšíření elementu vazby přidán oddíl elementu vazby.

<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>

Teď lze element vazby použít s vlastní vazbou stejně jako ostatní standardní prvky vazby:

<bindings>
 <customBinding>
   <binding name="TextOverHttp">
     <durableInstanceContext contextType="HttpCookie"/>
     <reliableSession />
     <textMessageEncoding />
     <httpTransport />
   </binding>
 </customBinding>
</bindings>

Závěr

Tato ukázka ukázala, jak vytvořit vlastní kanál protokolu a jak přizpůsobit chování služby, aby bylo možné ho povolit.

Rozšíření je možné dále vylepšit tím, že uživatelům umožní určit implementaci IStorageManager pomocí oddílu konfigurace. Díky tomu je možné upravit záložní úložiště bez opětovného zkompilování kódu služby.

Kromě toho byste se mohli pokusit implementovat třídu (například StateBag), která zapouzdřuje stav instance. Tato třída zodpovídá za zachování stavu při každé změně. Tímto způsobem se můžete vyhnout použití atributu SaveState a přesnějšímu provádění trvalé práce (například můžete zachovat stav, když se stav skutečně změní, místo aby se při každém zavolání metody s atributem SaveState šetřil).

Při spuštění ukázky se zobrazí následující výstup. Klient přidá do nákupního košíku dvě položky a pak získá seznam položek v nákupním košíku ze služby. Stisknutím klávesy ENTER v každém okně konzoly vypnete službu a klienta.

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

Poznámka:

Opětovné sestavení služby přepíše soubor databáze. Pokud chcete sledovat stav zachovaný napříč několika spuštěními ukázky, nezapomeňte ukázku mezi spuštěními znovu sestavit.

Nastavení, sestavení a spuštění ukázky

  1. Ujistěte se, že jste pro ukázky windows Communication Foundation provedli jednorázovou instalační proceduru.

  2. Pokud chcete sestavit řešení, postupujte podle pokynů v části Sestavení ukázek Windows Communication Foundation.

  3. Pokud chcete spustit ukázku v konfiguraci s jedním nebo více počítači, postupujte podle pokynů v části Spuštění ukázek windows Communication Foundation.

Poznámka:

Abyste mohli spustit tuto ukázku, musíte používat SQL Server 2005 nebo SQL Express 2005. Pokud používáte SQL Server 2005, musíte upravit konfiguraci připojovací řetězec služby. Při spuštění mezi počítači se SQL Server vyžaduje jenom na serverovém počítači.