Reputation: 675
In my project I have a few objects that are used as part of multiple services. Following is a brief example of the architecture (interfaces left out intentionally):
public class Plc : IPlc
{
public Plc(string connectionString) { ... }
}
public class PlcRegister
{
public string Address { get; set; }
public int Length { get; set; }
}
public class PlcJobWriter : IPlcJobWriter
{
public PlcJobWriter(IPlc plc,
PlcRegister commandRegister,
PlcRegister statusRegister,
...)
{ ... }
}
public class JobService : IJobService
{
public JobService(Dictionary<string, PlcJobWriter> plcJobWriters) { ... }
}
public class PlcProgramWriter : IPlcProgramWriter
{
public PlcProgramWriter(IPlc plc,
PlcRegister commandRegister,
PlcRegister statusRegister,
...)
{ ... }
}
public class ProgramService : IProgramService
{
public ProgramService(List<PlcProgramWriter> plcProgramWriters) { ... }
}
What I will get in production is one or more PLC's. Every Plc
will have one PlcProgramWriter
and one or more PlcJobWriter
. Any Program update will be written out to all PlcProgramWriters (thus the plcProgramWriters
param in ProgramService
and a job will be written out to the matching PlcJobWriter
, matched on key in plcJobWriters
in the JobService
constructor.
I want the values required for the Plc
connectionString and the commandRegister and statusRegister for the PlcJobWriter
and PlcProgramWriter
configurable through app.config, as well as the keys that the PlcJobWriter
uses to find the matching JobWriter.
I could just define my own configuration schema and write code to handle the configuration data, but I'm far more interested in what approach I could take if I'd want to have Windsor handle all of it. I don't mind adding infrastructure to make this possible, but I can't find any examples of such a solution at all (probably looking for the wrong stuff).
Upvotes: 0
Views: 1695
Reputation: 12951
I think this should be possible using an approach as follows:
Firstly you will need this in the configSections
in the web.config. The name can be anything:
<section name="castle" type="Castle.Windsor.Configuration.AppDomain.CastleSectionHandler, Castle.Windsor" />
Then in web.config you create your castle
section containing your DI config. I've tried to give the best example I can here based on your code and description of your requirements. The important points are that you use the ${ }
syntax to reference other components by their ID, the type names will need to be fully qualified with namespace and assembly, and if you've got more than one implementation defined for each interface then you'll probably have to resolve by key rather than interface type.
<castle>
<properties>
<connectionString>YourConnectionStringHere</connectionString>
</properties>
<components>
<component id="PlcA" service="Namespace.IPlc, AssemblyName" type="Namespace.Plc, AssemblyName">
<parameters>
<connectionString>${connectionString}</connectionString>
</parameters>
</component>
<!-- Other IPlc implementations as required -->
<component id="PlcJobWriterA" service="Namespace.IPlcJobWriter, AssemblyName" type="Namespace.PlcJobWriter, AssemblyName">
<parameters>
<plc>${PlcA}</plc>
<commandRegister>
<parameters>
<address>CommandRegisterAddress</address>
<length>CommandRegisterLength</length>
<parameters>
</commandRegister>
<statusRegister>
<parameters>
<address>StatusRegisterAddress</address>
<length>StatusRegisterLength</length>
<parameters>
</statusRegister>
</parameters>
</component>
<!-- Other PlcJobWriters as required -->
<!-- PlcProgramWriter can be configured in the same way -->
<component id="JobService" service="Namespace.IJobService, AssemblyName" type="Namespace.JobService, AssemblyName">
<parameters>
<plcJobWriters>
<dictionary keyType="System.String" valueType="Namespace.IPlcJobWriter, AssemblyName">
<entry key="JobWriterA">${PlcJobWriterA}</entry>
<!-- Other entries as required -->
</dictionary>
</plcJobWriters>
</parameters>
</component>
<component id="ProgramService" service="Namespace.IProgramService, AssemblyName" type="Namespace.ProgramService, AssemblyName">
<parameters>
<plcProgramWriters>
<list>
<item>${ProgramWriterA}</item>
<!-- Other items as required -->
</list>
</plcProgramWriters>
</parameters>
</component>
</components>
</castle>
You would then pass the name of the config section to your Windsor container when you create it:
using Castle.Core.Resource;
using Castle.Windsor;
using Castle.Windsor.Configuration.Interpreters;
...
using (var castleConfig = new ConfigResource("castle"))
{
container = new WindsorContainer(new XmlInterpreter(castleConfig));
}
Obviously that's not a complete working solution for you but hopefully it should illustrate how to use Castle's XML configuration. Castle's error messages tend to be pretty informative about anything that's amiss. More info can be found here: http://docs.castleproject.org/Windsor.XML-Registration-Reference.ashx
Upvotes: 1
Reputation: 21191
This doesn't really answer the question as asked, but if one of your requirements is met by getting typed access to web.config values, then you have two options.
One, you could create a class that derives from ConfigurationSection
, which requires that you register the type in the web.config <configSections>
, uh, section, like so:
<section name="sectionName" type="Fully.Qualified.Namespace.CustomSettings, YourAssemblyName" />
and then the section itself looks something like:
<customSettings propertyName1="somevalue" />
It has the advantage of being able to enforce some rules via data annotations. Here's an implementation I used at one point for an Akismet helper:
public class AkismetSettings : ConfigurationSection
{
private static AkismetSettings settings = ConfigurationManager.GetSection("akismet") as AkismetSettings;
public static AkismetSettings Settings
{
get
{
return settings;
}
}
[ConfigurationProperty("key", IsRequired = true)]
public string Key
{
get { return (string)this["key"]; }
set { this["key"] = value; }
}
[ConfigurationProperty("registeredsite", IsRequired = true)]
public string RegisteredSite
{
get { return (string)this["registeredsite"]; }
set { this["registeredsite"] = value; }
}
}
with its relevant section:
<akismet key="0000000000" registeredsite="http://example.com/" />
I would then use it like so:
var helper = new AkismetHelper(AkismetSettings.Settings.Key, AkismetSettings.Settings.RegisteredSite);
I didn't have a persistent need for the helper, and it seemed like overkill to resolve an instance via Windsor, so I just new'd one up when I needed it.
Two, the option I prefer, is to simply create a class that reads the relevant values from <appSettings>
and does the necessary type conversion.
Here's a trimmed-down version from what I'm using at the moment.
public class AppSettings
{
public static int MinPasswordLength
{
get
{
// Recommended minimum password length.
// See https://www.owasp.org/index.php/Password_length_%26_complexity
int min = 8;
if (!string.IsNullOrWhiteSpace(ConfigurationManager.AppSettings["app.minPasswordLength"]))
{
min = int.Parse(ConfigurationManager.AppSettings["app.minPasswordLength"]);
}
return min;
}
}
/* ... */
public static string Find(string key)
{
return ConfigurationManager.AppSettings[key] ?? string.Empty;
}
}
You could use the same idea to write a somewhat cleaner accessor for the <connectionStrings>
section. If you want to use <appSettings>
, but don't want a lot of values in the web.config, it helps to remember that <appSettings>
has a file
attribute that can be used to link to another .config
file that also contains an <appSettings>
section:
<appSettings file="settings.config">
Upvotes: 1