InfiniTec - Henning Krauses Blog

Don't adjust your mind - it's reality that is malfunctioning

Workflow Foundation Part III: Participating in persistence

This is the third post about the Workflow Foundation 4. The first two posts can be found here and here.

In the second article I introduced the IncrementExtension which asynchronously increments an integer value and waits one second each time it is called. The activity used bookmarks to stop and resume the workflow while it is running. The problem is this: When the application is forcefully stopped and restarted, the index will start again at zero. This is a shame, since the workflow instance is already persisted to the database between each call.

Enter PersistenceParticipant. This is a base class for extension that which to persist data to the instance store configured for a workflow application. The PersistenceParticipant class has three virtual methods that enable the persistence functionality. Here is the IncrementExtension from the last post extended with the persistence features:

   1: using System;
   2: using System.Activities;
   3: using System.Activities.Hosting;
   4: using System.Activities.Persistence;
   5: using System.Collections.Generic;
   6: using System.Threading;
   7: using System.Xml.Linq;
   8:  
   9: namespace WorkflowConsoleApplication1
  10: {
  11:     class IncrementExtension : PersistenceParticipant, IWorkflowInstanceExtension
  12:     {
  13:         private static XName PersistencePropertyName = XNamespace.Get("urn:example.com:WorkflowDemo/1.0/Increment").GetName("LastValue");
  14:  
  15:         int _Index;
  16:         private WorkflowInstance _Instance;
  17:         Bookmark _Bookmark;
  18:  
  19:         public void Increment(Bookmark bookmark)
  20:         {
  21:             ThreadPool.QueueUserWorkItem(state =>
  22:             {
  23:                 Thread.Sleep(1000);
  24:                 var value = Interlocked.Increment(ref _Index);
  25:                 _Bookmark = bookmark;
  26:                 ContinueWorkflow(value);
  27:             }, null);
  28:         }
  29:  
  30:         private void ContinueWorkflow(int value)
  31:         {
  32:             _Instance.BeginResumeBookmark(_Bookmark, value, CompleteResume, null);
  33:         }
  34:  
  35:         public void CompleteResume(IAsyncResult ar)
  36:         {
  37:             var result = _Instance.EndResumeBookmark(ar);
  38:         }
  39:  
  40:         IEnumerable<object> IWorkflowInstanceExtension.GetAdditionalExtensions()
  41:         {
  42:             yield break;
  43:         }
  44:  
  45:         void IWorkflowInstanceExtension.SetInstance(WorkflowInstance instance)
  46:         {
  47:             _Instance = instance;
  48:         }
  49:  
  50:         protected override void CollectValues(out IDictionary<XName, object> readWriteValues, out IDictionary<XName, object> writeOnlyValues)
  51:         {
  52:             readWriteValues = new Dictionary<XName, object> 
  53:                 {
  54:                     {PersistencePropertyName, new KeyValuePair<Bookmark, int>(_Bookmark, _Index)}
  55:                 };
  56:             writeOnlyValues = null;
  57:         }
  58:  
  59:         protected override void PublishValues(IDictionary<XName, object> readWriteValues)
  60:         {
  61:             var keyvalue = (KeyValuePair<Bookmark, int>)readWriteValues[PersistencePropertyName];
  62:             _Index = (int)keyvalue.Value;
  63:             _Bookmark = (Bookmark)keyvalue.Key;
  64:  
  65:             ContinueWorkflow(_Index);
  66:         }
  67:     }
  68: }

To save data to the instance store, a unique id for the the data, represented by an XName instance. This can be created using the XNamespace class (see line 13). With this unique id, the data of the extension instance can be persisted to the instance store. To save the data the extension needs to override the CollectValues method and add the properties to be persisted to the readWriteValues (lines 50 to 56). The runtime uses the DataContractSerializer to serialize the data. Since .NET 3.5 practically every class that has the SerializableAttribute on its class and/or implements the ISerializable interface can be serialized. At minimum, this method should serialize the bookmark. This is required to continue the workflow when it’s restarted.

When the workflow is restarted the runtime calls the PublishValues method on the extension to load the instance data from the store (line 59-66). This method also should check whether to resume any bookmarks to continue the workflow.

That’s all.

Currently rated 4.0 by 1 people

  • Currently 4/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Henning Krause on Wednesday, October 21, 2009 3:39 PM, last modified on Wednesday, October 21, 2009 3:39 PM
Permalink | Comments (0) | Post RSSRSS comment feed

Workflow Foundation 4 – Part II: NativeActivities and Extensions

This is the second post about Workflow Foundation 4, Beta 2. This time, it’s about native activities and custom extensions. The sample used here is the same as in my last post (Persistence in Workflow Foundation 4). In that post, I used a simple workflow which incremented a integer value by one each time it was executed. To make things more interesting, the custom activity does this in an asynchronous manner.

The activity is a rather simple one, since it uses an extension to do the actual work:

   1: using System.Activities;
   2:  
   3: namespace WorkflowConsoleApplication1
   4: {
   5:     public class IncrementActivity : NativeActivity
   6:     {
   7:         public OutArgument<int> Result { get; set; }
   8:  
   9:         protected override bool CanInduceIdle { get { return true; } }
  10:  
  11:         protected override void Execute(NativeActivityContext context)
  12:         {
  13:             var extension = context.GetExtension<IncrementExtension>();
  14:  
  15:             var bookmark = context.CreateBookmark("test", IncrementCallback);
  16:             extension.Increment(bookmark);
  17:         }
  18:  
  19:         private void IncrementCallback(NativeActivityContext context, Bookmark bookmark, object value)
  20:         {
  21:             Result.Set(context, (int)value);
  22:         }
  23:     }
  24: }

The action has an out argument, the Result property. The value written to this argument is incremented by one each time the activity is called. The key to enable automatic workflow persistence are bookmarks. A short summary about bookmarks can be found in plenty of blogs (for example here). But most of them are wrong with their examples: The syntax has changed quite a bit when it comes to how to create bookmarks. When an activity calls the CreateBookmark method, the runtime automatically stops the workflow instance at that point and persists it to the instance store, if configured. The bookmark takes a name and a callback. The workflow instance is resumed, when the callback is called. In this example, the callback function sets the ‘Result’ argument to the value it receives from the extension.

Note that the activity needs to return true in the CanInduceIdle property. Otherwise, a call to CreateBookmark method will throw an exception.

Once the bookmark has been created, it is handed over to the IncrementExtension.

The IncrementExtension has one method that actually does something: The Increment method. It takes a Bookmark instance as input and resumes it after waiting for 1 second on a different thread:

   1: using System;
   2: using System.Activities;
   3: using System.Activities.Hosting;
   4: using System.Collections.Generic;
   5: using System.Threading;
   6:  
   7: namespace WorkflowConsoleApplication1
   8: {
   9:     class IncrementExtension : IWorkflowInstanceExtension
  10:     {
  11:         int _Index;
  12:         private WorkflowInstance _Instance;
  13:         Bookmark _Bookmark;
  14:  
  15:         public void Increment(Bookmark bookmark)
  16:         {
  17:             ThreadPool.QueueUserWorkItem(state =>
  18:             {
  19:                 Thread.Sleep(1000);
  20:                 var value = Interlocked.Increment(ref _Index);
  21:                 _Bookmark = bookmark;
  22:                 ContinueWorkflow(value);
  23:             }, null);
  24:         }
  25:  
  26:         private void ContinueWorkflow(int value)
  27:         {
  28:             _Instance.BeginResumeBookmark(_Bookmark, value, CompleteResume, null);
  29:         }
  30:  
  31:         public void CompleteResume(IAsyncResult ar)
  32:         {
  33:             var result = _Instance.EndResumeBookmark(ar);
  34:         }
  35:  
  36:         IEnumerable<object> IWorkflowInstanceExtension.GetAdditionalExtensions()
  37:         {
  38:             yield break;
  39:         }
  40:  
  41:         void IWorkflowInstanceExtension.SetInstance(WorkflowInstance instance)
  42:         {
  43:             _Instance = instance;
  44:         }
  45:     }
  46: }

To resume the workflow instance, the extension needs to know which instance it should resume. By default, it does not have this info. It only has an instance of a Bookmark. To get the workflow instance associated with an instance of an extension instance, the extension needs to implement the IWorkflowInstanceExtension. This method is called during the initialization of the extension instance.

Given the instance and the bookmark, the extension can now call the BeginResumeBookmark method on the workflow instance.

One question remains: Where does the extension instance come from? Simple, it is provided during the initialization of the workflow:

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         var app = new WorkflowApplication(new Workflow1());
   6:  
   7:         // Specify that the workflow should be persisted everytime it is idle.
   8:         app.PersistableIdle = e => PersistableIdleAction.Persist;
   9:  
  10:         // Add an instance store that is used to persist the workflow.
  11:         app.InstanceStore = new SqlWorkflowInstanceStore("Data Source=(local);Initial Catalog=WorkflowDemo;Integrated Security=True");
  12:  
  13:         app.Extensions.Add(() => new IncrementExtension());
  14:         app.Load(new Guid("1E1A5265-A135-4D42-9D64-2ECCCF22E888"));
  15:         app.Run();
  16:  
  17:         Console.WriteLine("Current Instance id: {0}", app.Id);
  18:  
  19:         Console.ReadLine();
  20:  
  21:         app.Unload();
  22:     }
  23: }

The extension is added to the workflow application in line 13. The app.Extensions.Add method either takes a singleton instance of the extension (which is then used each time the extension is requested) or a factory method which creates a new instance each time the extension is requested.

Currently rated 4.0 by 1 people

  • Currently 4/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Henning Krause on Tuesday, October 20, 2009 7:24 AM, last modified on Tuesday, October 20, 2009 7:24 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Persistence in Workflow Foundation 4 Beta 2

The Workflow Foundation has changed dramatically since .NET 3.5… and many changes have been made even since Beta one. One of the big changes is related to persistence. In earlier versions, either an SqlWorkflowPersistenceService or the and SQLPersistenceProviderFactory was used. Things have become much easier, though, with the release of the second beta of Visual Studio 2010. A workflow can now easily be persisted with a few lines of code. For this sample, I have created a simple workflow which just counts from zero upwards. Since persistence is primarily useful for long-running workflows, I have come up with this example:

image

Obviously, this workflow runs very long… espacially, since the custom IncrementActivity waits one second before incrementing the variable ‘Index’.

To execute the workflow, only a few lines of code are required:

   1: class Program
   2: {
   3:     static void Main(string[] args)
   4:     {
   5:         var app = new WorkflowApplication(new Workflow1());
   6:  
   7:         // Specify that the workflow should be persisted everytime it is idle.
   8:         app.PersistableIdle = e => PersistableIdleAction.Persist;
   9:         
  10:         // Add an instance store that is used to persist the workflow.
  11:         app.InstanceStore = new SqlWorkflowInstanceStore("Data Source=(local);Initial Catalog=WorkflowDemo;Integrated Security=True");
  12:         
  13:         app.Run();
  14:  
  15:         Console.WriteLine("Current Instance id: {0}", app.Id);
  16:         Console.ReadLine();
  17:  
  18:         app.Unload();
  19:     }
  20: }

Sure, there are simpler ways to execute a workflow but AFAIK only this method support persistence using the SqlWorkflowInstanceStore.

To reload the workflow from the database one line needs to be added to the code (in line 12, just before the app.Run() call):

   1: app.Load(new Guid("1E1A5265-A135-4D42-9D64-2ECCCF22E888"));

Of course, the id needs to be replaced with the instance id of the workflow. Once the workflow is running, the instance id can be retrieved from the app.Id property (as shown in line 15).

That’s all.

To persist workflows to a database, the necessary tables and stored procedures must be created in a database. This can easily be accomplished with two files from the .NET 4 runtime directory under %windir%\Microsoft.NET\Framework\v4.0.21006\SQL\en. The SQL statements from the two files SqlWorkflowInstanceStoreSchema.sql and SqlWorkflowInstanceStoreLogic.sql have to be executed against the target database.

Currently rated 4.7 by 3 people

  • Currently 4.666667/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Henning Krause on Monday, October 19, 2009 2:55 PM, last modified on Tuesday, October 20, 2009 7:24 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Developing Windows Services with .NET

Developing Windows services with .NET is not always easy – sure, one can attach a debugger once the service is started (at least when developing on Windows XP / 2003). But this is not exactly an ‘F5’ experience one has with normal programs or even Websites. Another drawback of the “Attach to process” method is that the start of the service cannot be debugged. A common way to circumvent this is to write a System.Diagnostics.Debugger.Break statement within the first lines. This will bring up the Just-In-Time Debugger window that let’s you choose a debugger. Sadly, this does not work any more with Windows Vista / 2008 because services run on an isolated window station.

Luckily, there is a solution: A windows service is nothing more than a console application that is started slightly differently. One can take advantage of this by starting the service as a stand-alone program when started normally (via F5 or starting the service from the command line). But if the program detects if it is started by the Service Control manager, run it as a service. So, how can a program check whether it is started by the Service control manager? It depends on the user account the program runs under. If it’s running as “LocalSystem”, one can safely assume it’s running as a service. In any other case, the user account token has a special group membership: The System.Security.Principal.WellknownSidType.Service.

The default body of a program.cs file of blank service solution looks like this:

   1: using System.ServiceProcess;
   2:  
   3: namespace WindowsService1
   4: {
   5:     static class Program
   6:     {
   7:         /// <summary>
   8:         /// The main entry point for the application.
   9:         /// </summary>
  10:         static void Main()
  11:         {
  12:             var servicesToRun = new ServiceBase[] 
  13:                                               { 
  14:                                                   new Service1() 
  15:                                               };
  16:             ServiceBase.Run(servicesToRun);
  17:         }
  18:     }
  19: }

With a few additional lines, a comfortable F5 experience can be gained:

   1: using System;
   2: using System.Linq;
   3: using System.Security.Principal;
   4: using System.ServiceProcess;
   5:  
   6: namespace WindowsService1
   7: {
   8:     static class Program
   9:     {
  10:         /// <summary>
  11:         /// The main entry point for the application.
  12:         /// </summary>
  13:         static void Main()
  14:         {
  15:             var identity = WindowsIdentity.GetCurrent();
  16:             var principal = new WindowsPrincipal(identity);
  17:             // Check whether the current user account is the LocalSystem account
  18:             var isLocalSystemUser = 
  19:                 identity.User == new SecurityIdentifier(WellKnownSidType.LocalSystemSid, null);
  20:  
  21:             if (isLocalSystemUser || principal.IsInRole(new SecurityIdentifier(WellKnownSidType.ServiceSid, null)))
  22:             {
  23:                 var servicesToRun = new ServiceBase[]
  24:                                         {
  25:                                             new Service1()
  26:                                         };
  27:                 ServiceBase.Run(servicesToRun);
  28:             }
  29:             else
  30:             {
  31:                 var service = new Service1();
  32:                 service.StartAsApplication();
  33:             }
  34:         }
  35:  
  36:     }
  37: }

I have added a StartAsApplication() method to the Service1 class, because the OnStart() method of the default service template is protected and therefore not accessible. And that is all there is. The service can now be started by just pressing F5.

Drawbacks

The gain of the F5 experience does not come without a cost – normally, a Windows service runs under a special account (which is hopefully not LocalSystem): NetworkService, LocalService or a dedicated service account. With this approach, the service runs with the permissions of the currently logged on user. Normally, that user account has far more security permissions than the service account would have. For example, the current user might have the permission to host WCF services on arbitrary ports – something only administrators are allowed to do. A service account would need explicit permission to host that service. Another example are private keys of certificates in the LocalComputer store – not accessible for the NetworkService account by default. But if a service needs to access those keys, it won’t fail unless during development. This should be kept in mind.!

Currently rated 3.0 by 2 people

  • Currently 3/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Henning Krause on Saturday, June 13, 2009 6:26 AM, last modified on Saturday, June 13, 2009 6:28 AM
Permalink | Comments (2) | Post RSSRSS comment feed

Getting the fullqualified DNS name of the current computer

Under certain circumstances a program needs to determine the name of the computer it’s running on. The first approach to get this name is to use the System.Environment.MachineName property. However, this name only reflects the NETBIOS name of the current machine. But in larger environments a full-qualified name including the DNS domain the computer belongs to. This can be something like computername.contoso.local. One example where this full qualified name might be needed are Exchange Push notification. I’ve published a component to CodePlex makes it really easy to incorporate them in an application. However, for the notifications to reach the client the component needs to tell the Exchange server a correct callback address. In a very simple network environment, it is sufficient to specify the NETBIOS hostname. But in more complex environments, Exchange might not be able to send a notification because it cannot correctly resolve the unqualified hostname to an IP address.

The full qualified domain name of the current host can be resolved with a call tot the System.Net.NetworkInformation.IPGlobalProperties.GetIPGlobalProperties method. This method returns, among other things, the required information:

   1: var ipGlobalProperties = IPGlobalProperties.GetIPGlobalProperties();
   2: string fullQualifiedDomainName;
   3:  
   4: if (!string.IsNullOrEmpty(ipGlobalProperties.DomainName))
   5: {
   6:     fullQualifiedDomainName = string.Format("{0}.{1}", ipGlobalProperties.HostName, ipGlobalProperties.DomainName);
   7: }
   8: else
   9: {
  10:     fullQualifiedDomainName = ipGlobalProperties.HostName;
  11: }

I have updated the PushNotification component to reflect this new behavior.

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Henning Krause on Saturday, June 06, 2009 12:23 PM, last modified on Saturday, June 06, 2009 5:39 PM
Permalink | Comments (0) | Post RSSRSS comment feed

X509Certificate2 Constructor creates two empty files in the temporary files directory [Update]

2009-04-18: Microsoft has indeed a knowledgebase article on this topic and there is a hotfix availbe.

The .NET Framework has a nice class called X509Certificate2 that simplifies the handling of X.509 certificates. Specifically, it has a constructor that takes a byte array that allows a developer to load a certificate from an arbitrary storage like a database. The class uses some Win32 to parse the byte array, and one of the used functions contain a bug: During the load process, two empty temporary files are created in the temporary files directory of the user running the code. Unfortunately, these files are never cleaned up, so they accumulate over time. What’s worse, they seem to be created by the GetTempFilename function. This function creates an empty file in a specific directory with a name guaranteed to be unique. These files have a prefix (up to three letters) and a number. The number is generated by the GetTempFilename function in a sequential manner. Since the two files are never deleted by the Win32 function, the call to GetTempFilename takes longer and longer. In my case, I had over 65.000 files in the temporary files directory and the X509Certificate2 constructor took several seconds to complete.

Microsoft knows about this bug, but they don’t seem eager to fix it. More details can be found on this connect link: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=284551.

Since there will be no fix in the near future, you can use this workaround: Instead of loading the certificate from a byte array, dump it to a file and read it from there. The following snippet demonstrates this workaround:

   1: public static X509Certificate2 LoadCertificate(byte[] buffer)
   2: {
   3:     if (buffer == null) throw new ArgumentNullException("buffer");
   4:  
   5:     string filename = Path.GetTempFileName();
   6:     try
   7:     {
   8:         File.WriteAllBytes(filename, buffer);
   9:         return new X509Certificate2(filename);
  10:     }
  11:     finally
  12:     {
  13:         try
  14:         {
  15:             File.Delete(filename);
  16:         }
  17:         catch
  18:         {
  19:             // This is ok - the file is in the temporary files directory. No harm done.
  20:         }
  21:     }
  22: }

Hotfix

The knowledgebase article 931908 deals with this problem and offers a hotfix. The hot fix can be requested directly from the article.

Currently rated 5.0 by 2 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Henning Krause on Sunday, March 29, 2009 9:13 AM, last modified on Saturday, April 18, 2009 5:02 PM
Permalink | Comments (1) | Post RSSRSS comment feed

InvalidCastException: Unable to cast object of Type 'X' to 'X'

In a recent project, the above exception was thrown when returning from a web service call. Interestingly, the exception was only thrown if the code was running within a web application. It would run fine when executed in an executable or even the Visual Studio development web server. At first I suspected some sort of permission problem but that couldn't really explain that particular error message. Next suspect was the shadow-copy feature of ASP.NET: The runtime does not actually run the assemblies from the path the IIS virtual directory points. Instead it copies all assemblies to the Temporary ASP.NET Files (C:\Windows\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files on x86 machines or C:\Windows\Microsoft.NET\Framework64\v2.0.50727\Temporary ASP.NET Files on x64 machines) and executes them from there. So I added two lines of code right before the exception would be thrown:

   1:  Trace.WriteLine(instance.GetType().Assembly.Location)
   2:  Trace.WriteLine(GetType(MyType).Assembly.Location)

Upon execution, the trace output was the following:

   1:  c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727\Temporary ASP.NET Files\root\c88143cc\98574940\assembly\dl3\98dad73b\f3cf1948_ddb4c801\MyAssembly.DLL 
   2:  C:\Development\TestApplication\MyAssembly.dll

This was the problem: One assembly was loaded from two different sources. What was the cause? Turns out that the first assembly was loaded because of an assembly reference. This way it was correctly placed in the Temporary ASP.NET Files. But the second instance of the assembly was loaded via reflection with a call to System.Reflection.Assembly.LoadFile. The solution is to use System.Reflection.Assembly.LoadFrom instead of the LoadFile method.

Why is this a solution? The main difference between LoadFrom and LoadFile is that LoadFrom goes through the normal Fusion bind process, thus allowing assembly redirection to happen. LoadFile on the other hand, just loads the assembly from the specified location.

For more information on this, see this blog posting from Suzanne Cooks blog.

Currently rated 4.7 by 7 people

  • Currently 4.714286/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Henning Krause on Tuesday, May 13, 2008 4:23 PM, last modified on Tuesday, May 13, 2008 4:23 PM
Permalink | Comments (4) | Post RSSRSS comment feed

Windows Installer Xml 3.0 Extension for managed installers

The Windows Installer technology unfortunately lacks support for managed custom actions or the System.Configuration.Install.Installer class. Rob Mensching posted an article on his blog a while back why Microsoft considers custom action in general and managed custom action in particular a bad idea. While he makes some valid points (some technical and some strategic), I think managed custom installers are not a bad thing:

  • While they add a dependency on the .NET Framework at setup time, for applications using a windows service written in managed code, the Framework must be installed on the target computer at the setup time because Windows Installer will start the service during setup. And for all other managed applications you'll need the framework right after the application has been installed (to run it). Since the Framework cannot be installed using a Merge module, it must be installed before the actual setup of the application.
  • I consider myself a fairly good software developer when it comes to managed code. But my C or C++ knowledge is minimum at best. So I can either write solid managed custom actions (where I have a well-tested BCL at hand) or create spooky and unreliable custom actions in C. I prefer the former option.
  • He mentions a problem with managed custom actions using different versions of the CLR. While this may be a problem, you'll mostly write custom actions using .NET 2.0 these days. And .NET 3.0 and 3.5 both use the same CLR as 2.0.
  • What remains of his technical problems is the fact that Windows Installer on Windows 2003 will try to load the .NET Framework 1.1 into the deferred-custom action server when it tries to register assemblies into the Global Assembly Cache, which will fail if you force the .NET 2.0 runtime into the process with a managed custom action.
  • All those strategic reasons might be ok for the Windows Installer team, but I can't wait a few years until the Windows Installer team bakes all the actions I need into the core of the product. And when they do, you'll need Windows 2015 at the very least…. Not an option.

Apparently, the Visual Studio Team doesn't consider managed custom actions to be harmful - otherwise they wouldn't give you the option to run managed installers in those Visual Studio deployment projects. But these installers do lack a serious feature: The Windows Installer context. It's not that the installer context isn't propagated to the runtime (You may have wondered what the IManagedInstaller interface is meant for :-) ).

Windows Installer Xml also doesn't support managed custom actions out of the box. You have to do this yourself. One option is to decompile a Visual Studio Deployment project and see what Visual Studio does to call a managed custom action. While this will certainly work, you'll end up with a managed installer which has the same limitations as the Visual Studio deployment project: No access to the installer context. Additionally, these installer classes are always called as deferred custom actions. This means that they neither work in the immediate phase of the InstallExecuteSequence nor in the InstallUISequence.

Reinventing the wheel…

To call managed code in-process from the Windows Installer process, an intermediate unmanaged DLL must be called which in turn loads the .NET Framework into the process, spawns an AppDomain and finally runs the managed code inside this AppDomain. This is actually what the Visual Studio Deployment Project does.

The approach I'm using here is based on the article "WiX - Managed Custom Actions" by ForestWalk, which in turn is based on two other aricles: "Wrapping the Windows Installer 2.0 API" by Ian Marino and  “Hosting the CLR within a custom action” by Pablo M. Cibraro (aka Cibrax). The code in the article make it possible to call managed code in every part of the sequence. But the usage is not very intuitive to use, especially managed installer classes.

Since deferred custom actions can only access one property (CustomActionData), all information needed by be managed installer must be placed in this property. And since the CustomActionData is only an unstructured simple string property, some form of serialization is needed to put multiple properties into it.

To support all four methods of the managed installer class, you'll have to create and sequence eight custom actions: For each of the four methods (Install, Commit, Rollback, Uninstall) one action for the parameters (CustomActionData) and one action to run it.

Multiple managed installers will seriously degrade the readability of your Windows Installer XML file. That's why a took the code from the article and put it into a Windows Installer Xml Extension. I also created a small framework to simplify the development of managed installers.

Here is a simple example setup file:

    1 <?xmlversion="1.0"encoding="UTF-8"?>

    2 <Wixxmlns="http://schemas.microsoft.com/wix/2006/wi"

    3     >

    4   <ProductId="4ed3ff4f-7b33-4915-9801-a0fdd5515647"

    5     UpgradeCode="d4bacea3-a59a-4d44-b95b-1e144edfb88b"

    6     Name="Acme Sample Application"Language="1033"Version="1.0.0.0"

    7     Manufacturer="Acme Software Ltd."

    8   >

    9     <PackageInstallerVersion="300"Compressed="yes"   />  

   10     <MediaId="1"Cabinet="Sample.cab"EmbedCab="yes" />

   11     <DirectoryId="TARGETDIR"Name="SourceDir"FileSource=".\">

   12       <ComponentId="ProductComponent"Guid="865018ca-dc6f-4987-9766-cffe792cb937">

   13         <FileId="f1"Name="ManagedCustomAction.dll"Source="Include\ManagedCustomAction.dll" >

   14           <ManagedInstallerxmlns="http://schemas.infinitec.de/wix/MCAExtension">

   15             <ParameterName="TargetDir">[TARGETDIR]</Parameter>

   16             <ParameterName="AssemblyFile">Assembly is run from [#f1]</Parameter>

   17           </ManagedInstaller>

   18         </File>

   19       </Component>

   20     </Directory>

   21     <FeatureId="ProductFeature"Title="Main Feature"Level="1">

   22       <ComponentRefId="ProductComponent" />

   23     </Feature>

   24     <UIRefId="WixUI_Minimal"/>

   25     <BinaryId="ManagedCustomAction"SourceFile="Include\ManagedCustomAction.dll" />

   26     <ManagedCustomActionId="test"BinaryKey="ManagedCustomAction"Type="ManagedCustomAction.CustomAction"Execute="immediate"xmlns="http://schemas.infinitec.de/wix/MCAExtension" />

   27     <ManagedActionSequencexmlns="http://schemas.infinitec.de/wix/MCAExtension">

   28       <ManagedAction="test"After="CostFinalize"SequenceTable="InstallUISequence" />

   29     </ManagedActionSequence>

   30   </Product>

   31 </Wix>

Managed Installers

The extension makes it very easy to call managed installers or managed custom action.

Just put the tag ManagedInstaller into a File tag and the installer will be called during setup. If you need context information stored in other MSI properties, add a Parameter tag into the ManagedInstaller tag with an appropiate name and the value. From your managed installer, you can use the Parameters dictionary from the InstallContext class. Here is a sample implementation for the Install method of a System.Configuration.Install.Installer class:

    1 publicoverridevoid Install(IDictionary stateSaver)

    2 {

    3     string targetDir = Context.Parameters["TargetDir"];

    4 

    5     for (int i = 3 - 1; i >= 0; i--)

    6     {

    7         InstallerContext.Current.StartAction("ManagedCustomAction", string.Format("Install: Waiting {0} seconds...", i), "");

    8         Thread.Sleep(1000);

    9     }

   10     base.Install(stateSaver);

   11 }

In line 5, the property TargetDir is accessed. This property contains the value as specified in line 15 of the Windows Installer XML file. But far more interesting are the lines 7 and 11: These lines access the Windows Installer process and report details about what the custom action is doing. The two function wrap two flavors of the MsiProcessRecord function. The StartAction method reports the start of a major action (such as "Copying files" or "Creating registry values"). Additionally, a format string for details is specified, in this case "Waiting [1] more seconds"). The ReportDetails now just take the replacement values for the format string, in this case the number of seconds remaining).

Another important method of the InstallerContext class is the LogMessage method which writes directly to the Windows Installer log. Note that you don't have to use this method to log data. You can also use InstallContext.LogMessage or Trace.WriteLine or Console.WriteLine. The output of all those methods is captured and written to the log.

All unhandled exceptions from an Installer class are catched by the framework and cause an error message to be displayed. Unhandled exceptions in the Install, Commit and Rollback methods cause the installation to be aborted. If an exception occurs in the Uninstall method, an error dialog is displayed, but the uninstall will continue.

The four methods are sequence in the InstallExecuteSequence at the following positions:

  • Install, Commit, Rollback: Before InstallServices

  • Uninstall: Before UnpublishComponents

The installer will only be invoked if the component the file is associated with is installed.

Managed custom actions

To run a managed custom action, two things have to be done: Create a ManagedCustomAction tag under the Product tag and fill in the blanks:

  • Id: The name of the custom action
  • BinaryKey: If you want to run the custom action in the immediate sequence or in the InstallUISequence table, add the assembly to the binary table (via the Binary tag) and enter its key here.
  • FileKey: If you want to run this custom in the deferred sequence, add the assembly to the file table (via the File tag) and enter its key here.
  • Type: Name full qualified name of the type you want to run (Namespace + type name)
  • Execute: Either commit, deferred, firstSequence, immediate, oncePerProcess, rollback or secondSequence. These are the same options you have with normal Custom actions (Cutom tag)
  • Impersonate: Yes to run the custom action in the security context of the logged on user. False otherwise. The default is true. Only valid for deferred custom actions.
  • Return: asyncNoWait, asyncWait, check or ignore. These are the same options you have with normal Custom actions (Cutom tag)

Unfortunately Windows Installer XML does not allow extensions in the sequence tables, so I had to create my own: ManagedActionSequence. Add a Managed tag for each custom action you want to schedule. The Managed tag has these attributes:

  • Action: The name of the managed custom action to run.
  • After: The name of the action the managed custom action should be executed after.
  • Before: The name of the action the managed custom action should be execute before.
  • Sequence: The absolute sequence where the managed custom action should run.
  • SequenceTable: The name of the sequence table where the managed custom action should be scheduled: Either InstallUISequence, InstallExecuteSequence, AdminUISequence, AdminExecuteSequence.

The managed custom action must be a class which implements the InfiniTec.Configuration.Install.ICustomAction interface, like in this example:

    1 publicclassCustomAction: ICustomAction

    2     {

    3         publicvoid Execute()

    4         {

    5             string targetDir = InstallerContext.Current.GetProperty("TARGETDIR");

    6 

    7             InstallerContext.Current.StartAction("ManagedCustomAction", "Running custom action...", "Waiting [1] seconds...");

    8             for (int i = 3 - 1; i >= 0; i--)

    9             {

   10                 InstallerContext.Current.ReportDetails(i.ToString());

   11                 Thread.Sleep(1000);

   12             }

   13 

   14 

   15         }

   16     }

This implementation has full access to the MSI properties (see line 5, if scheduled as immediate action) and of course access to the Installer log via InstallerContext.Current.LogMessage.

Other useful classes

Since the custom actions can be executed in the immediate sequence of the install process, it has full access to all properties and tables of the installer. The rows of a view can be accessed via the View class:

    1 using (View view = InstallerContext.Current.OpenView("SELECT * FROM Binary"))

    2 using (RecordCollection records = view.Execute())

    3 {

    4     foreach (Record record in records)

    5     {

    6         string name = record[1].GetString();

    7     }

    8 }

The view returns a RecordCollection which in turn provides access to it's Record instances. Each record consists of one or more fields. Note that if you create a record with the Record.Create(int columnCount) method, the resulting record will have columnCount+1 fields - 0 to the specified value.

Modifications to the original source code

Apart from the newly added code, I made significant changes to the existing code:

To load the .NET runtime into the process I use the CLRHosting project from the article mentioned above. I have replaced all dangerous API calls (strcat, sprintf) with secure ones. But my C and C++ knowledge is VERY limited. I would appreciate it if someone with more knowledge could take a look at the code….

I have also made significant changes to the managed part of the solution. Mainly, I have encapsulated all unmanaged MSI handles in a custom SafeHandle class.

Open issues

  • Deferred custom actions with assemblies in the Binary table are not yet supported
  • Immediate custom actions with assemblies in the File table are not supported (And I don't see how this could work)
  • Managed installers do not have an immediate part
  • Managed custom actions and managed installer classes don't add ticks to the progress bar.
  • A much cleaner approach is to call the custom action in a separate process and provide access to the Windows Installer context via remoting. This approach is dicussed in more detail in the article A New Approach to Managed Custom Actions by Christopher Painter. Unfortunately he didn't release any source code and I'm lacking the necessary C and C++ skills right now.

Installation

Just copy the zip file to a directory and decompress it. In your WIX Project add a reference to the ManagedInstallerWixExtension.dll. In your setup file add the namespace http://schemas.infinitec.de/wix/MCAExtension to the list of namespace definitions.

Requirements

The extension is compiled against WIX 3.0.361 (build from December 21, 2007) using .NET 2.0.

License

The authors of the original articles haven't lost a word about the license, so I assume it's freely available. To keep this stuff freely available I publish this work under the Common Public License, the same license Windows Installer XML is published under.

Downloads

ManagedInstallerWixExtension.zip (637,678 Bytes)
Source files and binaries

Currently rated 3.0 by 1 people

  • Currently 3/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Henning Krause on Saturday, December 29, 2007 6:00 PM, last modified on Monday, December 31, 2007 6:00 AM
Permalink | Comments (2) | Post RSSRSS comment feed

Visual Studio 2008 WCF proxy generation tool and the ObservableCollection

Visual Studio 2008 has a nice WSDL generator for WCF services. Among other things, one nice feature is it's ability to generate strongly-typed lists (List<T> in C#) where the Visual Studio 2005 service proxy generator would have generated arrays. But there's more: The configuration program let's you actually choose the type of list to implement:


The WCF service configuration editor (click to enlarge)

Since I'm using WPF here I badly missed one list in the available dropdown: The ObservableCollection<T>.

But it turns out that you can force the configuration editor to generate a service proxy using the ObservableCollection<T> for arrays: Just hit the "Show all files" button in the solution explorer and navigate to your WCF reference, expand the node and open the file called Reference.svcmap. It's an XML file containing the settings for the proxy generation tool. And this file contains a tag called CollectionMapping. If you have previously changed the collection mapping with the editor, you'll find your settings there. For the above selection of the standard generic List, you'll find an entry like this:

    1 <CollectionMappings>

    2   <CollectionMappingTypeName="System.Collections.Generic.List`1"Category="List" />

    3 </CollectionMappings>

If you change the TypeName attribute from System.Collections.Generic.List`1 to System.Collections.ObjectModel.ObservableCollection`1 the service proxy generation tool will use ObservableCollections in the next update. And the configuration editor will just show a (Custom) in the drop-down, so you can still use it to modify the other settings.

Currently rated 5.0 by 2 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Henning Krause on Sunday, August 12, 2007 6:00 PM, last modified on Tuesday, August 14, 2007 6:00 AM
Permalink | Comments (2) | Post RSSRSS comment feed

Building SQL Server 2000 compatible scripts with the SQL Server Management Studio

A very nice feature was added to the SQL Management Studio with SQL Server 2005 Service Pack 2: The ability to generate scripts which are compatible with SQL Server 2000!

To enable this option, open the options dialog (Tools --> Options) and select the Scripting node on the left. Click on Script for server version and select either SQL Server 2000 or 2005.


Options dialog from the SQL Server Management Studio (click to enlarge)

 

Currently rated 5.0 by 1 people

  • Currently 5/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Posted by Henning Krause on Sunday, July 22, 2007 6:00 PM, last modified on Monday, July 23, 2007 6:00 AM
Permalink | Comments (0) | Post RSSRSS comment feed