In the last post of this series, we created a new example code base used to display help text for various "commands". This was a simple code base that extended previous examples by using external assemblies and different catalogs to identify all of the parts that can be imported and mapped. This example covers a lot of scenarios when applied beyond the means of console-based text output since each imported "command" could literally be a functional piece of code by itself. However, what happens when the imports require something from our main application? When one object has a dependency with another object of another type; only for the dependent have a dependency towards the initial type; this is called a circular dependency. In this post, we're going to look at the condition of a circular dependency and see how MEF encounters such issues.
A Closer Look at Circular Dependencies in General
To reiterate what was said a moment ago, a circular dependency is where two objects depend on each other for different things in somewhat of a symbiotic relationship. Let's look at an example. Let's say we have an application that contains two special types. One type is used to read configuration information and implements the IConfig interface. The second type is used to log any errors that may occur in the system and implements the IErrorManager interface.
In this illustration, our configuration manager requires an instance of an error manager in case there was an error reading the configuration information. In addition, our error manager requires an instance of a configuration manager to know how the system is configured for logging errors. Since each type has a required dependency on the other, a common pattern is to place the required dependencies into a constructor so that the proper dependencies are provided and the type is instantiated in a valid state. Below is an example what our constructors may look like.
1: class ConfigurationManager : IConfig
2: {
3: private IErrorManager _errMgr;
4:
5: public ConfigurationManager (IErrorManager errorManager)
6: {
7: _errMgr = errorManager;
8: }
9: }
10:
11: class ErrorManager: IErrorManager
12: {
13: private IConfig _configMgr;
14:
15: public ErrorManager(IConfig configManager)
16: {
17: _configMgr = configManager;
18: }
19: }
If we have to pass in a valid instance of IErrorManager to our configuration manager, we would first have to instantiate an instance of IConfig for our error manager. Since there's not a way based on the above code to instantiate a type without the other, the code has to be changed to allow for each to be created in an invalid state and the dependency to be passed to it via a property like below.
1: class ConfigurationManager : IConfig
2: {
3: public IErrorManager ErrMgr { get; set; }
4:
5: public ConfigurationManager() { }
6: }
7:
8: class ErrorManager : IErrorManager
9: {
10: public IConfig ConfigMgr { get; set; }
11:
12: public ErrorManager() { }
13: }
The issue with this pattern is that it requires the developer to now remember to always inject the proper dependencies after the types have been instantiated through their constructors. Ultimately, it's risky because we're all human and tend to forget from time to time. IoC containers help here a little bit, but can still confuse people. An alternative solution which many people prefer is to used a bootstrapped version of one of the two objects that doesn't depend on the other. This pattern also has issues due to what may not be available in a bootstrapped version. In our example, a bootstrapped IConfig object may not contain any error management code with in it.
Looking At a Simple MEF Circular Reference
Now that we, hopefully, understand a bit more about what a circular reference is and where it can occur, let's see about recreating our example with MEF. Let's create a program that has a property for a IErrorManager and IConfigMgr instances. To build the circular reference, let's inject the dependencies through the constructors of our ErrorManager and ConfigManager classes and mark the constructors as Imports. Below is our Circular Reference implementation.
Program.cs
1: namespace MEFExample5
2: {
3: class Program
4: {
5: [Import(typeof(IConfigManager))]
6: public IConfigManager ConfigurationManager { get; set; }
7:
8: [Import(typeof(IErrorManager))]
9: public IErrorManager ErrorManager { get; set; }
10:
11: static void Main(string[] args)
12: {
13: var prog = new Program();
14: prog.Run();
15: }
16:
17: void Run()
18: {
19: Compose();
20: Console.WriteLine(ConfigurationManager.TestText);
21: Console.WriteLine(ErrorManager.TestText);
22: Console.ReadKey();
23: }
24:
25: void Compose()
26: {
27: var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());
28: var container = new CompositionContainer(catalog);
29: container.ComposeParts(this);
30: }
31: }
32: }
ConfigMgr.cs
1: namespace MEFExample5
2: {
3: public interface IConfigManager
4: {
5: string TestText { get; set; }
6: }
7:
8: [Export(typeof(IConfigManager))]
9: public class ConfigMgr : IConfigManager
10: {
11:
12: public string TestText { get; set; }
13:
14: public ConfigMgr()
15: {
16: TestText = "Config";
17: }
18:
19: [ImportingConstructor]
20: public ConfigMgr(IErrorManager errorManager) : this()
21: {
22: _errorManager = errorManager;
23: }
24: }
25: }
ErrorManager.cs
1: namespace MEFExample5
2: {
3: public interface IErrorManager
4: {
5: string TestText { get; set; }
6: }
7:
8: [Export(typeof(IErrorManager))]
9: public class ErrorManager : IErrorManager
10: {
11: public string TestText { get; set; }
12:
13: public ErrorManager ()
14: {
15: TestText = "Error";
16: }
17:
18: [ImportingConstructor]
19: public ErrorManager(IConfigManager configManager) : this()
20: {
21: _configManager = configManager;
22: }
23: }
24: }
When we try to run this, we get the following error message:
As we can see, MEF doesn't completely remove issues inherit from constructor injection-based circular references. Like we discussed above though, we can move our dependencies into properties instead of a constructor. What's nice about doing such with MEF though is that we can don't have to truly remember to wire up the dependencies manually. With the ability to declaratively set our dependent properties as Imports, we won't need any additional code. Below is the updated code to that addresses the circular reference.
ConfigMgr.cs
1: namespace MEFExample5
2: {
3: public interface IConfigManager
4: {
5: string TestText { get; set; }
6: }
7:
8: [Export(typeof(IConfigManager))]
9: public class ConfigMgr : IConfigManager
10: {
11: private IErrorManager _errorManager;
12:
13: public string TestText { get; set; }
14:
15: [Import(typeof(IErrorManager))]
16: public IErrorManager ErrorMngr
17: {
18: get { return _errorManager; }
19: set { _errorManager = value; }
20: }
21:
22: public ConfigMgr()
23: {
24: TestText = "Config";
25: }
26:
27: public ConfigMgr(IErrorManager errorManager) : this()
28: {
29: _errorManager = errorManager;
30: }
31: }
32: }
ErrorManager.cs
1: namespace MEFExample5
2: {
3: public interface IErrorManager
4: {
5: string TestText { get; set; }
6: }
7:
8: [Export(typeof(IErrorManager))]
9: public class ErrorManager : IErrorManager
10: {
11: private IConfigManager _configManager;
12:
13: public string TestText { get; set; }
14:
15: [Import(typeof(IConfigManager))]
16: public IConfigManager ConfigManager
17: {
18: get { return _configManager; }
19: set { _configManager = value; }
20: }
21:
22: public ErrorManager ()
23: {
24: TestText = "Error";
25: }
26:
27: public ErrorManager(IConfigManager configManager) : this()
28: {
29: _configManager = configManager;
30: }
31: }
32: }
Summary:
So in this post we looked at Circular References and and how they are handled through MEF. In the next post of this series, we'll dive into how we can use lazy loading towards imported parts and where they could be applied at.
Make one type with constructor injection and set itself as the other property. Fixed :p
ReplyDelete@XIU
ReplyDeleteWhile this is true, I've found that such can also hurt the readability of the code since you have an inconsistency. By using Property Injection in both and not just one class, it can lead to a consistent pattern between the two objects (and others).
Regardless, you are correct that one constructor injection and one property injection would also work. It ultimately turns into a matter of preference.
Hi James
ReplyDeleteNice post (as well as your series). One thing you might want to clarify is that this is not really lazy loading, it's lazy instantiation i.e. the types instances have not gotten created yet.
They are not lazy loaded because the assemblies actually are loaded when the catalog is created. We reflect over them to find the exports.
Now MEF actaully is designed for supporting lazy loading. But this is something handled at the catalog level i.e. a catalog can persist it's information about parts and then only load the actual assembly when needed. We don't ship a default implementation of this yet, but we might in the future. At a minimum we will proabably have some blog posts on this.
We provide an API called ReflectionModelServices that you can use for creating a cached catalog. Here is a code sample which illustrates using it to create a convention based catalog.
Keep up the good wwork!
@Glenn Block
ReplyDeleteThanks Glenn. I'll make sure to make the appropriate corrections about lazy loading vs lazing instantiation.
Thanks for the information.