IoC comparison: Spring.NET CodeConfig
Spring.NET has a new add-on called Spring.NET CodeConfig 1.0.1 that I hoped may adress some of what I saw as spring’s shortcomings in my comparisons of .Net IoC containers, specifically it should allow different configs be used in different tests in the same project. The docs for Spring.NET code config are here. it's also on github and nuget and is apparently just "the first step in the process of expanding Spring.NET's support for non-XML-dependent configuration scenarios". It works, and goes about it in a different way which is not as terse and as most other containers.
I tried most of my tests in my test suite with Spring.NET CodeConfig, and the code below demonstrates that you can indeed vary things from one test to another. The way that it works is a bit like NInject - one class declares the bindings, and configuration object this is used to populate a container. However the class that defines the bindings is filled not with type registrations, but with attributed factory methods.
The class that declares the bindings can be of any type, and need only be marked with the [Configuration] attribute, and constructed objects are returned by methods on it marked with the [Definition] attribute. You could put any amount of code in these methods, which enables flexibility, but it has the problem that it is too explicit. I have to do return new SweetVendingMachine(new VanillaJellybeanDispenser());
The constructor arguments to SweetVendingMachine are not computed. It doesn't do the de-coupling of constructor arguments that is characteristic of most IoC tools. If a constructor changes, the change has to be done in the definition class as well as the constructor itself. Eliminating that friction is a key feature of IoC in my opinion, and Spring.Net doesn't do it.
I use context.ScanWithTypeFilter to read in the right definition class. You can read multiple definitions, or indeed all of them that can be found. But we don’t use the advanced scan-the-app features in this demo. The factory methods are virtual so that a subclass can be constructed that handles concerns such as singleton instances.
If you were using this code in test and production, you can use different definition classes for your various live and tests configurations, and you could put code in them that chooses what kind of object to return. As seen in the lemon configuration, using a constructor parameter in a registered object is trivial; you just do it and don't have any of the hoops to jump through that this scenario entails in most IoC containers. So it enables a fair amount of flexibility at runtime. e. g. you could do:
if (somecondition) return new SweetVendingMachine(new VanillaJellybeanDispenser()); else return new SweetVendingMachine(new StrawberryJellybeanDispenser());
But with a more conventional IoC container, you get that flexibility in a different way, by deciding what types to put into the container; e.g. doing
if (somecondition) Bind<IJellybeanDispenser>().To<VanillaJellybeanDispenser>(); else Bind<IJellybeanDispenser>().To<StrawberryJellybeanDispenser>();
And as usual you would then let the container decide how to wire the constructors together; the right class for IJellybeanDispensor would be used throughout, not just in SweetVendingMachine. This gives the same flexibility with less code, which is ideal for unit tests or flexible live apps.
The syntax for getting objects out of the container has been improved, you can say SweetShop sweetShop = context.GetObject<SweetShop>()
I think that Spring.NET CodeConfig enables a fair amount of flexibility for Spring.net users, but if I was starting from scratch I would rather use another tool, such as Ninject, which is less verbose.
The code is maintained in the repository on github, but is shown here too:
namespace IoCComparison
{
using System;
using Spring.Context;
using Spring.Context.Support;
using Spring.Objects.Factory.Support;
using Spring.Context.Attributes;
using NUnit.Framework;
[Configuration]
public class VanillaConfiguration
{
[Definition]
public virtual SweetShop SweetShop()
{
return new SweetShop(SweetVendingMachine());
}
[Definition]
public virtual SweetVendingMachine SweetVendingMachine()
{
return new SweetVendingMachine(new VanillaJellybeanDispenser());
}
}
[Configuration]
public class StrawberryConfiguration
{
[Definition]
public virtual SweetShop SweetShop()
{
return new SweetShop(SweetVendingMachine());
}
[Definition]
public virtual SweetVendingMachine SweetVendingMachine()
{
return new SweetVendingMachine(new StrawberryJellybeanDispenser());
}
}
[Configuration]
public class VanillaConfigurationWithNewInstances
{
[Definition]
[Scope(ObjectScope.Prototype)]
public virtual SweetShop SweetShop()
{
return new SweetShop(SweetVendingMachine());
}
[Definition]
[Scope(ObjectScope.Prototype)]
public virtual SweetVendingMachine SweetVendingMachine()
{
return new SweetVendingMachine(IJellybeanDispenser());
}
[Definition]
[Scope(ObjectScope.Prototype)]
public virtual IJellybeanDispenser IJellybeanDispenser()
{
return new StrawberryJellybeanDispenser();
}
}
[Configuration]
public class VanillaConfigurationWithSingletonJellybeanDispenser
{
[Definition]
[Scope(ObjectScope.Prototype)]
public virtual SweetShop SweetShop()
{
return new SweetShop(SweetVendingMachine());
}
[Definition]
[Scope(ObjectScope.Prototype)]
public virtual SweetVendingMachine SweetVendingMachine()
{
return new SweetVendingMachine(IJellybeanDispenser());
}
[Definition]
[Scope(ObjectScope.Singleton)] // singleton scope is the default, but it is added here for emphasis
public virtual IJellybeanDispenser IJellybeanDispenser()
{
return new StrawberryJellybeanDispenser();
}
}
[Configuration]
public class AniseedConfiguration
{
[Definition]
public virtual SweetShop SweetShop()
{
return new AniseedSweetShop();
}
}
[Configuration]
public class LemonConfiguration
{
[Definition]
public virtual SweetShop SweetShop()
{
return new SweetShop(SweetVendingMachine());
}
[Definition]
public virtual SweetVendingMachine SweetVendingMachine()
{
return new SweetVendingMachine(new AnyJellybeanDispenser(Jellybean.Lemon));
}
}
[TestFixture]
public class SpringCodeConfigTest
{
private static IApplicationContext CreateContextFromDefinition(Type definitionType)
{
CodeConfigApplicationContext context = new CodeConfigApplicationContext();
context.ScanWithTypeFilter(t => t.Equals(definitionType));
context.Refresh();
return context;
}
[Test]
public void CanMakeSweetShopWithVanillaJellybeans()
{
IApplicationContext ctx = CreateContextFromDefinition(typeof(VanillaConfiguration));
SweetShop sweetShop = ctx.GetObject<SweetShop>();
Assert.AreEqual(Jellybean.Vanilla, sweetShop.DispenseJellyBean());
}
[Test]
public void CanMakeSweetShopWithStrawberryJellybeans()
{
IApplicationContext ctx = CreateContextFromDefinition(typeof(StrawberryConfiguration));
SweetShop sweetShop = ctx.GetObject<SweetShop>();
Assert.AreEqual(Jellybean.Strawberry, sweetShop.DispenseJellyBean());
}
[Test]
public void JellybeanDispenserHasNewInstanceEachTime()
{
IApplicationContext ctx = CreateContextFromDefinition(typeof(VanillaConfigurationWithNewInstances));
SweetShop sweetShop = ctx.GetObject<SweetShop>();
SweetShop sweetShop2 = ctx.GetObject<SweetShop>();
Assert.IsFalse(ReferenceEquals(sweetShop, sweetShop2), "Root objects are equal");
Assert.IsFalse(ReferenceEquals(sweetShop.SweetVendingMachine, sweetShop2.SweetVendingMachine), "Contained objects are equal");
Assert.IsFalse(ReferenceEquals(sweetShop.SweetVendingMachine.JellybeanDispenser, sweetShop2.SweetVendingMachine.JellybeanDispenser), "services are equal");
}
[Test]
public void CanMakeSingletonJellybeanDispenser()
{
IApplicationContext ctx = CreateContextFromDefinition(typeof(VanillaConfigurationWithSingletonJellybeanDispenser));
SweetShop sweetShop = ctx.GetObject<SweetShop>();
SweetShop sweetShop2 = ctx.GetObject<SweetShop>();
Assert.IsFalse(ReferenceEquals(sweetShop, sweetShop2), "Root objects are equal");
Assert.IsFalse(ReferenceEquals(sweetShop.SweetVendingMachine, sweetShop2.SweetVendingMachine), "Contained objects are equal");
// should be same service
Assert.IsTrue(ReferenceEquals(sweetShop.SweetVendingMachine.JellybeanDispenser, sweetShop2.SweetVendingMachine.JellybeanDispenser), "services are not equal");
}
[Test]
public void CanMakeAniseedRootObject()
{
IApplicationContext ctx = CreateContextFromDefinition(typeof(AniseedConfiguration));
SweetShop sweetShop = ctx.GetObject<SweetShop>();
Assert.AreEqual(Jellybean.Aniseed, sweetShop.DispenseJellyBean());
}
[Test]
public void CanUseAnyJellybeanDispenser()
{
IApplicationContext ctx = CreateContextFromDefinition(typeof(LemonConfiguration));
SweetShop sweetShop = ctx.GetObject<SweetShop>();
Assert.AreEqual(Jellybean.Lemon, sweetShop.DispenseJellyBean());
}
}
}
2 Comments
Steve Bohlen said
Anthony:
Thanks for the post and the comments. Yes, you're entirely correct that the present CodeConfig approach prevents what Spring.NET refers to as "autowiring" ctor dependencies (the container inspecting the ctor args and determining if it is 'aware' of a type that can satisfy that dependency and resolving it automatically). This makes the CodeConfig approach an ideal choice in some scenarios but probably inappropriate in others and its part of the reason for the comment you reference that this represents only the first step in broadening the config story for Spring.NET beyond its XML roots.
An important part of the full planned config story is that all config choices can be easily mixed and matched, enabling the developer to use each of them where they are best suited:
- XML config for those elements that need to change at deployment time (since XML may be changed without requiring recompilation)
- CodeConfig for those elements that need complex assembly logic (since as you quite correctly point out CodeConfig leverages Spring.NET's inherent support for FactoryMethods)
- an explicit defintion API along the lines of "Define<TService, TImplementation>()" that excels at defining elements that need to be explicitly registered but for which type-safety and refactoring support is of paramount concern
-a rules-based declarative fluent API that excels at defining elements that adhere to some common rule and for declaring how the container should react/handle types that satisfy the rule
By mixing and matching these configuration choices, the developer should be able to leverage the approach that best suits the needs of the different parts of their application to achieve a very rich configuration story.
As mentioned, more to come~!
(and thanks again for the post and the feedback on the present state of the effort around the Spring.NET configuration story)
-Steve B.
AnthonySteele said
Hi Steve.
Thanks for the response and the roadmap.
I agree that XML config is important for configuring a program at time of deployment. I did not include it in my suite of tests for a couple of reasons – I hadn't had much experience with it, since the programs that I had used IoC with had no need for it. Also if it is not present in the container it can be faked with code like
bool registerFoo = bool.Parse(ConfigurationManager.AppSettings["useFoo"]);
if (registerFoo)
{
container.RegisterType<ISomeRepository, Foo>();
}
else
{
container.RegisterType<ISomeRepository, Bar>();
}
Or even
string fooTypeName = ConfigurationManager.AppSettings["fooType"];
Type fooType = Type.GetType(fooTypeName);
container.RegisterType(typeof(ISomeRepository), fooType);
Obviously this is very simple, hacky code, but it could be extended if more complexity was needed. It is prone to failing due to errors in strings, but xml config designed for the container is also just text and has the same issues.
I dislike configuration that is entirely in XML – I'd prefer configuration in code for the majority of the registrations, so I'd agree that a "mix and match" approach is ideal to cover all possible cases.