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.5 by 2 people

  • Currently 4.5/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

Exchange WebServices Bug with Lineendings

When you fetch the body of a mail via Exchange WebServices you might have noticed the fact that the body of the mail has its line endings, which are normally represented by a CRLF (“\r\n” in C#) replaced with a simple LF (“\n”). For the text and HTML body, this is the expected behavior. But Exchange also enforces this behavior on custom properties, which was not intended.

Below is a small program that demonstrates the problem. It creates a new item in the drafts folder of the current users mailbox and then reads the content back. You’ll notice the missing \r characters when you examine the propertyValue property.

   1: using System;
   2: using System.Linq;
   3: using System.Net;
   4: using Microsoft.Exchange.WebServices.Data;
   5:  
   6: namespace TestApplication
   7: {
   8:     internal class Program
   9:     {
  10:         private static void Main()
  11:         {
  12:             ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
  13:             var service = new ExchangeService(ExchangeVersion.Exchange2007_SP1)
  14:                           {
  15:                           Url = new Uri("https://w2k3x64/ews/exchange.asmx"),
  16:                           UseDefaultCredentials = true,
  17:                           };
  18:             Folder folder = Folder.Bind(service, WellKnownFolderName.Drafts);
  19:             var item = new PostItem(service) {Subject = "test", Body = "Line1\r\nLine2"};
  20:  
  21:             var definition = new ExtendedPropertyDefinition(DefaultExtendedPropertySet.PublicStrings, "TestProperty",
  22:                                                             MapiPropertyType.String);
  23:             item.ExtendedProperties.Add(definition, "Line1\r\nLine2");
  24:             item.Save(folder.Id);
  25:  
  26:             ItemId id = item.Id;
  27:             item = PostItem.Bind(service, id, new PropertySet(BasePropertySet.FirstClassProperties, definition));
  28:             string propertyValue = (from property in item.ExtendedProperties 
  29:                                     where property.PropertyDefinition == definition 
  30:                                     select property.Value).First();
  31:  
  32:             Console.Out.WriteLine("item.Body = {0}", item.Body);
  33:             Console.Out.WriteLine("propertyValue = {0}", propertyValue);
  34:  
  35:             Console.ReadLine();
  36:         }
  37:     }
  38: }

So, if you are struggling with this ‘anomaly’, you need to open a case with the Microsoft products support (PSS) to get a fix.

Currently rated 5.0 by 1 people

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

Posted by Henning Krause on Tuesday, August 25, 2009 3:04 PM, last modified on Tuesday, August 25, 2009 3:04 PM
Permalink | Comments (0) | Post RSSRSS comment feed

InfiniTec.DirectoryServices now on CodePlex

I’ve just created my second CodePlex project (the first one being my PushNotification Lister on http://exchangenotification.codeplex.com/). It’s the DirectoryServices.Protocols wrapper I created a while ago. The last version was still based on my old InfiniTec.Threading library which is seriously broken. The new one runs rather flawlessly and is very stable.

I will add example programs soon, I hope.

Be the first to rate this post

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

Posted by Henning Krause on Tuesday, August 11, 2009 3:14 PM, last modified on Tuesday, August 11, 2009 3:14 PM
Permalink | Comments (0) | Post RSSRSS comment feed

Troubleshooting Push notifications failures

It can sometimes be tricky to get Exchange push notifications working. The development of a listener is only part of the solution. The other part is getting the notifications from the Exchange server to the listener, especially when the listener expects the notifications on a secure channel. Exchange is of no particular help here. The only error message that is generated when Exchange can’t send a notification message to a listener is something like this:

image

This message is not helpful at all. Ok – Exchange could not send a notification a specific subscription. But why?

That’s why I wrote a small utility that should be able to diagnose the three most common problems:

  • The Exchange server cannot reach the notification listener because a firewall is blocking the access.
  • The host specified in the callback address can not be resolved to an IP address
  • If the callback address of the listener is secured using TLS/SSL, Exchange will not send notifications if the server certificate used by the listener is not trusted.

Instructions:

Copy the program on the Exchange server. Open a command prompt and start the program. It expects the address of the listener as the first parameter. The execution of the program can take up to one minute.

Download the program here:

   (105kb)

The source code can be downloaded from CodePlex.

Currently rated 4.0 by 1 people

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

Posted by Henning Krause on Saturday, June 20, 2009 6:14 PM, last modified on Sunday, June 21, 2009 4:59 AM
Permalink | Comments (0) | Post RSSRSS comment feed

Least privilege with Windows Services

Some actions in Windows require the logged on account to have certain privileges. A good explanation about these special permissions can be found on MSDN. The privilege assignment can viewed and modified through either Group Policy (on a domain) or the Local Security Policy (secpol.msc).

image

The privileges that an account has on a certain application can be monitored with the Process Explorer from Microsoft (former SysInternals). Here is an example of my explorer.exe:

image

The explorer has a number of privileges, bust most of them are disabled. The SeChangeNotifyPrivilege (which is called "Bypass traverse checking”) is the only one that is enabled by default. All other must be explicitly enabled before actions can be executed which are guarded by the respective privilege. But they also can be disabled for the lifetime of the process. Once turned off, they can’t be turned on again unless the process is restarted.

A rather important privilege, at least for WCF hosts and web server is the “Impersonate a client after authentication”.

Depending on the account your service and/or program is running, the account may have a number of privileges that are not required by your application or service. Especially since there are very mighty privileges such as the “Debug programs” or “Create global objects”. To comply with a least-privilege account, it is a good practice to turn off unnecessary privileges. This is especially true for services or applications which either utilize addins or exchange data over the network.

Unfortunately, there is no managed API available, but the required interop is not very hard. I have wrapped the whole stuff and it’s available from my InfiniTec.Common library. Currently, the latest version is available from CodePlex (its part of my Exchange Push Notification component. Just get the latest build or download the source from the repository). After you have added a reference to your service, it’s a matter of three lines of code to disable all expendable privileges:

   1: var priviliges = new PrivilegeCollection();
   2: priviliges.Refresh();
   3: priviliges.ReducePrivilegesToMinimum();

This call removes all unnecessary priviliges from the current process token. If you are hosting WCF services in your application and need to impersonate the caller, you should leave the “Impersonate a client after authentication” privilege intact. This requirement changes the code to the following three lines of code:

   1: var priviliges = new PrivilegeCollection();
   2: priviliges.Refresh();
   3: priviliges.ReducePrivilegesToMinimum(Privilege.Impersonate);

The InfiniTec.Security.Privilege class contains the internal names of most of the privileges in Windows, and the ReducePrivilegesToMinimum takes a string array as its first parameter, so it is very easy to keep multiple privileges intact.

Enabling a required privilege

It is not enough just to have a certain privilege to execute a secured operation. For example, to shutdown a computer it is not enough to have the "Shutdown the Computer” privilege. A program must explicitly enable it to call the InitiateShutdown function. This can also be easily achieved by executing these lines of code:

   1: var priviliges = new PrivilegeCollection();
   2: priviliges.Refresh();
   3:  
   4: var privilege = priviliges[Privilege.Shutdown];
   5: privilege.Enable();

Be the first to rate this post

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

Posted by Henning Krause on Tuesday, June 16, 2009 10:23 AM, last modified on Tuesday, June 16, 2009 10:23 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

Custom Forms with Outlook Web Access 2007

In my first article about custom forms for Outlook WebAccess I wrote about the difficulties that are associated with the development of custom forms. Thanks to a hint from reader Ciusso I was able to come up with another solutions that makes the whole stuff a lot easier. Here is a walkthrough to create custom forms the easy way – with support for standard postback, AJAX… the whole enchilada. As a prerequisite, download the EWS Managed API. It’s much simpler to use than the Exchange Web Services.

First, start with a new WebProject. If you are developing directly on an Exchange 2007 server (which is hopefully a test machine and not production – don’t do that!), the new solution can be created directly under the directory for custom OWA forms: %ProgramFiles%\Microsoft\Exchange Server\ClientAccess\Owa\forms. This simplifies development because no files have to be deployed after each build.

image

The problems I outlined in the first article are caused by an HTTP module, which is defined in the web.config file of Outlook WebAccess. The web.config file of Outlook Web Access is located in the folder %ProgramFiles%\Microsoft\Exchange Server\ClientAccess\Owa:

image

When you open the web.config file, you’ll see that Outlook Web Access adds two assemblies, one HTTP module and two HTTP handlers.

 image

Since the custom form is located beneath the OWA folder, it inherits all its configuration settings from the Outlook Web Access configuration. The trick is to remove the artifacts introduced  by Outlook Web Access. For the HTTP module and the two HTTP handlers this is an easy task: As shown in the next picture, just add “remove” tags for each artifact.

image

Things get more complicated with the assemblies, however. The two assemblies can not be removed via “remove tags”. It seems to be necessary to clear the entire collection of assemblies with a “clear” tag. But this has a nasty side effect: The assembly of the custom form is also removed and needs to be re-added.

image

The next step is to configure the web application in the IIS Manager: Open the IIS snapin and navigate to the owa virtual directory and then select the directory of the custom form:

image

Open the properties of the CustomForm directory configure a Web Applicaton by clicking on the “Create” button. Ensure that the application pool is MSExchangeOWAAppPool.

image

Next, ensure that the ASP.NET version of the application is set to 2.0:

image

Close the dialog and return to Visual Studio.

The custom form will not be loaded by Outlook Web Access until it finds a registry.xml describing a mapping from an item class to the custom forms. Here is the registry.xml from this example:

   1: <Registry xmlns="http://schemas.microsoft.com/exchange/2004/02/formsregistry.xsd" Name="PremiumExtensions" InheritsFrom="Premium" IsRichClient="true">
   2:   <Experience Name="Premium">
   3:     <Client Application="MSIE" MinimumVersion="6" Platform="Windows NT" />
   4:     <Client Application="MSIE" MinimumVersion="6" Platform="Windows 2000" />
   5:     <Client Application="MSIE" MinimumVersion="6" Platform="Windows 98; Win 9x 4.90" />
   6:     <ApplicationElement Name="Item">
   7:       <ElementClass Value="IPM.Note.CustomClass">
   8:         <Mapping Action="New" Form="https://w2k3x64/owa/forms/CustomForm/EditItem.aspx"/>
   9:         <Mapping Action="Open" Form="https://w2k3x64/owa/forms/CustomForm/EditItem.aspx"/>
  10:         <Mapping Action="Preview" Form="https://w2k3x64/owa/forms/CustomForm/ViewItem.aspx"/>
  11:         <Mapping Action="Reply" Form="https://w2k3x64/owa/forms/CustomForm/EditItem.aspx"/>
  12:         <Mapping Action="ReplyAll" Form="https://w2k3x64/owa/forms/CustomForm/EditItem.aspx"/>
  13:         <Mapping Action="Forward" Form="https://w2k3x64/owa/forms/CustomForm/EditItem.aspx"/>
  14:       </ElementClass>
  15:     </ApplicationElement>
  16:     <ApplicationElement Name="PreFormAction">
  17:       <ElementClass Value="IPM.Note.CustomClass">
  18:         <Mapping Action="Open" Form="Microsoft.Exchange.Clients.Owa.Premium.Controls.CustomFormRedirectPreFormAction,Microsoft.Exchange.Clients.Owa"/>
  19:         <Mapping Action="Preview" Form="Microsoft.Exchange.Clients.Owa.Premium.Controls.CustomFormRedirectPreFormAction,Microsoft.Exchange.Clients.Owa"/>
  20:         <Mapping Action="Reply" Form="Microsoft.Exchange.Clients.Owa.Premium.Controls.CustomFormRedirectPreFormAction,Microsoft.Exchange.Clients.Owa"/>
  21:         <Mapping Action="ReplyAll" Form="Microsoft.Exchange.Clients.Owa.Premium.Controls.CustomFormRedirectPreFormAction,Microsoft.Exchange.Clients.Owa"/>
  22:         <Mapping Action="Forward" Form="Microsoft.Exchange.Clients.Owa.Premium.Controls.CustomFormRedirectPreFormAction,Microsoft.Exchange.Clients.Owa"/>
  23:         <Mapping Action="New" Form="Microsoft.Exchange.Clients.Owa.Premium.Controls.CustomFormRedirectPreFormAction,Microsoft.Exchange.Clients.Owa"/>
  24:       </ElementClass>
  25:     </ApplicationElement>
  26:   </Experience>
  27: </Registry>

The mapping above is for the message class “IPM.Note.CustomClass”. It seems that it’s necessary to specify a full-qualified name for the address of the formulars – relative path will not work.

Now, add a reference to the Managed API and the System.DirectoryServices.AccountManagement. Create a new form named “ViewItem.aspx”.

For the sake of simplicity, just add this fragment to the page below the scriptmanager reference:

   1: <div>
   2:     <asp:Label runat="server" ID="Label1" Text="Subject: " />
   3:     <asp:Label runat="server" ID="SubjectLabel" Text="Subject" />
   4: </div>

In the code behind file, add the following code:

   1: using System;
   2: using System.DirectoryServices.AccountManagement;
   3: using System.Net;
   4: using System.Security.Principal;
   5: using System.Web;
   6: using System.Web.UI;
   7: using Microsoft.Exchange.WebServices.Data;
   8:  
   9: namespace CustomForm
  10: {
  11:     public partial class ViewItem : Page
  12:     {
  13:         static ViewItem()
  14:         {
  15:             ServicePointManager.ServerCertificateValidationCallback = (sender, certificate, chain, errors) => true;
  16:         }
  17:  
  18:         protected string OwaItemId
  19:         {
  20:             get { return HttpUtility.UrlEncode(Request["id"]); }
  21:         }
  22:  
  23:         protected string ContentClass
  24:         {
  25:             get { return Request["t"]; }
  26:         }
  27:  
  28:         protected string State
  29:         {
  30:             get { return Request["s"]; }
  31:         }
  32:  
  33:         protected string Action
  34:         {
  35:             get { return Request["a"]; }
  36:         }
  37:  
  38:         protected void Page_Load(object sender, EventArgs e)
  39:         {
  40:             Item item = LoadItem();
  41:             SubjectLabel.Text = item.Subject;
  42:         }
  43:  
  44:         private Item LoadItem()
  45:         {
  46:             using (((WindowsIdentity) HttpContext.Current.User.Identity).Impersonate())
  47:             {
  48:                 string emailAddress = UserPrincipal.Current.EmailAddress;
  49:                 var exchangeService = new ExchangeService(ExchangeVersion.Exchange2007_SP1)
  50:                                           {
  51:                                               Url = new Uri("https://localhost/ews/exchange.asmx"),
  52:                                               UseDefaultCredentials = true
  53:                                           };
  54:                 ServiceResponseCollection<ConvertIdResponse> convertIds =
  55:                     exchangeService.ConvertIds(new[] {new AlternateId(IdFormat.OwaId, OwaItemId, emailAddress)}, 
  56:                         IdFormat.EwsId);
  57:                 ServiceResponseCollection<GetItemResponse> items =
  58:                     exchangeService.BindToItems(new[] {new ItemId(((AlternateId) convertIds[0].ConvertedId).UniqueId)},
  59:                                                 new PropertySet(BasePropertySet.FirstClassProperties));
  60:                 return items[0].Item;
  61:             }
  62:         }
  63:     }
  64: }

In the static constructor, SSL certificate checking is effectively disabled. Since this example binds to the Exchange Server with the hostname "localhost”, certificate would fail regardless of the certificate used.

All the heavy lifting is done in the LoadItems method. In the first line the code impersonates the user logged on to OWA. The next step requires a prerequisite: The website gets the OWA id of the item. this id cannot directly be used with the Exchange Web Services (or the EWS Managed API, for that matter). The ExchangeService.ConvertIds method is used here. But that method requires the primary email address of the user the OWA id belongs to. For simple environments (read: the mail attribute of the user object in Active Directory contains the primary email address), the method used here can be used. The System.DirectoryServices.AccountManagement.UserPrincipal class was introduced with .NET 3.5 and offers a very simple way to get the required information. After the ID has been converted to an EWS id, the item can finally be loaded from the store. In this example, only the subject of the item is displayed on the form.

Finally, you need to restart the IIS – contrary to what to official documentation states, Outlook Web Access only scans for new custom forms on startup and not periodically. You need to keep this in mind when deploying your solution. After the IIS has been restarted, you can check wether the custom form has been loaded by taking a look at the EventLog. If the form was loaded successfully, you’ll find an entry similar to this one:

image

The form created here will be used to preview every item with the message class of IPM.Post.Custom.

The solution containing the relevant files for this example is attached to this post. The file has a digital signature to ensure it’s not modified.

Currently rated 5.0 by 1 people

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

Posted by Henning Krause on Friday, June 12, 2009 5:31 PM, last modified on Friday, June 12, 2009 5:33 PM
Permalink | Comments (2) | Post RSSRSS comment feed

Getting the body of an Email with a FindItems request

The FindItem operation (or the corresponding ExchangeService.FindItems method) does not return the body of an email by default. And when trying to explicitly request them via a custom propertyset, the call fails. Consider this method, which uses the EWS Managed API to execute a FindItems method to get every message from the inbox folder of a mailbox, fetching only the item id and the body:

   1: private static void GetAllItems(ExchangeService exchangeService)
   2: {
   3:     var offset = 0;
   4:     const int pageSize = 100;
   5:  
   6:     FindItemsResults<Item> result;
   7:     do
   8:     {
   9:         var view = new ItemView(pageSize, offset)
  10:                        {
  11:                            PropertySet = new PropertySet(BasePropertySet.IdOnly)
  12:                                              {
  13:                                                  ItemSchema.Body
  14:                                              }
  15:                        };
  16:  
  17:         result = exchangeService.FindItems(WellKnownFolderName.Inbox, view);
  18:  
  19:         foreach (var item in result)
  20:         {
  21:             ProcessItem(item);
  22:         }
  23:         offset += pageSize;
  24:     } while (result.MoreAvailable);
  25: }

When executed, the ExchangeService instance throws a ServiceValidationException stating “The property Body cannot be used in FindItem requests”. The official workaround proposed by Microsoft is to use a FindItem request to get the item ids of the items in a folder and afterward issue a GetItem request containing all the item ids and request the body property. However, there is another solution: The body properties can be fetched by requesting the MAPI properties containing the body:

   1: private static ExtendedPropertyDefinition TextBodyProperty = new ExtendedPropertyDefinition(0x1000, MapiPropertyType.String);
   2: private static ExtendedPropertyDefinition HtmlBodyProperty = new ExtendedPropertyDefinition(0x1013, MapiPropertyType.Binary);

The first property definition can be used to fetch the text body of a mail. The second one fetches the Html body. The new GetAllItemsMethod now looks like this:

   1: private static void GetAllItems(ExchangeService exchangeService)
   2: {
   3:     var offset = 0;
   4:     const int pageSize = 100;
   5:  
   6:     FindItemsResults<Item> result;
   7:     do
   8:     {
   9:         var view = new ItemView(pageSize, offset)
  10:                        {
  11:                            PropertySet = new PropertySet(BasePropertySet.IdOnly)
  12:                                              {
  13:                                                  HtmlBodyProperty
  14:                                              }
  15:                        };
  16:  
  17:         result = exchangeService.FindItems(WellKnownFolderName.Inbox, view);
  18:  
  19:         foreach (var item in result)
  20:         {
  21:             object body;
  22:             if (item.ExtendedProperties.TryGetValue(HtmlBodyProperty, out body))
  23:             {
  24:                 Console.Out.WriteLine("item.Body = {0}", Encoding.UTF8.GetString(Convert.FromBase64String((string)body)));
  25:             }
  26:         }
  27:         offset += pageSize;
  28:     } while (result.MoreAvailable);
  29: }

The HtmlBody is requested with the addtion of the HtmlBodyProperty to the ItemView in line 13. Since the Html body is stored in binary form and returned in Base64 encoded format, it needs to be decoded before it can be displayed. This is done in line 24. If the plaintext body is requested, the value of the body property in line 24 can simply be converted to a string.

Currently rated 5.0 by 1 people

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

Posted by Henning Krause on Tuesday, June 09, 2009 3:00 PM, last modified on Tuesday, June 09, 2009 3:00 PM
Permalink | Comments (2) | Post RSSRSS comment feed