Friday, January 14, 2011

Forbidden Namespace Dependencies Wildcard

The architecture modeling project that shipped with Visual Studio 2010 has some nice features, one of which is the Layer Diagram where you can drop assemblies, namespaces, classes, and methods. There is one feature that is not supported yet and it's the ability to specify a wildcard in the Forbidden Namespace Dependencies as well as the Forbidden Namespace properties of a particular layer inside the layer diagram. The idea behind this is that i need to be able to write something like: MyRootNamespace.* and the tool I wrote would list out all namespaces under the root namespace, further i could write something like MyRootNamespace.Level1.* and get same results. So, I wrote some code to do that task for any layer diagram in a modeling project. Below is a screenshot of the properties window. Just click on a layer and hit F4 to get to it.


It's straight forward and uses LINQ to XML. So, let's see how it's done.
First, I created a method that get me some namespaces to work with.

   1:  protected static void GetWorkingNameSpaces(string AssembliesDir)
   2:  {
   3:      Namespaces = new HashSet<string>();
   4:      DirectoryInfo di = new DirectoryInfo(AssembliesDir);
   5:      var workingAssemblies = di.Exists ? 
   6:                                di.EnumerateFiles()
   7:                                  .Where(s=>s.Extension.ToLower() ==".dll" ||
   8:                                 s.Extension.ToLower() == ".exe" ) : null;
   9:      Parallel.ForEach(workingAssemblies, a =>
  10:      {
  11:          Assembly reference = Assembly.ReflectionOnlyLoadFrom(a.FullName);
  12:          try
  13:          {
  14:              HashSet<string> refernceNamespaces = 
  15:                  new HashSet<string>(reference.GetTypes()
  16:                                               .Select(f => f.Namespace)
  17:                                               .Where(n => n != null));
  18:              Namespaces.UnionWith(refernceNamespaces);
  19:          }
  20:          catch (ReflectionTypeLoadException) { }
  21:      });
  22:  }

So, not a whole lot going on, just newing up an instance of a HashSet and doing reflection over a list of assemblies and extracting some namespaces, and storing them in a HashSet collection so that i can work with them later.

Next, I created a method that will process the wildcard token. It takes in a namespace root or sub-n-root and returns nested namespaces.

   1:  protected static HashSet<string> ProcessWildCard(string Token)
   2:  {
   3:      HashSet<string> namespaces = new HashSet<string>();
   4:      int index = default(int);
   5:      string Name = default(string);
   6:      if (Token.Contains("."))
   7:          index = Token.LastIndexOf('.');
   8:      else return namespaces;
   9:      Name = Token.Substring(0, index).Trim();
  10:      foreach (var t in from c in Namespaces 
  11:                          where c.StartsWith(Name) 
  12:                          select c)
  13:      {
  14:          namespaces.Add(t);
  15:      }
  16:      return namespaces;
  17:  }

Now that i got this far, i need to build an update method that takes in an attribute Name and an XElement - since I am going to be updating both the Forbidden Namespace Dependencies and Forbidden Namespaces.

   1:  protected static void UpdateForiddenAttribute(ref string 
   2:                                                forbiddenAttributeName,
   3:                                                XElement t)
   4:  {
   5:      List<string> proccessed = new List<string>();
   6:      string _forbiddenAttributeValue = default(string);
   7:      if (t.Attribute(forbiddenAttributeName) != null)
   8:      {
   9:          _forbiddenAttributeValue = 
  10:                  t.Attribute(forbiddenAttributeName).Value;
  11:          string[] elements = default(string[]);
  12:          if (_forbiddenAttributeValue.Contains(";"))
  13:          {
  14:              elements = _forbiddenAttributeValue.Split(new[] { ";" },
  15:                               StringSplitOptions.RemoveEmptyEntries);
  16:          }
  17:          else
  18:          {
  19:              elements = new[] { _forbiddenAttributeValue };
  20:          }
  21:          foreach (var s in elements)
  22:          {
  23:              if (s.Contains("*") && !(s.Contains(",") || s.Contains("|")))
  24:              {
  25:                  var hash = ProcessWildCard(s);
  26:                  if (hash.Count > 0)
  27:                      foreach (var h in hash)
  28:                      {
  29:                          proccessed.Add(h);
  30:                      }
  31:                  else
  32:                  {
  33:                      proccessed.Add(s);
  34:                  }
  35:              }
  36:              else
  37:              {
  38:                  proccessed.Add(s);
  39:              }
  40:          }
  41:      }
  42:      string formattedAttributeValue = default(string);
  43:      if (proccessed.Count > 1)
  44:      {
  45:          formattedAttributeValue = String.Join(";", proccessed);
  46:      }
  47:      else
  48:      {
  49:          if (proccessed.Count == 1)
  50:              formattedAttributeValue = proccessed[0];
  51:          else
  52:              formattedAttributeValue = null;
  53:      }
  54:      t.SetAttributeValue(forbiddenAttributeName, formattedAttributeValue);
  55:  }

So far so good, all i did is get some attribute value, do some processing on it and update the .layerdiagram file with the newly constructed list of namespaces. So, the next step is the actual method that runs and collects data about the layerdiaram file/s in the modeling project, calls the update method and finally saves each layerdiagram file to complete the process.

   1:  static void RunTool(string layerdiagramDirectoryPath, 
   2:                      string AssembliesDirectory)
   3:  {
   4:      string forbiddenNamespaceDependenciesName = 
   5:                              "forbiddenNamespaceDependencies";
   6:      string forbiddenNamespaceName = "forbiddenNamespace";
   7:      GetWorkingNameSpaces(AssembliesDirectory);
   8:   
   9:      var q= Directory.EnumerateFiles(layerdiagramDirectoryPath, 
  10:                                      "*.layerdiagram", 
  11:                                      SearchOption.AllDirectories)  
  12:          .Select(x => new{
  13:                  s =  XDocument.Load(x),
  14:                  p = x 
  15:              })
  16:          .Where(d => d.s.Root
  17:                      .Elements()
  18:                      .Descendants()
  19:                      .Any(v => v.Attributes(
  20:                       forbiddenNamespaceDependenciesName)
  21:                      .Any())
  22:                  ||d.s.Root
  23:                      .Elements()
  24:                      .Descendants()
  25:                      .Any(v => v.Attributes(forbiddenNamespaceName)
  26:                      .Any())
  27:                  );
  28:      foreach (var x in q)
  29:      {
  30:          bool DocumentHasChanges = default(bool);
  31:          IEnumerable<XElement> xElements = 
  32:                  ( from c in x.s.Root
  33:                              .Elements()
  34:                              .Descendants()
  35:                  where c.Attributes(forbiddenNamespaceDependenciesName)
  36:                         .Any() ||
  37:                          c.Attributes(forbiddenNamespaceName).Any()
  38:                  select c
  39:                  );
  40:          Parallel.ForEach(xElements, t =>
  41:          {
  42:              if (t.Attribute(forbiddenNamespaceDependenciesName) != null)
  43:              {
  44:                  UpdateForiddenAttribute(ref 
  45:                                   forbiddenNamespaceDependenciesName, t);
  46:                  DocumentHasChanges = true;
  47:              }
  48:              if (t.Attribute(forbiddenNamespaceName) != null)
  49:              {
  50:                  UpdateForiddenAttribute(ref forbiddenNamespaceName, t);
  51:                  DocumentHasChanges = true;
  52:              }
  53:          });
  54:          if (DocumentHasChanges)
  55:          x.s.Save(x.p);
  56:      }
  57:  }

And that's all the code needed to now have an out of box wildcard functionality. To get this working just hook it up to the main entry point of your program and pass in 2 arguments (modeling project directory and assemblies directory) and wrap it up with a try catch block. Would look something like this:

   1:  #region Program Entry
   2:      public static void Main(string[] args)
   3:      {
   4:          try
   5:          {
   6:              RunTool(args[0], args[1]);
   7:          }
   8:          catch (Exception g)
   9:          {
  10:              Console.WriteLine("Error :{0} ", g.Message);
  11:          }
  12:      }
  13:  #endregion

So, to use this tool, right click on the modeling project and choose Edit project - add an MSBUILD Exec task and give it the location of batch file under your solution items or where ever you like to keep .bat files related to your solution and that's all. Next time you build the solution and if there are any broken forbidden namespace dependencies, they will show up as build errors.