Tuesday, December 1, 2009

Managing Composition Through Lazy Loading Parts

So far in this post series, we've been looking at various aspects of working with MEF in the context of a single level of composition.  One interesting thing about MEF is that its composition is recursive based on the assemblies and types identified in the catalogs within the container.  What this means is that if one of our parts also has imports defined for parts of its own, the composition container will continue loading parts for the initial type as well as all parts loaded until no more parts are found or all imports are fulfilled.  This is a really nice feature since it will ensure everything is ready for you once compose the initial type; however, this eager loading can greatly cause a performance issue if the parts are not constructed properly.  In this post on our ongoing series about MEF, we'll look into the concept of parts of parts and how to apply lazy loading principles towards them.

Putting the Pieces of Pieces Together

To start this example off, we'll add to the code we last used in the fourth post of this series (Playing Nice with Other Assemblies using MEF Catalogs).  In order to enhance the code to illustrate a part of parts, let's add a new field to our IHelp interface that will represent subcommands of our parts.  Since we're going to assume that any of our commands could have multiple subcommands, let's make it of type IEnumerable<IHelp>.  Since it's an enumeration of IHelp, the amount of embedding could be infinite in theory (which sounds like fun but maybe another time).  After we update our IHelp interface and our current commands, let's mark only our ExampleCommand class to have its Subcommands property to be a MEF Import point as shown below.

IHelp.cs

   1:  public interface IHelp
   2:  {
   3:      string CommandName { get; }
   4:      string HelpText { get; }
   5:      IEnumerable<IHelp> Subcommands { get; set; }
   6:  }

ExampleCommand.cs

   1:  [Export("Commands", typeof(IHelp))]
   2:  public class ExampleCommand : IHelp
   3:  {
   4:      private string _helpText = "Lorem ipsum dolor sit amet, c ..." +
   5:                                      "Nulla molestie erat rhon ..." +
   6:                                      "amet dolor. Aliquam rhon ..." +
   7:                                      "vel est. Vestibulum et u ..." +
   8:                                      "id tellus. Fusce lectus  ..."
   9:   
  10:      public string CommandName
  11:      {
  12:          get { return AppName + " Example1"; }
  13:      }
  14:   
  15:      public string HelpText
  16:      {
  17:          get { return _helpText; }
  18:      }
  19:   
  20:      [ImportMany("Subcommands", typeof(IHelp))]
  21:      public IEnumerable<IHelp> Subcommands { get; set; }
  22:   
  23:      [Import("AppName")]
  24:      public string AppName { get; set; }
  25:  }

With this done, let's modify our Program.cs code slightly as well.  In order to ensure the we can infinitely loop through our subcommands, let's change our output information slightly by using recursion as shown below.

   1:  void Run()
   2:  {
   3:      Compose();
   4:   
   5:      OutputHelp(Commands, 0);
   6:      Console.ReadKey();
   7:  }
   8:   
   9:  private void OutputHelp(IEnumerable<IHelp> helpCommands, int padding)
  10:  {
  11:      foreach (var help in helpCommands)
  12:      {
  13:          Console.WriteLine(string.Empty.PadLeft(padding, '-') + FormatCommandOutput(help));
  14:   
  15:          if (help.Subcommands != null && help.Subcommands.Count() > 0)
  16:          {
  17:              OutputHelp(help.Subcommands, ++padding);
  18:          }
  19:      }
  20:  }

In the above code, we're passing our recursive function, OutputHelp, two parameters.  The first parameter represent the collection of IHelp instances to be outputted to the screen while the second is to provide depth of the recursion.  On line 13 of the snippet above we use this depth parameter to append our formatted command name with hyphens equal to the depth.  Lastly, we check to see if the Subcommands collection has values and call our OutputHelp method again passing the collection and incrementing the depth.

Next, let's create a new class called ExampleSubcommand.cs.  This will represent a subcommand for our ExampleCommand.cs class.  Once our ExampleSubcommand is all set, we can run the application to see the following output.

Example6-OutputTest1

In the above image, we can see our Subcommand being displayed after the Example1 command as expected.  In addition, we've applied the hyphens prior to the command name to indicate the depth correctly.  Alternatively, we could have added an export to our ExampleCommand class similar to that of our AppName property to send the "MEFExample6 Example1" down the tree as well.

Working with Lazing Parts

Now that we have our example prepped, it's time to see what we can do to optimize the code slightly through lazy loading.  For those who may not be familiar with the concept, lazy loading ultimately means that you do not load the objects or data until they are needed.  What this translates to is that when we call container.ComposeParts(this) in our Program.cs class, we want only our initial commands to be instantiated but not any subcommands it has in order to ensure only the objects that are currently being used are the ones currently in memory.

in order to accomplish lazy loading of exports in MEF, we need to use the System.Lazy<T> type in place of the contract type of our exports.  This type (built into the MEF assembly for .Net 3.5)  tells MEF to delay the instantiation of the value until it's actually called.  This is exactly what we're looking for to prevent a full line of instances from being created as we compose our parts.  Before we dive into the nuances of Lazy<T>, let's look at a simplified snippet below:

   1:  public class LazyExample
   2:  {
   3:      [Import()]
   4:      public System.Lazy<IHelp> command { get; set; }
   5:  }

In the above code, we created an example class called LazyExample which contains a singular import of type IHelp.  Because we wanted to delay the instantiation of this instance, we changed the type from IHelp to System.Lazy<IHelp> and we are all set from a definition standpoint.  While this looks easy, there are two things to be aware of when working with Lazy<T>.

  1. System.Lazy<T> requires you to grab instances of the underlying type using the Value property.  In the example snippet above, we would need to call command.Value in order to get the instance of IHelp instead of just calling the command property like we've been doing up to this point.
  2. System.Lazy<T> is not a collection type.  System.Lazy<T> does not implement nor inherit any class that implement IEnumerable<T>.  What this means is that just wrapping Lazy<T> around your currently defined properties that are decorated with ImportMany() won't work.  In order to apply lazy loading to collections of parts, you must change the type to IEnumerable<Lazy<IHelp>>, keeping inline with the above example.  This means any looping within the collection must be item.Value now instead of just item.

Applying Lazy Loading to Our Exports

Since we now see how to apply lazy loading to our exports, let's modify our code to implement such.  To do this, we'll need to modify our IHelp interface to change the Subcommands property we added to be of type IEnumerable<Lazy<IHelp>> and propagate such between the various parts.  Next, we need to copy our recursive OutputHelp command to create a new method called OutputSubcommandHelp which will address the output.  Once this is done, our code should look something like this:

IHelp.cs

   1:  public interface IHelp
   2:  {
   3:      string CommandName { get; }
   4:      string HelpText { get; }
   5:      IEnumerable<Lazy<IHelp>> Subcommands { get; set; }
   6:  }

ExampleCommand.cs

   1:  [ImportMany("Subcommands", typeof(IHelp))]
   2:  public IEnumerable<Lazy<IHelp>> Subcommands { get; set; }

Program.cs

   1:  [ImportMany("Commands", typeof(IHelp))]
   2:  public IEnumerable<IHelp> Commands { get; set; }
   3:   
   4:  [Export("AppName")]
   5:  public string AppName { get { return "MEFExample6"; } }
   6:   
   7:  void Run()
   8:  {
   9:      Compose();
  10:   
  11:      OutputHelp(Commands, 0);
  12:      Console.ReadKey();
  13:  }
  14:   
  15:  private void OutputHelp(IEnumerable<IHelp> helpCommands, int padding)
  16:  {
  17:      foreach (var help in helpCommands)
  18:      {
  19:          Console.WriteLine(string.Empty.PadLeft(padding, '-') + FormatCommandOutput(help));
  20:   
  21:          if (help.Subcommands != null && help.Subcommands.Count() > 0)
  22:          {
  23:              OutputSubcommandHelp(help.Subcommands, ++padding);
  24:          }
  25:      }
  26:  }
  27:   
  28:  private void OutputSubcommandHelp(IEnumerable<Lazy<IHelp>> helpCommands, int padding)
  29:  {
  30:      foreach (var help in helpCommands)
  31:      {
  32:          Console.WriteLine(string.Empty.PadLeft(padding, '-') + FormatCommandOutput(help.Value));
  33:   
  34:          if (help.Value.Subcommands != null && help.Value.Subcommands.Count() > 0)
  35:          {
  36:              OutputSubcommandHelp(help.Value.Subcommands, ++padding);
  37:          }
  38:      }
  39:  }

Adding Another Subcommand Layer

The last thing we will do in this example is to further extend the recursion tree by adding a new subcommand onto our current ExampleSubcommand.  We'll call it ExampleSubcommand2 and give the contract a label of "Subcommands2".  We'll decorate our ExampleSubcommand class to apply Import such into its own Subcommand collection and we'll run our application to see the following:

Example6-OutputTest2

Summary:

Hopefully after this post you can see the benefits of lazy loading exports as well as how easy it is to implement such.  Given the pattern that we have outlined above, the true depth can open a number of possibilities without over-utilizing our memory.  In the next section, we're going to mix things up a bit by looking at what it may take to have an export be used to defined and interact with a custom configuration section while the assembly is not in the same directory as the calling assembly.

Resources:


kick it on DotNetKicks.comShout it

No comments:

Post a Comment