Monday, December 17, 2012

Outlook 2007 addin. Distribution list based on AD look up.

Hello there! Today I'm going to describe my recent small project. The goal was simple: provide colleagues with a flexible way of creation of email distribution lists. Something like outlook groups but with possibility of excluding particular users from the groups. Useful thing if you want to notify the group of people about someone's birthday. Of course you don't want the guy whose name-day is celebrated be in the list.

So I scanned our company Active Directory with ADExporer and found all the details I need to build a small console application. I decided to use builder pattern and keep distribution list building logic in a separate assembly. Here is the class diagram:



And here is the corresponding code:

namespace DLExt.Builder
{
    public class AddressBuilder
    {
        private readonly IList<DirectoryEntry> containersToSearch;
        private readonly IList<Location> locationsToSearch;
        private readonly List<Person> extractedPersons;
        private readonly IList<Person> personsToExclude;
        private readonly List<Person> filteredPersons;

        public string Server { get; private set; }

        public string ResultAddress { get; private set; }

        public AddressBuilder(string server, IEnumerable<Location> locations, IEnumerable<Person> personsToExclude)
        {
            Server = server;
            locationsToSearch = locations.ToList();
            this.personsToExclude = personsToExclude.ToList();

            containersToSearch = new List<DirectoryEntry>();
            extractedPersons = new List<Person>();
            filteredPersons = new List<Person>();
            ResultAddress = string.Empty;
        }

        public void Build()
        {
            try
            {
                LocateContainersToSearch(locationsToSearch);
                GetPersons();
                ExcludePersons();
                BuildDistributionList();
            }
            finally
            {
                FinalizeBuilder();
            }
        }

        protected virtual void LocateContainersToSearch(IList<Location> locations)
        {
            foreach (Location location in locations)
            {
                containersToSearch.Add(new DirectoryEntry(string.Format("LDAP://{0}/{1},{2}", Server, "OU=Users", location.Path)));
            }
        }

        protected virtual void GetPersons()
        {
            foreach (DirectoryEntry container in containersToSearch)
            {
                using (var directorySearcher = new DirectorySearcher(container, "(objectCategory=Person)"))
                {
                    SearchResultCollection results = directorySearcher.FindAll();
                    if (results.Count == 0)
                    {
                        return;
                    }

                    extractedPersons.AddRange((from SearchResult result in results
                                select new Person(
                                    result.Properties["displayName"][0].ToString(), 
                                    result.Properties["mail"][0].ToString())).ToList());
                }
            }
        }

        protected virtual void ExcludePersons()
        {
            filteredPersons.AddRange(extractedPersons.Except(personsToExclude, new PersonEqualityComparer()));
        }

        protected virtual void BuildDistributionList()
        {
            var builder = new StringBuilder();
            foreach (Person person in filteredPersons)
            {
                builder.Append(person.Email).Append(';');
            }

            ResultAddress = builder.ToString();
        }

        protected virtual void FinalizeBuilder()
        {
            foreach (DirectoryEntry container in containersToSearch)
            {
                container.Dispose();
            }
        }
    }
}

namespace DLExt.Builder.Retrievers
{
    public class PersonsRetriever : IRetriever<Person>
    {
        public string Server { get; private set; }

        public PersonsRetriever(string server)
        {
            Server = server;
        }

        public IList<Person> Retrieve(string path)
        {
            var result = new List<Person>();
            try
            {
                using (DirectoryEntry entry = new DirectoryEntry(string.Format("LDAP://{0}/{1}", Server, path)))
                {
                    using (DirectorySearcher searcher = new DirectorySearcher(entry, "(objectCategory=Person)"))
                    {
                        SearchResultCollection locations = searcher.FindAll();
                        if (locations.Count == 0)
                        {
                            return result;
                        }

                        result.AddRange(from SearchResult location in locations
                                        orderby location.Properties["displayName"][0].ToString()
                                        select new Person(
                                            location.Properties["displayName"][0].ToString(),
                                            location.Properties["mail"][0].ToString()));
                    }
                }
            }
            catch
            {
            }

            return result;
        }
    }
}

namespace DLExt.Builder.Retrievers
{
    public class LocationsRetriever :IRetriever<Location>
    {
        public string Server { get; private set; }

        public LocationsRetriever(string server)
        {
            Server = server;
        }

        public virtual IList<Location> Retrieve(string path)
        {
            var result = new List<Location>();
            try
            {
                using (DirectoryEntry entry = new DirectoryEntry(string.Format("LDAP://{0}/{1}", Server, path)))
                {
                    using (DirectorySearcher searcher = new DirectorySearcher(entry, "(objectClass=organizationalUnit)") { SearchScope = SearchScope.OneLevel })
                    {
                        SearchResultCollection locations = searcher.FindAll();
                        if (locations.Count == 0)
                        {
                            return result;
                        }

                        result.AddRange(from SearchResult location in locations 
                                        select new Location(
                                            location.Properties["name"][0].ToString(), 
                                            location.Properties["distinguishedName"][0].ToString()));
                    }
                }
            }
            catch
            {
            }

            return result;
        }
    }
}

After the searching logic was implemented and the console application was finished I decided to move further and build the UI. One option was to create a small tray WPF application. But who wants to run an additional application for such a minor task? I guess nobody. So I decided to build an Outlook 2007 addin. After some searching I found two great articles on MSDN and Codeproject. I was very excited when discovered that I can use WPF for achieving my goal. Here is a code I got:

using System;
using System.Windows.Forms;
using Microsoft.Office.Core;

namespace DLExt.OutlookAddin
{
    public partial class ThisAddIn
    {
        static readonly Timer Timer = new Timer();
        
        private void ThisAddIn_Startup(object sender, EventArgs e)
        {
            // HACK: looks like it is the only way to deal with minimized outlook startup
            Timer.Interval = 100;
            Timer.Tick += (o, args) =>
                              {
                                  var activeExplorer = Application.ActiveExplorer();
                                  if (activeExplorer != null)
                                  {
                                      Timer.Stop();
                                      CreateToolbar();
                                  }
                              };
            Timer.Start();
        }

        private void ThisAddIn_Shutdown(object sender, EventArgs e)
        {
            RemoveToolbar();
        }

        #region VSTO generated code

        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InternalStartup()
        {
            this.Startup += new System.EventHandler(ThisAddIn_Startup);
            this.Shutdown += new System.EventHandler(ThisAddIn_Shutdown);
        }
        
        #endregion

        private const string MenuToolbarTag = "Distribution List Addin";
        private CommandBar toolBar;
        private CommandBarButton toolBarButton;

        private CommandBar FindBar()
        {
            var activeExplorer = Application.ActiveExplorer();
            return activeExplorer != null
                       ? (CommandBar) activeExplorer.CommandBars.FindControl(missing, missing, MenuToolbarTag, true)
                       : null;
        }

        private void RemoveToolbar()
        {
            var commandBar = FindBar();
            if (commandBar != null)
            {
                commandBar.Delete();
            }
        }

        private void CreateToolbar()
        {
            try
            {
                toolBar = FindBar() 
                    ?? Application.ActiveExplorer().CommandBars.Add(MenuToolbarTag, MsoBarPosition.msoBarTop, false, true);

                toolBarButton = (CommandBarButton)toolBar.Controls.Add(MsoControlType.msoControlButton, missing, missing, 1, true);
                toolBarButton.Style = MsoButtonStyle.msoButtonIconAndCaption;
                toolBarButton.Caption = "Generate Distribution List";
                toolBarButton.FaceId = 65;
                toolBarButton.Tag = MenuToolbarTag;
                toolBarButton.Click += (CommandBarButton ctrl, ref bool @default) =>
                {
                    MainWindow window = new MainWindow(
                        "controller",
                        "OU=Sites,OU=Company,DC=domain,DC=corp",
                        "OU=Sites,OU=Company,DC=domain,DC=corp");
                    window.Show();
                };
            }
            catch (Exception ex)
            {
                MessageBox.Show("Error: " + ex.Message, "Error Message");
            }
        } 
    }
}

using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Windows;
using DLExt.Builder;
using DLExt.Builder.Model;
using DLExt.Builder.Retrievers;
using Microsoft.Office.Interop.Outlook;

namespace DLExt.OutlookAddin
{
    public partial class MainWindow : INotifyPropertyChanged
    {
        private readonly BackgroundWorker loadingWorker;
        private readonly BackgroundWorker composeWorker;
        private AddressBuilder builder;
        private IList<Location> locationsList;
        private IList<Person> personsList;

        public event PropertyChangedEventHandler PropertyChanged;

        public string Server { get; private set; }

        public string LocationsRootPath { get; private set; }

        public string PersonsRootPath { get; private set; }

        private bool isProcessing;

        public bool IsProcessing
        {
            get { return isProcessing; }
            set
            {
                isProcessing = value;
                PropertyChanged(this, new PropertyChangedEventArgs("IsProcessing"));
            }
        }

        public MainWindow(string server, string locationsRootPath, string personsRootPath)
        {
            Server = server;
            LocationsRootPath = locationsRootPath;
            PersonsRootPath = personsRootPath;

            loadingWorker = new BackgroundWorker();
            loadingWorker.DoWork += (o, args) =>
            {
                IsProcessing = true;
                locationsList = new LocationsRetriever(Server).Retrieve(LocationsRootPath);
                personsList = new PersonsRetriever(Server).Retrieve(PersonsRootPath);
            };

            loadingWorker.RunWorkerCompleted += (sender, args) =>
            {
                locations.ItemsSource = locationsList;
                persons.ItemsSource = personsList;
                IsProcessing = false;
            };

            composeWorker = new BackgroundWorker();
            composeWorker.DoWork += (sender, args) =>
            {
                IsProcessing = true;
                builder = new AddressBuilder(
                    Server,
                    locations.Items.OfType<Location>().Where(loc => loc.IsSelected),
                    personsToExclude.Items.OfType<Person>());
                builder.Build();
            };

            composeWorker.RunWorkerCompleted += (sender, args) =>
            {
                IsProcessing = false;
                try
                {
                    var app = new Microsoft.Office.Interop.Outlook.Application();
                    var mailItem = (MailItem)app.CreateItem(OlItemType.olMailItem);

                    mailItem.To = builder.ResultAddress;
                    mailItem.Display(true);

                }
                catch (COMException)
                {
                }
            };

            loadingWorker.WorkerSupportsCancellation = true;
            DataContext = this;
            InitializeComponent();
        }

        private void WindowLoaded(object sender, RoutedEventArgs e)
        {
            loadingWorker.RunWorkerAsync();
        }

        private void ComposeEmail(object sender, RoutedEventArgs e)
        {
            composeWorker.RunWorkerAsync();
        }

        private void ExcludePerson(object sender, RoutedEventArgs e)
        {
            if (!personsToExclude.Items.Contains(persons.SelectedItem))
            {
                personsToExclude.Items.Add(persons.SelectedItem);
            }
        }

        private void CloseForm(object sender, RoutedEventArgs e)
        {
            Close();
        }

        private void WindowClosing(object sender, CancelEventArgs e)
        {
            if (loadingWorker.IsBusy)
            {
                loadingWorker.CancelAsync();
            }
        }
    }
}

The UI was completed in a first approximation. The main thing I wasn't satisfied with was execution of LDAP requests in the same thread as UI. So the next step was implementing multithreading with background worker.

public MainWindow(string server, string locationsRootPath, string personsRootPath)
{

 ...            

 loadingWorker = new BackgroundWorker();
 loadingWorker.DoWork += (o, args) =>
 {
  IsProcessing = true;
  locationsList = new LocationsRetriever(Server).Retrieve(LocationsRootPath);
  personsList = new PersonsRetriever(Server).Retrieve(PersonsRootPath);
 };

 loadingWorker.RunWorkerCompleted += (sender, args) =>
 {
  locations.ItemsSource = locationsList;
  persons.ItemsSource = personsList;
  IsProcessing = false;
 };


 loadingWorker.WorkerSupportsCancellation = true;

 ...

 InitializeComponent();
}

 ...

private void WindowLoaded(object sender, RoutedEventArgs e)
{
 loadingWorker.RunWorkerAsync();
}

The application was completed but unfortunately it was crushing during Outlook start up. After some research I found the problem was the absence of ActiveExplorer instance when outlook started minimized. No UI - no ActiceExplorer instance. Sounds fair but how to fix it? I had to recognize that the hack with a timer was the best solution for me.

private void ThisAddIn_Startup(object sender, EventArgs e)
{
 // HACK: looks like it is the only way to deal with minimized outlook startup
 Timer.Interval = 100;
 Timer.Tick += (o, args) =>
  {
    var activeExplorer = Application.ActiveExplorer();
    if (activeExplorer != null)
    {
     Timer.Stop();
     CreateToolbar();
    }
  };
 Timer.Start();
}

The app was completed this time. But I understood that I need to provide a way for easy distribution of the addin. So I decided to build a setup project to handle all prerequisites and registry stuff. The hardest part here was to find this great article for excel plugins distribution. I followed it to create a setup project for my outlook plugin.

Final sources can be found here.

Friday, November 9, 2012

Serialization of entities (Entity Framework)

I want to tell you about interesting issue I faced when tried to serialize an entity in Microsoft Entity Framework.

I'm going to use Northwind database for demonstration purposes. Suppose you've added the Northwind model to your solution. And you are interested in three entity classes: Customer, Order and Order_Detail. Here is the corresponding diagram:
 You want to serialize particular customer with all his orders and order details. Consider the following code:

using System.Data.Objects;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Xml;
using System.Xml.Serialization;
using Serializing;

namespace EntitiesSerialization
{
    class Program
    {
        static void Main()
        {
            using (var context = new SerializingContext())
            {
                context.ContextOptions.LazyLoadingEnabled = false;
                var query = context.Customers.Include("Orders.Order_Details");
                var customer = query.Where("it.CustomerID = @Id", new ObjectParameter("Id", "ALFKI")).First();                
                var serializer = new XmlSerializer(typeof (Customers), new[] {typeof(Orders), typeof(Order_Details)});
                serializer.Serialize(new XmlTextWriter("test.xml", Encoding.UTF8), customer);                
            }
        }
    }
}

As you can see, I use XmlSerializer to serialize the customer to xml file. Just to make sure that LazyLoading doesn't affect the solution I've turned it off and included all entities explicitly. I pass Order and Order_Details types to XmlSerializer constructor as extraTypes. Unfortunatelly this solution won't work as it should. Result xml file contains only customer fields, no orders and order details are persisted:

<?xml version="1.0" encoding="utf-8"?>
<Customers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <EntityKey>
    <EntitySetName>Customers</EntitySetName>
    <EntityContainerName>SerializingContext</EntityContainerName>
    <EntityKeyValues>
      <EntityKeyMember>
        <Key>CustomerID</Key>
        <Value xsi:type="xsd:string">ALFKI</Value>
      </EntityKeyMember>
    </EntityKeyValues>
  </EntityKey>
  <CustomerID>ALFKI</CustomerID>
  <CompanyName>Alfreds Futterkiste</CompanyName>
  <ContactName>Maria Anders</ContactName>
  <ContactTitle>Sales Representative</ContactTitle>
  <Address>Obere Str. 57</Address>
  <City>Berlin</City>
  <PostalCode>12209</PostalCode>
  <Country>Germany</Country>
  <Phone>030-0074321</Phone>
  <Fax>030-0076545</Fax>
</Customers>

One way to make this work is described here. You can use DataContractSerializer to persist all the data to xml. Like this:

DataContractSerializer serializer = new DataContractSerializer(customer.GetType());
serializer.WriteObject(new XmlTextWriter("test.xml", Encoding.UTF8), customer);

Another option is to use binary formatter. In the following code I perform sequental serialization/deserialization of the customer object:

var binaryFormatter = new BinaryFormatter();
var stream = new MemoryStream();
binaryFormatter.Serialize(stream, customer);
stream.Position = 0;
var result = binaryFormatter.Deserialize(stream);

Thursday, November 1, 2012

Setting the value of new properties for old pages.

PageTypeBuilder is a great tool and I can't imagine developing against EPiServer CMS without it. But it has a well known issue: after a new property is added to page type old pages of this type have this property empty. It becomes worth if new property has required attribute set to true. You can't update old pages without filling this property.

In this post I want to share one possible solution. The idea is simple. Get all pages of certain type, find all required attributes with specified default value. Then iterate through these pages and set the required properties with their default values.

The first thing we need to do is to find all pages of certain type. It can be done using FindPagesWithCriteria method. To create search criteria we will need an ID of page type. So we have to use one of three overloads of PageType.Load method. Of course we can hardcode the guid or name, but more convenient way is to lookup this data. Here is what we've got for now:

using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using EPiServer;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAccess;
using EPiServer.Filters;
using PageTypeBuilder;

public class DefaultValuesSetter<TPageData> where TPageData : PageData
{        
 private const string PageTypeCriteriaName = "PageTypeID";

 private readonly PageType pageType;
 private readonly IList<PageDefinition> targetPageDefinitions;

 public DefaultValuesSetter()
 {
  pageType = GetPageType();
  targetPageDefinitions = GetRequiredDefinitionsWithDefaultValue();
 }

 public virtual IList<TPageData> GetPages()
 {
  if (pageType == null)
  {
   return new List<TPageData>();
  }

  var criteria = new PropertyCriteria
         {
          Condition = CompareCondition.Equal,
          Name = PageTypeCriteriaName,
          Type = PropertyDataType.PageType,
          Value = pageType.ID.ToString(CultureInfo.InvariantCulture),
          Required = true
         };  

  return DataFactory.Instance
   .FindPagesWithCriteria(PageReference.RootPage, new PropertyCriteriaCollection {criteria})
   .OfType<TPageData>()
   .ToList();
 } 

 private static PageType GetPageType()
 {
  var pageTypeId = PageTypeResolver.Instance.GetPageTypeID(typeof (TPageData));
  return pageTypeId.HasValue 
       ? PageType.Load(pageTypeId.Value) 
       : null;
 } 
}

The next thing to deal with is searching for property attributes. As I mentioned before we are interested in required properties with not empty default value. Here is the code:

private IList<PageDefinition> GetRequiredDefinitionsWithDefaultValue()
 {
  return pageType.Definitions
   .Where(def => def.Required && 
    def.DefaultValueType == DefaultValueType.Value && 
    def.DefaultValue != null)
   .ToList();
 }

Finally we need to iterate through all found pages and set their required properties to default values:

public virtual void SetDefaultValues(IList<TPageData> pages)
 {
  foreach (TPageData page in pages)
  {
   var pageData = page.CreateWritableClone();
   foreach (PageDefinition definition in targetPageDefinitions)
   {
    if (pageData[definition.Name] == null)
    {
     pageData[definition.Name] = definition.DefaultValue;
    }
   }

   try
   {
    DataFactory.Instance.Save(pageData, SaveAction.Publish);
   }
   catch (Exception exception)
   {
    // TODO: add logging here    
   }
  }
 }

We're done with infrastructure. Now suppose you've created a new page type class:

[PageType(Name = "TestPageType")]
public class TestPageType : TypedPageData
{
}

You've created a page of this type in CMS and decided to add a property to it:

[PageType(Name = "TestPageType")]
public class TestPageType : TypedPageData
{
 [PageTypeProperty(            
  EditCaption = "Test Property",            
  Type = typeof(PropertyString),
  Required = true,            
  DefaultValue = "My default value",
  DefaultValueType = DefaultValueType.Value)]
 public virtual string TestProperty { get; set; }        
}

To update the page you can use the following code:

var setter = new DefaultValuesSetter<TestPageType>();
var pages = setter.GetPages();
setter.SetDefaultValues(pages);

Happy coding!

Wednesday, October 24, 2012

Byte arrays instead of strings when retrieving custom attributes from Active Directory.

Few days ago when I was working with Active Directory I noticed a strange thing. I was quering AD for user with a custom string attribute. The results were being varied on different dev boxes. On one of them I was getting attribute's value as a string but on another - as a byte array.

When I set application pool Identity with the same AD account user identity on both dev boxes I get the same behavior. So I thought that one of AD accounts doesn't have enough access rights to get AD schema.

The goal was to retrieve a Guid from this attribute so I decided to implement a method which can handle all major cases. Here what I got:

public static Guid GuidFromADAttribute(object attribute)
{
    if (attribute is byte[])
    {
        var bytes = (byte[])attribute;
        switch (bytes.Length)
        {
            case 16:
                return new Guid(bytes);
            case 36:
                var stringRepresentation = Encoding.Default.GetString(bytes);
                Guid result;
                return Guid.TryParse(stringRepresentation, out result) ? result : default(Guid);
        }
    }
    else if (attribute is string)
    {
        Guid result;
        return Guid.TryParse((string) attribute, out result) ? result : default(Guid);
    }

    return default(Guid);
}

I kept the opportunity to retrieve "normal" Guid from AD. By "normal" I mean 16 bytes long. Another option here is to pass byte array representation of guid string (it must be 36 bytes long in order to be parsed). The final option is to pass a string representation of guid.

The bad thing is that I didn't find the cause of such a strange AD behavior. Here is the list of similar posts:

Friday, October 19, 2012

Add link between contact and organization entities in Mediachase ecommerce framework

Yesterday I was trying to assign a contact to organization in my code. It seemed really natural to had such a requirement but unfortunatelly I didn't find any examples.

In Mediachase.Commerce.Customers.CustomerContext class there is a method called InnerGetAllCustomerContactsInOrganization. It lists all Contact entities filtered by OwnerId field. So we can set link between this two entities in a following way:
myContact.OwnerId = myOrganization.PrimaryKeyId;
Hope this helps. 

Tuesday, September 25, 2012

Launching Vocabulary Extender!

Today I'm starting my new open source service. It is called "Vocabulary Extender". The main purpose of it is to help people to learn foreign languages. As you probably know one of the most important aspects of mastering the new language is to extend your vocabulary. The more words you remember - the better. It will help you on all levels - from elementary to advance.

But we often don't want to spend our time sitting with a dictionary remembering a bunch of words. More than that such approach is usually inefficient because of simple fact: when you don't use the word you forget it. So I've decided that the process of learning should be continuous. It means that one should be able to repeat the words during his/her working day without loss of productivity.

One possible way of achieving this is to use small desktop application that popups one time in a couple of minutes and prompts you to translate one random word. It offers you few possible answers so you need only to choose one. That's it, only one click per 10 minutes (or pick any interval that suits you).

The other important thing in learning is socializing. I think it is much more fun when you have an opportunity to share what you have learnt with others. When you create something your friends should be noticed that they can use it. With "Vocabulary Extenter" it is also possible. You can use your friend's vocabularies or create your own ones.

But enough words. Just see it in action on this video (only on Russian for now):

Wednesday, September 12, 2012

EPiServer dual Active Directory multiplexing role provider

The problem: when use EPiServer multiplexing role provider with more than one active directory, one can see groups only from first AD.

Let's examine EPiServer.Security.ActiveDirectoryRoleProvider class (EPiServer.dll). The key method for us here is:

public override string[] GetAllRoles()
{
    ICollection<DirectoryData> collection = 
        this._factory.FindAll("(objectClass=group)", SearchScope.Subtree, this._roleNameAttribute);
    if (collection == null)
    {
        return new string[0];
    }
    List<string> list = new List<string>();
    foreach (DirectoryData current in collection)
    {
        DirectoryData entry = this._factory.GetEntry(current.DistinguishedName);
        if (entry != null)
        {
            list.Add(entry[this._roleNameAttribute][0]);
        }
    }
    return list.ToArray();
}

So we get the groups with FindAll method. The actual type of this._factory is AdsiDataFactory so let's look into it's FindAll method:

public override IList<DirectoryData> FindAll(
    string filter, 
    SearchScope scope, 
    string sortByProperty)
{
    string text = "EPiServer:DirectoryServiceFindAll:" + filter + scope.ToString();
    IList<DirectoryData> list = (IList<DirectoryData>)HttpRuntime.Cache[text];
    if (list != null)
    {
        return list;
    }
 
 ...
}

These are only first lines of this method, but it is enough to understand what's wrong. This method caches the results of requests to AD with a key. The key consists of a filter and a scope. Both of these variables are the same for all ADs. So we will be getting the same value for all requests from cache.

The solution: we have to implement our own DirectoryDataFactory and reference it from our own ActiveDirectoryRoleProvider. We also should change the way the cacheKey is built. For example by adding the ConnectionString to it. Let's start with the ActiveDirectoryRoleProvider. Actually we don't need to implement it from scratch. We can subclass the ActiveDerectoryRoleProvider and set it's DirectoryDataFactory property in constructor. Like this:

public class MyOwnActiveDirectoryRoleProvider : EPiServer.Security.ActiveDirectoryRoleProvider
{
    public MyOwnActiveDirectoryRoleProvider()
    {
        DirectoryDataFactory = new MyOwnAdsiDataFactory();
    }
}

Now its time to deal with DirectoryDataFactory implementation. First thought is to override the FindAll method. But this method accesses a private field _propertiesToLoad that is shared between couple of other methods. So we have two options here:
1. Inherit new class from DirectoryDataFactory and implement all methods and properties.
2. Inherit new class from AdsiDataFactory. Replace the _propertiesToLoad field and override all methods that use it.

Personally I have chosen the second approach. So I've ended up with the following code:


public class MyOwnAdsiDataFactory : EPiServer.Security.AdsiDataFactory
{
 private const string CacheKeyEntryPrefix = "EPiServer:DirectoryServiceEntry:";
 private const string CacheKeyFindOnePrefix = "EPiServer:DirectoryServiceFindOne:";
 private const string CacheKeyFindAllPrefix = "EPiServer:DirectoryServiceFindAll:";
 private const string DistingushedNameAttribute = "distinguishedName";
 private const string ObjectClassAttribute = "objectClass";

 private List<string> propertiesToLoad;

 public MyOwnAdsiDataFactory()
 {
 }

 public MyOwnAdsiDataFactory(
  string connectionString,
  string username,
  string password,
  AuthenticationTypes connectionProtection,
  TimeSpan absoluteCacheTimeout) : base(connectionString, username, password, connectionProtection, absoluteCacheTimeout)
 {
  Initialize();
 }

 public override void Initialize(NameValueCollection config)
 {
  base.Initialize(config);
  Initialize();
 }

 public override void AddPropertyToLoad(string propertyName)
 {
  if (propertiesToLoad.Contains(propertyName))
  {
   return;
  }

  propertiesToLoad.Add(propertyName);
  ClearCache();
 }

 public override DirectoryData GetEntry(string distinguishedName)
 {
  string cacheKey = CacheKeyEntryPrefix + distinguishedName;
  DirectoryData directoryData = (DirectoryData)HttpRuntime.Cache[cacheKey];
  if (directoryData != null)
  {
   return directoryData;
  }

  using (DirectoryEntry directoryEntry = CreateDirectoryEntry(distinguishedName))
  {
   directoryData = CreateDirectoryDataFromDirectoryEntry(directoryEntry);
  }

  if (directoryData != null)
  {
   StoreInCache(cacheKey, directoryData);
  }

  return directoryData;
 }

 public override DirectoryData FindOne(string filter, SearchScope scope)
 {
  string cacheKey = new StringBuilder(CacheKeyFindOnePrefix)
   .Append(filter)
   .Append(scope)
   .ToString();

  DirectoryData directoryData = (DirectoryData)HttpRuntime.Cache[cacheKey];
  if (directoryData != null)
  {
   return directoryData;
  }

  using (DirectorySearcher directorySearcher =
   new DirectorySearcher(CreateDirectoryEntry(), filter, propertiesToLoad.ToArray(), scope))
  {
   directoryData = CreateDirectoryDataFromSearchResult(directorySearcher.FindOne());
   if (directoryData == null)
   {
    return null;
   }
  }

  StoreInCache(cacheKey, directoryData);
  return directoryData;
 }

 public override IList<DirectoryData> FindAll(string filter, SearchScope scope, string sortByProperty)
 {
  string cacheKey = new StringBuilder(CacheKeyFindAllPrefix)
   .Append(filter)
   .Append(scope)
   .Append(ConnectionString)
   .ToString();

  IList<DirectoryData> list = (IList<DirectoryData>) HttpRuntime.Cache[cacheKey];
  if (list != null)
  {
   return list;
  }

  using (DirectorySearcher directorySearcher = new DirectorySearcher(CreateDirectoryEntry(), filter, propertiesToLoad.ToArray(), scope))
  {
   directorySearcher.PageSize = PageSize;
   using (SearchResultCollection all = directorySearcher.FindAll())
   {
    if (sortByProperty == null)
    {
     list = new List<DirectoryData>(all.Count);
     foreach (SearchResult result in all)
     {
      list.Add(CreateDirectoryDataFromSearchResult(result));
     }
    }
    else
    {
     SortedList<string, DirectoryData> sortedList = new SortedList<string, DirectoryData>(all.Count);
     foreach (SearchResult result in all)
     {
      DirectoryData fromSearchResult = CreateDirectoryDataFromSearchResult(result);
      sortedList.Add(fromSearchResult.GetFirstPropertyValue(sortByProperty), fromSearchResult);
     }
     list = sortedList.Values;
    }
   }
  }

  StoreInCache(cacheKey, list);
  return list;
 }

 protected new DirectoryData CreateDirectoryDataFromDirectoryEntry(DirectoryEntry entry)
 {
  if (entry == null)
  {
   return null;
  }

  Dictionary<string, string[]> properties = new Dictionary<string, string[]>(propertiesToLoad.Count);
  foreach (string property in propertiesToLoad)
  {
   if (entry.Properties.Contains(property))
   {
    var propertyValueCollection = entry.Properties[property];
    var strArray = new string[propertyValueCollection.Count];
    for (int index = 0; index < propertyValueCollection.Count; ++index)
     strArray[index] = propertyValueCollection[index].ToString();
    properties.Add(property, strArray);
   }
  }

  return new DirectoryData(DistinguishedName(properties), entry.SchemaClassName, properties);
 }

 protected new DirectoryData CreateDirectoryDataFromSearchResult(SearchResult result)
 {
  if (result == null)
  {
   return null;
  }

  Dictionary<string, string[]> properties = new Dictionary<string, string[]>(propertiesToLoad.Count);
  foreach (string property in propertiesToLoad)
  {
   if (result.Properties.Contains(property))
   {
    var propertyValueCollection = result.Properties[property];
    var strArray = new string[propertyValueCollection.Count];
    for (int index = 0; index < propertyValueCollection.Count; ++index)
     strArray[index] = propertyValueCollection[index].ToString();
    properties.Add(property, strArray);
   }
  }

  return new DirectoryData(DistinguishedName(properties), SchemaClassName(properties), properties);
 }

 private void Initialize()
 {
  propertiesToLoad = new List<string>(5) { DistingushedNameAttribute, ObjectClassAttribute };
 }
}

Thursday, August 30, 2012

Integrating Autofac to IIS hosted WCF service application

In this post I'm going to describe the way I've integrated Autofac into my WCF IIS hosted service. While the process is thought to be quite common I've found few interesting aspects there.

First steps could be found at Autofac wikiAt the global application startup one should register the service and set the AutofacHostFactory.Container property. But where is the global application startup in case of WCF service hosted on IIS? Basically we have 3 ways to goI think the most natural for WCF is to create custom ServiceHostFactory where we can register our services.

The next step of integration process is to specify Autofac.Integration.Wcf.AutofacServiceHostFactory as  a factory in our service's svc file. But we've already decided to use our own factory. So we have to dig into autofac sources to try to combine these two factories. Here is the code I've ended up with:

namespace VX.Service
{
    public class ServiceHostFactory : AutofacHostFactory
    {
        public override ServiceHostBase CreateServiceHost(string constructorString, Uri[] baseAddresses)
        {
            var builder = new ContainerBuilder();
            builder.RegisterType<VocabExtService>();
            Container = builder.Build();

            return base.CreateServiceHost(constructorString, baseAddresses);
        }

        protected override ServiceHost CreateSingletonServiceHost(object singletonInstance, Uri[] baseAddresses)
        {
            if (singletonInstance == null)
            {
                throw new ArgumentNullException("singletonInstance");
            }
            if (baseAddresses == null)
            {
                throw new ArgumentNullException("baseAddresses");
            }
            return new ServiceHost(singletonInstance, baseAddresses);
        }
    }
}


As you can see I've inherited my factory from AutofacHostFactory and overrided a couple of methods. In CreateServiceHost method I register my service, the second method is left untouched.

The last thing you need to do is to specify the factory at the *.svc file markup. For example:

<%@ ServiceHost Language="C#" Debug="true" Service="VX.Service.VocabExtService, VX.Service" CodeBehind="VocabExtService.svc.cs" Factory="VX.Service.ServiceHostFactory, VX.Service"%>
Pay attention to Service attribute here. I have to change the default one to full name to make AutofacHostFactory work. That's it, you can access the service and use dependency injections in your code.