Varaktig instanskontext

Durable-exemplet visar hur du anpassar WCF-körningen (Windows Communication Foundation) för att aktivera varaktiga instanskontexter. Den använder SQL Server 2005 som sitt säkerhetskopieringsarkiv (SQL Server 2005 Express i det här fallet). Men det ger också ett sätt att komma åt anpassade lagringsmekanismer.

Kommentar

Installationsproceduren och bygginstruktionerna för det här exemplet finns i slutet av den här artikeln.

Det här exemplet omfattar att utöka både kanallagret och tjänstmodellskiktet i WCF. Därför är det nödvändigt att förstå de underliggande begreppen innan du går in på implementeringsinformationen.

Varaktiga instanskontexter finns i verkliga scenarier ganska ofta. Ett kundvagnsprogram har till exempel möjlighet att pausa shoppingen halvvägs genom och fortsätta den på en annan dag. Så att när vi besöker kundvagnen nästa dag återställs vår ursprungliga kontext. Observera att kundvagnsprogrammet (på servern) inte underhåller kundvagnsinstansen medan vi är frånkopplade. I stället bevaras dess tillstånd i ett beständigt lagringsmedium och använder det när du skapar en ny instans för den återställda kontexten. Därför är tjänstinstansen som kan fungera för samma kontext inte samma som den tidigare instansen (det vill: den har inte samma minnesadress).

Varaktig instanskontext möjliggörs av ett litet protokoll som utbyter ett kontext-ID mellan klienten och tjänsten. Det här kontext-ID:t skapas på klienten och överförs till tjänsten. När tjänstinstansen skapas försöker tjänstkörningen läsa in det beständiga tillstånd som motsvarar det här kontext-ID:t från en beständig lagring (som standard är det en SQL Server 2005-databas). Om inget tillstånd är tillgängligt har den nya instansen sitt standardtillstånd. Tjänstimplementeringen använder ett anpassat attribut för att markera åtgärder som ändrar tillståndet för tjänstimplementeringen så att körningen kan spara tjänstinstansen efter att ha anropat dem.

Med den föregående beskrivningen kan två steg enkelt särskiljas för att uppnå målet:

  1. Ändra meddelandet som går på kabeln för att bära kontext-ID:t.
  2. Ändra det lokala beteendet för tjänsten för att implementera anpassad instancinglogik.

Eftersom den första i listan påverkar meddelandena i tråden bör den implementeras som en anpassad kanal och anslutas till kanallagret. Det senare påverkar bara tjänstens lokala beteende och kan därför implementeras genom att utöka flera utökningspunkter för tjänsten. I de kommande avsnitten diskuteras vart och ett av dessa tillägg.

Beständig InstanceContext-kanal

Det första du bör titta på är ett kanallagertillägg. Det första steget när du skriver en anpassad kanal är att bestämma kanalens kommunikationsstruktur. När ett nytt trådprotokoll introduceras bör kanalen fungera med nästan alla andra kanaler i kanalstacken. Därför bör den ha stöd för alla mönster för meddelandeutbyte. Huvudfunktionerna i kanalen är dock desamma oavsett dess kommunikationsstruktur. Mer specifikt bör den från klienten skriva kontext-ID:t till meddelandena och från tjänsten ska det läsa det här kontext-ID:t från meddelandena och skicka det till de övre nivåerna. Därför skapas en DurableInstanceContextChannelBase klass som fungerar som den abstrakta basklassen för alla implementeringar av varaktiga instanskontextkanaler. Den här klassen innehåller vanliga tillståndsdatorhanteringsfunktioner och två skyddade medlemmar för att tillämpa och läsa kontextinformationen till och från meddelanden.

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

Dessa två metoder använder IContextManager implementeringar för att skriva och läsa kontext-ID:t till eller från meddelandet. (IContextManager är ett anpassat gränssnitt som används för att definiera kontraktet för alla kontexthanterare.) Kanalen kan antingen inkludera kontext-ID:t i en anpassad SOAP-rubrik eller i en HTTP-cookierubrik. Varje implementering av kontexthanteraren ärver från klassen ContextManagerBase som innehåller de vanliga funktionerna för alla kontexthanterare. Metoden GetContextId i den här klassen används för att komma från kontext-ID:t från klienten. När ett kontext-ID har sitt ursprung för första gången sparar metoden det i en textfil vars namn konstrueras av fjärrslutpunktsadressen (de ogiltiga filnamnsteckenen i de typiska URI:erna ersätts med @-tecken).

Senare när kontext-ID:t krävs för samma fjärrslutpunkt kontrollerar den om det finns en lämplig fil. Om den gör det läser den kontext-ID:t och returnerar. Annars returneras ett nyligen genererat kontext-ID och sparar det i en fil. Med standardkonfigurationen placeras dessa filer i en katalog med namnet ContextStore, som finns i den aktuella användarens temporära katalog. Den här platsen kan dock konfigureras med bindningselementet.

Den mekanism som används för att transportera kontext-ID:t kan konfigureras. Den kan antingen skrivas till HTTP-cookiehuvudet eller till en anpassad SOAP-rubrik. Den anpassade SOAP-huvudmetoden gör det möjligt att använda det här protokollet med icke-HTTP-protokoll (till exempel TCP eller namngivna pipes). Det finns två klasser, nämligen MessageHeaderContextManager och HttpCookieContextManager, som implementerar dessa två alternativ.

Båda skriver kontext-ID:t till meddelandet på rätt sätt. Klassen skriver till MessageHeaderContextManager exempel den till en SOAP-rubrik i WriteContext -metoden.

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 Både metoderna och ReadContextId i DurableInstanceContextChannelBase klassen anropar IContextManager.ReadContext respektive IContextManager.WriteContext. Dessa kontexthanterare skapas dock inte direkt av DurableInstanceContextChannelBase klassen. I stället används ContextManagerFactory klassen för att göra det jobbet.

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

Metoden ApplyContext anropas av de sändande kanalerna. Det matar in kontext-ID:t till de utgående meddelandena. Metoden ReadContextId anropas av de mottagande kanalerna. Den här metoden säkerställer att kontext-ID:t är tillgängligt i inkommande meddelanden och lägger till Properties det i klassens Message samling. Det genererar även en CommunicationException om det inte går att läsa kontext-ID:t och därmed gör att kanalen avbryts.

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

Innan du fortsätter är det viktigt att förstå användningen av Properties samlingen i Message klassen. Vanligtvis används den här Properties samlingen när du skickar data från lägre till de övre nivåerna från kanallagret. På så sätt kan önskade data tillhandahållas till de övre nivåerna på ett konsekvent sätt oavsett protokollinformation. Kanallagret kan med andra ord skicka och ta emot kontext-ID:t antingen som en SOAP-rubrik eller en HTTP-cookierubrik. Men det är inte nödvändigt att de övre nivåerna känner till den här informationen eftersom kanallagret gör den här informationen tillgänglig i Properties samlingen.

Nu med DurableInstanceContextChannelBase klassen på plats måste alla tio nödvändiga gränssnitt (IOutputChannel, IInputChannel, IOutputSessionChannel, IInputSessionChannel, IRequestChannel, IReplyChannel, IRequestSessionChannel, IReplySessionChannel, IDuplexChannel, IDuplexSessionChannel) implementeras. De liknar alla tillgängliga mönster för meddelandeutbyte (datagram, simplex, duplex och deras sessionskänsliga varianter). Var och en av dessa implementeringar ärver basklassen som tidigare beskrivits och anropar ApplyContextReadContextId lämpligt sätt. – DurableInstanceContextOutputChannel som implementerar IOutputChannel-gränssnittet – anropar ApplyContext till exempel metoden från varje metod som skickar meddelandena.

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

Å andra sidan DurableInstanceContextInputChannel anropar - som implementerar IInputChannel gränssnittet - ReadContextId metoden i varje metod, som tar emot meddelandena.

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

Förutom detta delegerar dessa kanalimplementeringar metodanropen till kanalen under dem i kanalstacken. Sessionskänsliga varianter har dock en grundläggande logik för att se till att kontext-ID:t skickas och är skrivskyddat för det första meddelandet som gör att sessionen skapas.

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

Dessa kanalimplementeringar läggs sedan till i WCF-kanalkörningen DurableInstanceContextBindingElement av klassen och DurableInstanceContextBindingElementSection klassen på lämpligt sätt. Mer information om bindningselement och bindningselement finns i exempeldokumentationen för HttpCookieSession-kanalen .

Tillägg för tjänstmodellskikt

Nu när kontext-ID:t har färdats genom kanallagret kan tjänstbeteendet implementeras för att anpassa instansieringen. I det här exemplet används en lagringshanterare för att läsa in och spara tillstånd från eller till det beständiga arkivet. Som tidigare beskrivits tillhandahåller det här exemplet en lagringshanterare som använder SQL Server 2005 som lagringsplats. Det är dock också möjligt att lägga till anpassade lagringsmekanismer i det här tillägget. För att göra det deklareras ett offentligt gränssnitt som måste implementeras av alla lagringshanterare.

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

Klassen SqlServerStorageManager innehåller standardimplementeringen IStorageManager . I metoden SaveInstance serialiseras det angivna objektet med hjälp av XmlSerializer och sparas i SQL Server-databasen.

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 I -metoden läss serialiserade data för ett visst kontext-ID och objektet som skapas från det returneras till anroparen.

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

Användare av dessa lagringshanterare ska inte instansiera dem direkt. De använder StorageManagerFactory klassen, som abstraherar från information om att skapa lagringshanteraren. Den här klassen har en statisk medlem, GetStorageManager, som skapar en instans av en viss typ av lagringshanterare. Om typparametern är nullskapar den här metoden en instans av standardklassen SqlServerStorageManager och returnerar den. Den validerar också den angivna typen för att se till att den implementerar IStorageManager gränssnittet.

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

Den infrastruktur som krävs för att läsa och skriva instanser från den beständiga lagringen implementeras. Nu måste du vidta nödvändiga åtgärder för att ändra tjänstbeteendet.

Som det första steget i den här processen måste vi spara kontext-ID:t, som kom via kanallagret till aktuell InstanceContext. InstanceContext är en körningskomponent som fungerar som länken mellan WCF-avsändaren och tjänstinstansen. Den kan användas för att tillhandahålla ytterligare tillstånd och beteende för tjänstinstansen. Detta är viktigt eftersom kontext-ID i sessionskänslig kommunikation endast skickas med det första meddelandet.

Med WCF kan du utöka dess InstanceContext-körningskomponent genom att lägga till ett nytt tillstånd och beteende med hjälp av dess utökningsbara objektmönster. Det utökningsbara objektmönstret används i WCF för att antingen utöka befintliga körningsklasser med nya funktioner eller för att lägga till nya tillståndsfunktioner i ett objekt. Det finns tre gränssnitt i det utökningsbara objektmönstret – IExtensibleObject<T>, IExtension<T> och IExtensionCollection<T>:

  • IExtensibleObject<T-gränssnittet> implementeras av objekt som tillåter tillägg som anpassar deras funktioner.

  • IExtension<T-gränssnittet> implementeras av objekt som är tillägg av klasser av typen T.

  • IExtensionCollection<T-gränssnittet> är en samling IExtensions som gör det möjligt att hämta IExtensions efter deras typ.

Därför bör en InstanceContextExtension-klass skapas som implementerar IExtension-gränssnittet och definierar det tillstånd som krävs för att spara kontext-ID:t. Den här klassen ger också tillstånd att lagra lagringshanteraren som används. När det nya tillståndet har sparats bör det inte vara möjligt att ändra det. Därför tillhandahålls och sparas tillståndet i instansen när den skapas och kan sedan endast nås med skrivskyddade egenskaper.

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

Klassen InstanceContextInitializer implementerar gränssnittet IInstanceContextInitializer och lägger till instanskontexttillägget till samlingen Extensions för InstanceContext som skapas.

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

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

Som tidigare beskrivits läss kontext-ID:t från Properties samlingen av Message klassen och skickas till konstruktorn för tilläggsklassen. Detta visar hur information kan utbytas mellan lagren på ett konsekvent sätt.

Nästa viktiga steg är att åsidosätta processen för att skapa tjänstinstanser. Med WCF kan du implementera anpassade instansieringsbeteenden och ansluta dem till körningen med hjälp av gränssnittet IInstanceProvider. Den nya InstanceProvider klassen implementeras för att utföra det jobbet. Den tjänsttyp som förväntas från instansprovidern godkänns i konstruktorn. Senare används detta för att skapa nya instanser. I implementeringen GetInstance skapas en instans av en lagringshanterare som letar efter en bevarad instans. Om den returnerar nullinstansen instansieras en ny instans av tjänsttypen och returneras till anroparen.

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

Nästa viktiga steg är att installera klasserna InstanceContextExtension, InstanceContextInitializeroch InstanceProvider i tjänstmodellkörningen. Ett anpassat attribut kan användas för att markera tjänstimplementeringsklasserna för att installera beteendet. DurableInstanceContextAttribute Innehåller implementeringen för det här attributet och implementerar IServiceBehavior gränssnittet för att utöka hela tjänstkörningen.

Den här klassen har en egenskap som accepterar vilken typ av lagringshanterare som ska användas. På så sätt gör implementeringen det möjligt för användarna att ange sin egen IStorageManager implementering som parameter för det här attributet.

I implementeringen ApplyDispatchBehavior verifieras det InstanceContextMode aktuella ServiceBehavior attributet. Om den här egenskapen är inställd på Singleton är det inte möjligt att aktivera varaktig instancing och en InvalidOperationException genereras för att meddela värden.

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

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

Efter detta skapas och installeras instanserna av lagringshanteraren, instanskontextinitieraren och instansprovidern i den DispatchRuntime skapade för varje slutpunkt.

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

Sammanfattningsvis har det här exemplet skapat en kanal som har aktiverat det anpassade trådprotokollet för utbyte av anpassat kontext-ID och det skriver även över standardbeteendet för instancing för att läsa in instanserna från den beständiga lagringen.

Det som finns kvar är ett sätt att spara tjänstinstansen till den beständiga lagringen. Som tidigare nämnts finns det redan de funktioner som krävs för att spara tillståndet i en IStorageManager implementering. Nu måste vi integrera detta med WCF-körningen. Ett annat attribut krävs som gäller för metoderna i tjänstimplementeringsklassen. Det här attributet ska tillämpas på de metoder som ändrar tjänstinstansens tillstånd.

Klassen SaveStateAttribute implementerar den här funktionen. Den implementerar IOperationBehavior även klassen för att ändra WCF-körningen för varje åtgärd. När en metod har markerats med det här attributet anropar WCF-körningen ApplyBehavior metoden medan lämpligt DispatchOperation skapas. Det finns en enda kodrad i den här metodimplementeringen:

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

Den här instruktionen skapar en instans av OperationInvoker typen och tilldelar den Invoker till egenskapen för den DispatchOperation som skapas. Klassen OperationInvoker är en omslutning för standardåtgärdsanroparen som skapats DispatchOperationför . Den här klassen implementerar IOperationInvoker gränssnittet. Invoke I metodimplementeringen delegeras det faktiska metodanropet till den inre åtgärdsanroparen. Innan du returnerar resultatet används dock lagringshanteraren i InstanceContext för att spara tjänstinstansen.

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;

Använda tillägget

Både kanalskiktet och tjänstmodelllagrets tillägg görs och de kan nu användas i WCF-program. Tjänsterna måste lägga till kanalen i kanalstacken med hjälp av en anpassad bindning och sedan markera tjänstimplementeringsklasserna med lämpliga attribut.

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

Klientprogram måste lägga till DurableInstanceContextChannel i kanalstacken med hjälp av en anpassad bindning. För att konfigurera kanalen deklarativt i konfigurationsfilen måste avsnittet bindningselement läggas till i samlingen bindningselementtillägg.

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

Nu kan bindningselementet användas med en anpassad bindning precis som andra standardbindningselement:

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

Slutsats

Det här exemplet visade hur du skapar en anpassad protokollkanal och hur du anpassar tjänstens beteende för att aktivera den.

Tillägget kan förbättras ytterligare genom att användarna kan ange implementeringen IStorageManager med hjälp av ett konfigurationsavsnitt. Detta gör det möjligt att ändra lagringsplatsen utan att kompilera om tjänstkoden.

Dessutom kan du försöka implementera en klass (till exempel StateBag), som kapslar in tillståndet för instansen. Klassen ansvarar för att bevara tillståndet när den ändras. På så sätt kan du undvika att använda SaveState attributet och utföra det bestående arbetet mer korrekt (du kan till exempel spara tillståndet när tillståndet faktiskt ändras i stället för att spara det varje gång en metod med SaveState attributet anropas).

När du kör exemplet visas följande utdata. Klienten lägger till två artiklar i sin kundvagn och får sedan listan över artiklar i sin kundvagn från tjänsten. Tryck på RETUR i varje konsolfönster för att stänga av tjänsten och klienten.

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

Kommentar

Om du återskapar tjänsten skrivs databasfilen över. Om du vill observera tillståndet som bevaras över flera körningar av exemplet måste du inte återskapa exemplet mellan körningarna.

Så här konfigurerar du, skapar och kör exemplet

  1. Kontrollera att du har utfört engångsinstallationsproceduren för Windows Communication Foundation-exempel.

  2. Skapa lösningen genom att följa anvisningarna i Skapa Windows Communication Foundation-exempel.

  3. Om du vill köra exemplet i en konfiguration med en eller flera datorer följer du anvisningarna i Köra Windows Communication Foundation-exempel.

Kommentar

Du måste köra SQL Server 2005 eller SQL Express 2005 för att kunna köra det här exemplet. Om du kör SQL Server 2005 måste du ändra konfigurationen av tjänstens anslutningssträng. När du kör flera datorer krävs ENDAST SQL Server på serverdatorn.