Introducing CodeConfigA Dependency Injection ExampleWe will introduce the new code based approach by working with a very
simple application that will provide us the context to understand the
concepts of CodeConfig. We start by examining a sample application that
uses Spring.NET configured via ‘traditional’ XML configuration files. Then
we show how CodeConfig can be used to achieve the same results without any
XML configuration files at all.To begin with, let’s explore the sample application that we will be
working with. This sample app is included in the Spring.NET CodeConfig
download package in the
/examples/Spring.CodeConfig.Migration folder.To keep things simple, it’s just a .NET console application designed
to calculate and display the prime numbers between zero and an arbitrary
maximum number. There are four classes that must collaborate together to
do the work: ConsoleReporter,
PrimeGenerator,
PrimeEvaluationEngine, and
OutputFormatter. ConsoleReporter
depends on the PrimeGenerator which in turn depends on
the PrimeEvaluationEngine to calculate the prime
numbers. ConsoleReporter also depends on
OutputFormatter to format the results. The main console
application then simply asks ConsoleReporter to write
its report and ConsoleReporter goes to work. The
following Figure is a UML class diagram showing a simple way to visualize
the dependencies between these objects.A simple Main() method that would do this without
the Spring.NET container could look something like Listing 1. Note the
in-line injection of dependencies via constructor arguments that builds up
the collaborating objects.//Listing 1 (sample Main method not using Spring.NET)
static void Main(string[] args)
{
ConsoleReport report = new ConsoleReport(
new OutputFormatter(),
new PrimeGenerator(new PrimeEvaluationEngine()));
report.MaxNumber = 1000;
report.Write();
Console.WriteLine("--- hit enter to exit --");
Console.ReadLine();
}Using Spring.NET, as opposed to manually injecting dependencies as
in Listing 1, the collaborating objects are composed together with their
dependencies injected by the Spring.NET container at run-time. Initially,
the configuration of these objects is controlled from a Spring.NET XML
configuration file (see Listing 2).<!-- Listing 2 (Spring.NET XML Configuration file, application-context.xml) -->
<?xml version="1.0" encoding="utf-8" ?>
<objects xmlns="http://www.springframework.net">
<object name="ConsoleReport" type="Primes.ConsoleReport, Primes">
<constructor-arg ref="PrimeGenerator"/>
<constructor-arg ref="OutputFormatter"/>
<property name="MaxNumber" value="1000"/>
</object>
<object name="PrimeGenerator" type="Primes.PrimeGenerator, Primes">
<constructor-arg>
<object type="Primes.PrimeEvaluationEngine, Primes"/>
</constructor-arg>
</object>
<object name="OutputFormatter" type="Primes.OutputFormatter, Primes"/>
</objects>In Listing 2 you can also see the use of “ref”
element to refer to collaborating objects and the property
“MaxNumber” being set to “1000” on
the ConsoleReport object after it’s constructed. This
is the maximum number up to which we want the software to calculate prime
numbers. In Listing 3 we see the construction of the
XmlApplicationContext which is initialized by passing
it the name of the XML Configuration file. This container is then used to
resolve the ConsoleReport object with all of its
dependencies properly satisfied and its MaxNumber
property assigned the value of 1000.//Listing 3 (initializing the XmlApplicationContext container)
static void Main(string[] args)
{
IApplicationContext ctx = CreateContainerUsingXML();
ConsoleReport report = ctx["ConsoleReport"] as ConsoleReport;
report.Write();
ctx.Dispose();
Console.WriteLine("--- hit enter to exit --");
Console.ReadLine();
}
private static IApplicationContext CreateContainerUsingXML()
{
return new XmlApplicationContext("application-context.xml");
}While this XML-based configuration is well-understood by Spring.NET
users and others alike as a common method for expressing configuration
settings, it suffers from several challenges common to all XML file
including being overly-verbose and full of string-literals that are
unfriendly to most of the modern refactoring tools.Migration to CodeConfigTo reduce or even eliminate the use of XML for configuring the
Spring.NET DI container, let’s look at how we can express the same
configuration metadata in code using Spring.NET CodeConfig. There are
several steps to using CodeConfig. We will look at each of them in the
likely sequence that one would follow to convert an existing XML-based
configuration for Spring.NET over to use the CodeConfig approach.The CodeConfig ClassesCreating the CodeConfig ClassesFirst, we need to construct one or more classes to contain our
configuration metadata and attribute them properly. Spring.NET
CodeConfig relies upon attributes applied to classes and methods to
convey its metadata. Shown in Listing 4 is the CodeConfig class
(PrimesConfiguration) for our sample application.
// Listing 4, (Spring.NET Configuration Class, PrimesConfiguration.cs)
using System;
using System.Configuration;
using Primes;
using Spring.Context.Attributes;
namespace SpringApp
{
[Configuration]
public class PrimesConfiguration
{
[ObjectDef]
public virtual ConsoleReport ConsoleReport()
{
ConsoleReport report = new ConsoleReport(OutputFormatter(), PrimeGenerator());
report.MaxNumber = Convert.ToInt32(ConfigurationManager.AppSettings.Get("MaximumNumber"));
return report;
}
[ObjectDef]
public virtual IOutputFormatter OutputFormatter()
{
return new OutputFormatter();
}
[ObjectDef]
public virtual IPrimeGenerator PrimeGenerator()
{
return new PrimeGenerator(new PrimeEvaluationEngine());
}
}
}Elements of the CodeConfig ClassesLet’s explore the important elements of the CodeConfig file in
Listing 4 to understand how it can convey the same information to the
Spring.NET container as the XML file in Listing 2.The ClassAt the class level, you will notice the
PrimesConfiguration class has the[Configuration]
attribute applied to it. During the initialization phase of the DI
container, Spring.NET CodeConfig reads classes with these
attributes. Note that there is no specific inheritance hierarchy
required of a configuration class: no special base class or
interface implementation is required, leaving you free to leverage
inheritance and polymorphism to achieve some interesting
configuration and composition scenarios. Also note these special
identifying attributes are only applied to your CodeConfig classes,
not the types for which they are providing object definition
metadata. This means that your classes that do the work of your
application (e.g., ConsoleReport,
PrimeGenerator, etc.) are free to remain
undiluted POCO (Plain-Old-CLR-Object) classes that have themselves
no direct dependency on the Spring.NET framework.The MethodsAt the member level of the
PrimesConfiguration class, you will notice
several methods having the [ObjectDef]
attribute. This attribute identifies the method to which it is
applied as being the logical representation of a single object
definition for the Spring.NET container.To begin understanding how this works let’s look at the
simplest of the definitions, that of the OutputFormatter. Let’s
start with the method visibility: all [ObjectDef]
methods must be declared both public and virtual.Why must the [ObjectDef] methods be virtual?The requirement for the [ObjectDef]
methods being virtual comes from the need when using CodeConfig
for the container to proxy [ObjectDef]
methods on the [Configuration]
classes. When the container is asked for an object, this proxy
intercepts the invocation of the [ObjectDef]
methods and ensures that requests for objects respect object
scoping rules like singleton and prototype. Singleton scope
ensures that if you ask the container multiple times for the same
named object, it will always return the same instance rather than
a new one each time. Singleton scope is common in server-side
programming and is the default lifecycle in Spring.NET.The method return type, IOutputFormatter,
becomes the type that the DI container will be configured to
register. The method name itself,
OutputFormatter, is the equivalent of the id or
name that will be assigned to the object in the container. This name
can also be controlled by setting the Names
property on the [ObjectDef]
attribute itself.The body of the OutputFormatter() method
simply creates a new instance of the
OutputFormatter and returns it. In simple terms,
we can think of the OutputFormatter() method as a
factory method that knows how to construct and return an instance of
something that implements the IOutputFormatter
interface (in this case, the concrete
OutputFormatter class).More Complex MethodsTo understand a slightly more complex [ObjectDef]
method, let’s now examine the PrimeGenerator()
method. Given what we already know about CodeConfig, it’s easy to
see that the PrimeGenerator() method describes an
Object Definition that will be registered with the container under
the name “PrimeGenerator” (the method name) and
the type IPrimeGenerator (the return type of the
method).The method needs to return a new
PrimeGenerator but unlike the
OutputFormater class that offers an empty default
constructor, the PrimeGenerator class’ only
public constructor requires an instance of the
PrimeEvaluationEngine class be passed to it. To
satisfy this constructor dependency, we simply create a new
PrimeEvaluationEngine object in-line and pass it
to the new PrimeGenerator class. In this way, the
dependency between PrimeGenerator and
PrimeEvaluationEngine is satisfied in much the
same way as when coded ‘by hand’ as shown in Listing 1.As a slightly more complex [ObjectDef]
example, let’s examine the ConsoleReport() method
next. This method needs to return a new
ConsoleReport instance, but as with the
PrimeGenerator class we lack a zero-argument
public constructor. The only public constructor of the
ConsoleReport class requires an
IOutputFormatter instance and an
IPrimeGenerator instance be provided. In our call
to new up an instance of the ConsoleReport class
in the ConsoleReport()[ObjectDef]
method, we are invoking the other [ObjectDef]
methods themselves to return these types. Since these other methods
in turn return IOutputFormatter and
IPrimeGenerator instances respectively, calls to
these other methods will satisfy the constructor dependency of the
ConsoleReport class and thus permit us to create
a new ConsoleReport to return at the end of the
ConsoleReport() method itself. In this manner, we
are delegating from one [ObjectDef] method to
the other [ObjectDef]
methods to create the object graph that we seek to return from the
call to the ConsoleReport() method.Controlling Properties on ObjectsBut what about the “MaxNumber” property
that is set for the ConsoleReport object in the
XML file in Listing 2? As you can see from Listing 4, setting this
property on our ConsoleReport object is as simple
as…well, setting the property on our
ConsoleReport object! Since our
ConsoleReport() method merely has to return a new
ConsoleReport instance, we are completely free to
use any approach (in code) we choose to modify the
ConsoleReport instance before we return it. In
this case, it’s a simple matter of reading the value out of the
App.Config file and then setting the property to
the desired value before we return the instance of the
ConsoleReport object from the method.Creating and Initializing the Application ContextOnce we have translated the XML configuration file in Listing 2
into the CodeConfig class in Listing 4, we need to tell our application
to use it. For that, we need to switch from encapsulating our container
in the Spring.NET XmlApplicationContext to
encapsulating it in the CodeConfigApplicationContext
instead. Just as the XmlApplicationContext is
designed to use XML as the initial entry point to its configuration
settings, the CodeConfigApplicationContext is
designed to scan assemblies for [Configuration]
classes and [ObjectDef]
methods as the initial entry point to its configuration settings.
Listing 5 shows the CreateContainerUsingCodeConfig()
method from the Program.cs file in the sample
application that demonstrates this process.//Listing 5 (bootstrapping the CodeConfigApplicationContext from Program.cs)
private static IApplicationContext CreateContainerUsingCodeConfig()
{
CodeConfigApplicationContext ctx = new CodeConfigApplicationContext();
ctx.ScanAllAssemblies();
ctx.Refresh();
return ctx;
}After instantiating the
CodeConfigApplicationContext, we next invoke the
ScanAllAssemblies() method to perform the scanning
for the [Configuration]-attributed
classes and [ObjectDef]-attributed
methods within our project. Lastly, the container is initialized by
calling the Refresh() method and then the
ready-to-use context is returned from the method. In the invocation of
the ScanAllAssemblies() method, we are asking the
CodeConfigApplicationContext to scan the current
AppDomain’s root folder and all subfolders recursively for all
assemblies that might contain CodeConfig classes (classes having the
[Configuration]
attribute).More Granular Control Using CodeConfigThe example in Listing 4 and Listing 5 demonstrates only the most
basic use-cases for CodeConfig. More granular control over each of the
definitions is provided by applying additional attributes to the [Configuration]
classes and [ObjectDef]
methods and by setting various values on these attributes. Among these are
the following:[Scope] for
controlling object lifecycle settings on a [ObjectDef]
such as singleton, prototype, etc.[Import] for
chaining [Configuration]
classes together so that you can logically divide your [ObjectDef]
methods among multiple classes in much the same way you may do so with
multiple XML files that provide pointers to other XML files[ImportResource]
for combining [Configuration]
classes with any of Spring.NET’s many implementations of its
IResource abstraction (file://, assembly://, etc.),
the most common one being an XML resource so that you can define part
of your configuration metadata in [Configuration]
classes and other parts of it in XML files as either embedded
resource(s) in your assemblies or as file(s) on disk.[Lazy] for
controlling lazy instantiation of singleton objectsIf you require aliases (additional, multiple names) for the Type
in the container, the [ObjectDef]
attribute also accepts an array of strings that if provided will be
registered as aliases for the Type registration.In addition, finer-grained control of the specific assemblies to
scan, and specific [Configuration]
classes to include and/or exclude is supported by the scanning API. It is
also possible to compose configurations by dividing your [ObjectDef]
methods into multiple different [Configuration]
classes and then to assemble them as building blocks to configure your
container as it is initialized.The CodeConfig approach enables us to express the same configuration
metadata to our Dependency Injection container as the XML file in Listing
2 had provided, but in a form that is at once both significantly more
powerful and flexible as well as more resilient to the refactoring our
container-managed application objects.To address additional common non-XML configuration scenarios, such
as the XML schemas for AOP and Transaction management, Spring.NET is also
evolving a more fluent-style configuration API that will build upon
CodeConfig in even more flexible ways in the near future including
convention-based registration of objects.Organizing and Composing Multiple [Configuration] ClassesThe examples referenced in this
document and provided in this distribution almost all employ merely a
single [Configuration] class
from which their ApplicationContext is to be configured. For these simple
examples this approach is viable but just as is the case when configuring
the ApplicationContext via XML, any significantly complex solution is
likely to require separating your [ObjectDef] methods into
multiple [Configuration]
classes.Just as there are several strategies for effectively managing
multiple XML configuration files, so too are there many options available
to the developer to organize and compose multiple [Configuration] classes
together in a larger solution. This section doesn't attempt to cover all
of the available options in deep detail, but is intended to provide a
high-level understanding of some of the techniques that can be combined
together to help manage multiple such classes. Users familair with the
common techniques for composing together multiple XML configuration files
for Spring.NET will recognize some of these same patterns applied to [Configuration] classes
in the following sections as well.Multiple Stand-Alone Configuration ClassesThe simplest organization approach is providing multiple
stand-alone [Configuration]
classes. In this scenario, [ObjectDef] methods are
organized into separate [Configuration]
classes but each of the [Configuration]
classes is entirely self-contained and unrealted to the others. The
decomposition principles can of course be anything of your choosing. One
simple possibility might be to divide your configuration data between
different kinds of services as in the following example:[Configuration]
public class WcfServicesConfigurations
{
[ObjectDef]
public virtual IWebService MySpecialService()
{
//construct and return a IWebService implementation here
}
}
[Configuration]
public class RepositoryServicesConfigurations
{
[ObjectDef]
public virtual ICustomerRepository MyCustomerRepository()
{
//construct and return a ICustomerRepository implementation here
}
[ObjectDef]
public virtual IShippingRepository MyShippingRepository()
{
//construct and return a IShippingRepository implementation here
}
}In this example, both of these [Configuration]
classes would need to be explicitly scanned and registered with the
CodeConfigApplicationContext
since they each are completely stand-alone and both are needed for the
proper configuration of the ApplicationContext. Since these two classes
in this example have no interdependencies between them, each class may
be placed into a different file or even assembly.High-Level [Configuration] Classes that [Import] OthersAnother strategy for composing multiple [Configuration]
classes together is to devise one or more 'high-level entry-point'
classes and leverage the [Import] attribute
so that the scanning of the high-level class automatically imports one
or more lower-level classes. The high-level classes may contain
[ObjectDef] methods of their own or merely hold reference to one or
more [Import] classes as needed.[Configuration]
[Import(typeof(MyWebServicesConfigurations))]
[Import(typeof(MyMessagingServicesConfigurations))]
[Import(typeof(MyPersistenceServicesConfigurations))]
public class ServicesConfigurations
{
//rest of class here as needed
}In this example, only the
ServicesConfigurations class needs to be scanned
because the [Import]
attributes point directly to the other classes to scan for
[Configuration]
and [ObjectDef]
metadata.Referencing [ObjectDef]s from one [Configuration] Class in
AnotherOnce you decompose your [ObjectDef]
methods into separate classes, often you will find that you have a need
to reference the objects defined in one [Configuration]
class when coding the [ObjectDef]
methods in another [Configuration]
class. The architecture of Spring CodeConfig for .NET makes it simple to
address this need: you can simply ask the ApplicationContext to resolve
the needed Type.To understand how this works, its first important to understand
that [Configuration]
classes are themselves registered as types in the ApplicationContext
in addition to the types defined in their [ObjectDef]
methods. Combining that knowledge with the special
IApplicationContextAware interface in Spring.NET
allows us to ask the ApplicationContext to inject
itself into our [Configuration]
classes. This ApplicationContext is then available to us to resolve
requests for needed types that may be defined elsewhere, whether in
other [Configuration]
classes or perhaps even other XML files.Let's explore the following example where the
SecondConfiguration class needs access to the
TransactionManager that is defined in the
FirstConfiguration class in order to properly build
and configure a CustomerRepository instance:[Configuration]
public class FirstConfiguration
{
[ObjectDef]
public virtual TransactionManager MySpecialTransactionManager()
{
return new TransactionManager();
}
}
[Configuration]
public class SecondConfiguration : IApplicationContextAware //note the interface implementation
{
//field to hold the injected context
private IApplicationContext _context;
//property setter defined by the IApplcationContextAware interface
// so that the context can inject itself into the class
public IApplicationContext ApplicationContext { set { _context = value; } }
[ObjectDef]
public virtual ICustomerRepository CustomerRepository()
{
//to construct the CustomerRepository, we need a TransactionManager instance
// as a constructor argument so let's ask the injected context to resolve one for us
return new CustomerRepository(_context.GetObject<TransactionManager>());
}
}
//somewhere else in your solution the CustomerRepository class is defined as follows...
public class CustomerRespository : ICustomerRespository
{
private TransactionManager _transactionManager;
public CustomerRespository(TransactionManager transactionManager)
{
_transactionManager = transactionManager;
}
}In this way, there is no direct coupling between [Configuration]
classes and the SecondConfiguration class is only aware of the
ApplicationContext itself and the actual Types it needs to construct the
Types described in its [ObjectDef]
methods.