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