Archiv für September 2009

Setzen der Persistenz Service ID für Workflow Services

Dienstag, 29. September 2009

Gestern habe ich einen kleinen, aber nützlichen Hack gezeigt, mit welchem man die Service ID des Persistenzdienstes setzen kann. Dies funktioniert auch ganz prima, solange man nicht mit Workflow Services arbeitet.

Wie man es auch versucht, man erhält keine gültige Referenz auf den Persistenz Service.

Also was tun? Reflector zeigt uns die Interna von WorkflowService.ApplyDispatchBehavior():

static class WorkflowExtensions
...
WorkflowPersistenceService service = item.WorkflowRuntime.GetService<WorkflowPersistenceService>();
if (service != null)
{
    bool isStarted = item.WorkflowRuntime.IsStarted;
    if (isStarted)
    {
        item.WorkflowRuntime.StopRuntime();
    }
    item.WorkflowRuntime.RemoveService(service);
    item.WorkflowRuntime.AddService(new SkipUnloadOnFirstIdleWorkflowPersistenceService(service));
    if (isStarted)
    {
        item.WorkflowRuntime.StartRuntime();
    }
}
...

Was geschieht hier genau?
Wenn via Config oder Code ein Persistenz Service hinzugefügt wurde, wird die Runtime gestoppt. Der Standard Persistenz Service wird entfernt, und ein neuer Service mit dem handlichen Namen SkipUnloadOnFirstIdleWorkflowPersistenceService wird hinzugefügt. Danach wird die Runtime wieder gestartet.
Die Workflow Runtime eines Workflow Services verwendet also intern nicht den SqlWorkflowPersistenceService, sondern einen speziellen, öffentlich nicht zugänglichen und gekapselten Dienst.

Wie also kann nun in einem solchen Kontext die Service ID gesetzt werden?

Die gestern gezeigte Erweiterungsmethode kann wie folgt ergänzt werden:

static class WorkflowExtensions
{
    public static void SetServiceInstanceId(this WorkflowPersistenceService workflowPersistenceService, Guid serviceId)
    {
        Type persistenceServiceType = workflowPersistenceService.GetType();
        FieldInfo instanceIdField = persistenceServiceType.GetField("_serviceInstanceId", BindingFlags.NonPublic | BindingFlags.Instance);
        instanceIdField.SetValue(workflowPersistenceService, serviceId);
    }

    public static WorkflowRuntime GetRuntime(this WorkflowServiceHost host)
    {
        return host.Description.Behaviors.Find<WorkflowRuntimeBehavior>().WorkflowRuntime;
    }

    public static T GetWFService<T>(this WorkflowRuntime runtime) where T : class
    {
        if (typeof(T) != typeof(SqlWorkflowPersistenceService))
        {
            return runtime.GetService<T>();
        }
        else
        {
            WorkflowPersistenceService service = runtime.GetService<WorkflowPersistenceService>();
            Type sqps = service.GetType();

            FieldInfo field = (from fieldinfo in sqps.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
                               where fieldinfo.FieldType == typeof (WorkflowPersistenceService)
                               select fieldinfo).First();
            return field.GetValue(service) as T;
        }
    }
}

Mit der Erweiterungsmethode GetRuntime() erhält man die WF Runtime vom WorkflowServiceHost.
Mittels wfRuntime.GetWFService<SqlWorkflowPersistenceService>() kann danach durch den Zugriff via Reflection auf den intern als Feld gekapselten Persistenzdienst zugegriffen werden.

Microsoft WebsiteSpark Programm

Montag, 28. September 2009

Microsoft unternimmt in letzter Zeit einiges, um ihre Technologien unters (Entwickler-)Volk zu bringen. Letzten Donnerstag habe ich im Blog von Scott Guthrie gelesen, dass Microsoft ein neues Programm namens WebsiteSpark lanciert hat:

WebsiteSpark is designed for independent web developers and web development companies that build web applications and web sites on behalf of others.  It enables you to get software, support and business resources from Microsoft at no cost for three years, and enables you to expand your business and build great web solutions using ASP.NET, Silverlight, SharePoint and PHP, and the open source applications built on top of them.

WebSiteSpark provides software licenses that you can use for three years at no cost.  Once enrolled, you can download and immediately use the following software from Microsoft:

  • 3 licenses of Visual Studio 2008 Professional Edition
  • 1 license of Expression Studio 3 (which includes Expression Blend, Sketchflow, and Web)
  • 2 licenses of Expression Web 3
  • 4 processor licenses of Windows Web Server 2008 R2
  • 4 processor licenses of SQL Server 2008 Web Edition
  • DotNetPanel control panel (enabling easy remote/hosted management of your servers)

Ich war letztes Jahr schon versucht, beim BizSpark Programm mitzumachen, habe es aber dann sein lassen, weil mir die Auskunftspflicht zu lästig war.  Nun jedoch sind die Modalitäten derart lasch, dass ich mich eingeschrieben habe.

Ich besitze eigentlich momentan punkto Lizenzen alles, was ich zum Entwickeln benötige. Expression Sketchflow würde ich hingegen gerne mal ausprobieren, momentan arbeite ich ja mit Balsamiq Mockups. In den Kommentaren zum Posting von Scott Guthrie habe ich dann auch noch gelesen, dass das Programm auch ein Update auf das kommende Visual Studio 2010 beinhaltet. Mal sehen, ob’s klappt :-) .

Workflow Ownership Probleme

Montag, 28. September 2009

Eines der mächtigsten Features der WF, der Persistenz Service (SqlWorkflowPersistenceService), erlaubt es einem, den aktuellen Status von Workflow Instanzen in der Workflow DB zu speichern. Microsoft stellt dabei alles, was benötigt wird, zur Verfügung, mindestens für den hauseigenen SQL Server.

Eine Applikation, die beispielsweise auf mehreren Servern gleichzeitig läuft, kann auf dieselbe WF Persistenz Datenbank zugreifen. Dazu wurde das relativ einfache Konzept der Workflow Ownership geschaffen. Dies ist eigentlich nichts anderes als ein einfacher Lock mit einem Timeout.

In der WF Persistenz Datenbank dienen dazu die beiden Felder ownerId und ownedUntil der Tabelle InstanceState:

Aufbau der Tabelle InstanceState

Aufbau der Tabelle InstanceState

Im Feld ownerId wird die GUID des Persistenz Services der jeweiligen WF Runtime eingetragen, und in ownedUntil steht (im UTC Format!), wie lange die Instanz gesperrt ist.
Wenn diese beiden Felder nicht null sind, wird die entsprechende WF Instanz also gerade von einem Host bearbeitet.
So wird sichergestellt, dass eine Workflow Instanz immer nur von einem Host gleichzeitig ausgeführt wird.

Dies funktioniert grundsätzlich auch prima, der Persistenz Service hat allerdings einige Unschönheiten.

Bei jedem Neustart der WorkflowRuntime wird der Persistenz Service neu initialisiert. Dabei erhält er auch immer eine neu GUID, mit welcher dann die WF Instanzen in der WF DB gelockt werden.
Wenn nun zum Beispiel die Ausführung einer Workflow Instanz abgebrochen wird, bleibt der Lock bestehen.

Nun würde man erwarten, dass die Instanz nach dem Ablaufen des Lock Timeouts von einem anderen Host geladen und weiterbearbeitet werden kann. Leider ist dies nicht der Fall. Der Persistenz Service lädt beim Pollen nach freien Workflow Instanzen nur solche mit abgelaufenem Timer. Diejenigen mit abgelaufenem Lock werden nur beim Start des Services geladen. Solange also der Service nicht neu gestartet wird, kann die Workflow Instanz nicht mehr geladen werden.

Es wäre doch schön, wenn man dem Persistenz Service eine ID zuweisen könnte, damit wäre nämlich das Problem gelöst. In einer Umgebung mit mehreren Hosts, könnte jeder Service seine eigene ID erhalten.
Leider ist dies nicht vorgesehen.

Dem kann man aber abhelfen. Nach einem kurzen Blick auf die Implementation des SqlWorkflowPersistenceService im Reflector kann man dank der in C# 3.0 eingeführten Extension Methods den Service wie folgt erweitern:

static class WorkflowExtensions
{
    public static void SetServiceInstanceId(this WorkflowPersistenceService workflowPersistenceService, Guid serviceId)
    {
        Type persistenceServiceType = workflowPersistenceService.GetType();
        FieldInfo instanceIdField = persistenceServiceType.GetField(&quot;_serviceInstanceId&quot;,
                                                                    BindingFlags.NonPublic |
                                                                    BindingFlags.Instance);
        instanceIdField.SetValue(workflowPersistenceService, serviceId);
    }
}

Die Service ID wird einfach via Reflection gesetzt. Verwendung auf eigene Gefahr, bei mir hat’s jedenfalls hervorragend funktioniert.