ASP.NET MVC Custom configuration GetSection returns null - asp.net-mvc-4

I'm working on an inherited ASP.NET MVC 4 project using .net framework 4.5.
We've added a new configuration section files and relevant class files and from what we can tell (docs.Microsoft and other online guides) it's set up correctly.
The Problem
ConfigurationManager.GetSection() returns null.
According to the docs this returns null if the section doesn't exist. Troubleshooting this has been troublesome.
The Code
The website is an ASP.NET Web Application. Properties window sets assembly name to Client.Project.UI.Base (which is the DLL in the published bin). This is the assembly name used for the config types FQN and assembly in web.config.
NB: the config section SupportCaseConfiguration was originally in a separate file and the SupportTickets section just specified the configSource. This has been moved into the web.config to reduce the number of potential issues while troubleshooting.
web.config:
<configSections>
<!-- define type for new section -->
<section name="SupportTickets" type="Client.Project.UI.Base.Infrastructure.Services.SupportCaseConfigurationSection, Client.Project.UI.Base"/>
</configSections>
<!-- new config section -->
<SupportTickets>
<SupportCaseConfiguration>
<caseTypes>
<add name="tenant.TestCase" label="Test Case" recipient="email_here" ccList="" bccList="" />
</caseTypes>
</SupportCaseConfiguration>
</SupportTickets>
SupportCaseConfiguration.cs:
namespace Client.Project.UI.Base.Infrastructure.Services
{
using System.Configuration;
//Extend the ConfigurationSection class.
public class SupportCaseConfigurationSection : ConfigurationSection
{
[ConfigurationProperty("caseTypes", IsDefaultCollection = true)]
public CaseTypeElementCollection CaseTypes
{
get { return (CaseTypeElementCollection)this["caseTypes"]; }
}
}
//Extend the ConfigurationElementCollection class.
[ConfigurationCollection(typeof(CaseTypeElement))]
public class CaseTypeElementCollection : ConfigurationElementCollection
{
public CaseTypeElement this[int index]
{
get { return (CaseTypeElement)BaseGet(index); }
set
{
if (BaseGet(index) != null)
BaseRemoveAt(index);
BaseAdd(index, value);
}
}
protected override ConfigurationElement CreateNewElement()
{
return new CaseTypeElement();
}
protected override object GetElementKey(ConfigurationElement element)
{
return ((CaseTypeElement)element).Name;
}
}
//Extend the ConfigurationElement class. This class represents a single element in the collection.
public class CaseTypeElement : ConfigurationElement
{
[ConfigurationProperty("name", IsRequired = true)]
public string Name
{
get { return (string)this["name"]; }
set { this["name"] = value; }
}
[ConfigurationProperty("label", IsRequired = true)]
public string Label
{
get { return (string)this["label"]; }
set { this["label"] = value; }
}
[ConfigurationProperty("recipient", IsRequired = true)]
public string Recipient
{
get { return (string)this["recipient"]; }
set { this["recipient"] = value; }
}
[ConfigurationProperty("ccList", IsRequired = true)]
public string CcList
{
get { return (string)this["ccList"]; }
set { this["ccList"] = value; }
}
[ConfigurationProperty("bccList", IsRequired = true)]
public string BccList
{
get { return (string)this["bccList"]; }
set { this["bccList"] = value; }
}
}
}
Elsewhere, getting new config data:
SupportCaseConfigurationSection supportTicketsConfigurationSection = ConfigurationManager.GetSection("SupportCaseConfiguration") as SupportCaseConfigurationSection;
The site is being published locally, I can attach a debugger to ensure the latest versions of files are being used. I can see the config section in the published web.config.
I've been looking at this I can no longer see if anything is amiss. It all looks fine for me...
Any ideas, troubleshooting tips or even pointing out I'm being a muppet would be useful.
Cheers.

I can only assume the issue was something to do with the config classes in the site assembly.
Even after trying online examples, copypasting into the project, not even they worked.
As soon as I put the configuration section/element classes in a separate project (moved out of the website project) it started working.

Related

How to access appsettings.json from module Configure Services

I have a blazor server side solution that contains an appsettings.json
I am configuring the blob storage in the ConfigureServices override in the Application Module of the Applications project. It currently has a hard coded connection string and is working perfectly.
Now I want to move the connection string to the appsettings.json file that is in the Blazor project.
I've tried to inject the IConfiguration into the constructor of the ApplicationModule, but the app throws an error when I try to do so.
I've searched through the ServiceConfigurationContext passed into to the ConfigureServices override. There is a Service property containing a collection of around 1,024 ServiceDescriptors and found one that contains the word IConfiguration in the ServiceType.FullName but haven't been able to figure out how to use it to get at the service itself in order to get at the appsettings.json values.
Can anyone shed any light on how to access the appsettings.json values from the application module?
Here is my code I am working with
namespace MyApp
{
[DependsOn(
typeof(MyAppDomainModule),
typeof(AbpAccountApplicationModule),
typeof(MyAppApplicationContractsModule),
typeof(AbpIdentityApplicationModule),
typeof(AbpPermissionManagementApplicationModule),
typeof(AbpTenantManagementApplicationModule),
typeof(AbpFeatureManagementApplicationModule),
typeof(AbpSettingManagementApplicationModule),
typeof(AbpBlobStoringModule),
typeof(AbpBlobStoringAzureModule)
)]
public class MyAppApplicationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.UseAzure(azure =>
{
azure.ConnectionString = "DefaultEndpointsProtocol=https;AccountName=MyApplocalsa;AccountKey=<truncated-account-key>;EndpointSuffix=core.windows.net";
azure.ContainerName = "Pictures";
azure.CreateContainerIfNotExists = true;
});
});
});
}
}
}
This answer has been update based on new information in the question.
If I understand the context correctly you are building your own DI services container within MyAppApplicationModule. As I don't have enough detail on MyAppApplicationModule, I'll demonstrate how you get to apllication configuration data in the context of OwningComponentBase which also defines it's own DI services container. Note I'm using Net6.0 here.
First the configuation data in appsettings.json of the web project.
{
"AzureData": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=MyApplocalsa;AccountKey=<truncated-account-key>;EndpointSuffix=core.windows.net",
"ContainerName": "Pictures"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Next define a data class to hold the configuration data
public class AzureData
{
public readonly Guid Id = Guid.NewGuid();
public string ConnectionString { get; set; } = string.Empty;
public string ContainerName { get; set; } = string.Empty;
}
Now register a configuration instance binding an AzureData instance against a section in the configuration file.
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
builder.Services.AddSingleton<WeatherForecastService>();
builder.Services.Configure<AzureData>(builder.Configuration.GetSection("AzureData"));
Finally the component.
Note:
We use IOptions<AzureData> to get the specific configuration instance, and Value to get the actual object.
AzureData is the same DI object, inside or outside the local service container. It's defined as a singleton.
#page "/di"
#inherits OwningComponentBase
#using Microsoft.Extensions.Options
<h3>DI Component</h3>
<div class="m-2 p-2">
Main Service Container <br />
Id: #AzureDataConfig?.Value.Id <br />
Connection String: #AzureDataConfig?.Value.ConnectionString
</div>
<div class="m-2 p-2">
Component Service Container <br />
Id:#azureData?.Value.Id <br />
Connection String: #azureData?.Value.ConnectionString
</div>
#code {
[Inject] private IOptions<AzureData>? AzureDataConfig { get; set; }
private IOptions<AzureData>? azureData;
protected override void OnInitialized()
{
azureData = ScopedServices.GetService<IOptions<AzureData>>();
base.OnInitialized();
}
}
I finally figured out the answer to the question by looking at other modules in the solution.
Here is the updated code
namespace MyApp
{
[DependsOn(
typeof(MyAppDomainModule),
typeof(AbpAccountApplicationModule),
typeof(MyAppApplicationContractsModule),
typeof(AbpIdentityApplicationModule),
typeof(AbpPermissionManagementApplicationModule),
typeof(AbpTenantManagementApplicationModule),
typeof(AbpFeatureManagementApplicationModule),
typeof(AbpSettingManagementApplicationModule),
typeof(AbpBlobStoringModule),
typeof(AbpBlobStoringAzureModule)
)]
public class MyAppApplicationModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<AbpAutoMapperOptions>(options =>
{
options.AddMaps<MyAppApplicationModule>();
});
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.UseAzure(azure =>
{
azure.ConnectionString = configuration.GetSection("BlobStorage:ConnectionString").Value;
azure.ContainerName = configuration.GetSection("BlobStorage:ContainerName").Value;
azure.CreateContainerIfNotExists = true;
});
});
});
}
}
}
I needed to add the using
using Microsoft.Extensions.DependencyInjection;
I was able to get a reference to the configuration
var configuration = context.Services.GetConfiguration();
I updated the hard coded connection string with retrieving it from the configuration.
azure.ConnectionString = configuration.GetSection("BlobStorage:ConnectionString").Value;
azure.ContainerName = configuration.GetSection("BlobStorage:ContainerName").Value;
I updated the appsettings.json file in my Blazor app
"BlobStorage": {
"ConnectionString": "DefaultEndpointsProtocol=https;AccountName=myapplocalsa;AccountKey=<truncated>;EndpointSuffix=core.windows.net",
"ContainerName" : "Pictures"
}
That was it!
Thank you Joe for investing your time in providing answers to my question!
For others who might be looking for a solution to the same problem - I have a couple of things to add. I was using a separated tenant with separate Product.IdentityServer, Product.Web, and Product.HttpApi.Host projects.
The configuration I was trying to perform was for the AbpTwilioSmsModule and AbpBlobStoringModule. The values for these modules were hardcoded into my
Product.Domain.ProductDomainModule class.
// TODO - Need to configure this through appsettings.json - JLavallet 2022-02-10 12:23
Configure<AbpTwilioSmsOptions>(options =>
{
options.AccountSId = "yada-yada-yada";
options.AuthToken = "yada-yada-yada";
options.FromNumber = "+18885551212";
});
// TODO - Need to configure this through appsettings.json - JLavallet 2022-02-10 12:24
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.IsMultiTenant = true;
container.UseFileSystem(fileSystem => { fileSystem.BasePath = #"D:\Product\DevFiles"; });
});
});
I modified that code to try and read from the context just like the OP. I wasn't sure what property of the context contained the configuration. I tried all kind of things and set breakpoints to try and find the configuration object in the context without success.
Configure<AbpTwilioSmsOptions>(options =>
{
options.AccountSId = context.WHAT?["AbpTwilioSms:AccountSId"];
options.AuthToken = context.WHAT?["AbpTwilioSms:AuthToken"];
options.FromNumber = context.WHAT?["AbpTwilioSms:FromNumber"];
});
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.IsMultiTenant = Convert.ToBoolean(context.WHAT?["AbpBlobStoring:IsMultiTenant"]);
container.UseFileSystem(fileSystem =>
{
fileSystem.BasePath = context.WHAT?["AbpBlobStoring:FileSystemBasePath"];
});
});
});
At that point I came across this post and found how to get the configuration object out of the context.
Not all was well, however…
For the longest time I could not understand why I could not read my appsettings.json configuration information that I had placed in the Product.HttpApi.Host root folder. I was able to get to the configuration object but my values were still null.
I then had the thought that I should add an appsettings.json file to my Product.Domain root folder; surprisingly that had no effect.
I finally came around to moving the service configuration code out of my Product.Domain.ProductDomainModule class and into my Product.HttpApi.Host.ProductHttpApiHostModule class and my Product.IdentityServer.ProductIdentityServerModule class.
[DependsOn(
typeof(ProductHttpApiModule),
typeof(AbpAutofacModule),
typeof(AbpCachingStackExchangeRedisModule),
typeof(AbpAspNetCoreMvcUiMultiTenancyModule),
typeof(AbpIdentityAspNetCoreModule),
typeof(ProductApplicationModule),
typeof(ProductEntityFrameworkCoreModule),
typeof(AbpSwashbuckleModule),
typeof(AbpAspNetCoreSerilogModule)
)]
// added by Jack
[DependsOn(typeof(AbpTwilioSmsModule))]
[DependsOn(typeof(AbpBlobStoringModule))]
[DependsOn(typeof(AbpBlobStoringFileSystemModule))]
public class ProductHttpApiHostModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
var hostingEnvironment = context.Services.GetHostingEnvironment();
ConfigureUrls(configuration);
ConfigureConventionalControllers();
ConfigureAuthentication(context, configuration);
ConfigureSwagger(context, configuration);
ConfigureCache(configuration);
ConfigureVirtualFileSystem(context);
ConfigureDataProtection(context, configuration, hostingEnvironment);
ConfigureCors(context, configuration);
ConfigureExternalProviders(context);
ConfigureHealthChecks(context);
ConfigureTenantResolver(context, configuration);
//added by Jack
ConfigureTwilioSms(configuration);
ConfigureBlobStoring(configuration);
}
private void ConfigureBlobStoring(IConfiguration configuration)
{
Configure<AbpBlobStoringOptions>(options =>
{
options.Containers.ConfigureDefault(container =>
{
container.IsMultiTenant = Convert.ToBoolean(configuration["AbpBlobStoring:IsMultiTenant"]);
container.UseFileSystem(fileSystem =>
{
fileSystem.BasePath = configuration["AbpBlobStoring:FileSystemBasePath"];
});
});
});
}
private void ConfigureTwilioSms(IConfiguration configuration)
{
Configure<AbpTwilioSmsOptions>(options =>
{
options.AccountSId = configuration["AbpTwilioSms:AccountSId"];
options.AuthToken = configuration["AbpTwilioSms:AuthToken"];
options.FromNumber = configuration["AbpTwilioSms:FromNumber"];
});
}
I then copied my configuration entries from the Product.HttpApi.Host\appsettings.json file into my Product.IdentityServer\appsettings.json file and everything worked beautifully.
{
...,
"AbpTwilioSms": {
"AccountSId": "yada-yada-yada",
"AuthToken": "yada-yada-yada",
"FromNumber": "+18885551212"
},
"AbpBlobStoring": {
"IsMultiTenant": true,
"FileSystemBasePath": "D:\\Product\\DevFiles\\"
}
}

NHibernate Spatial Mapping returns Antrl error

I am using NHibernate 3.3. I have a case where I want to insert a calculated column which is not referenced.
My Domain Entity can be reduced to the form
public class Location
{
public virtual IPoint GeoLocation {get;set;}
}
public class MappingOverride : IAutoMappingOverride<Location>
{
public void Override(AutoMapping<Location> mapping)
{
mapping
.Map(e => e.GeoLocation)
.Column("GeoLocation")
.CustomSqlType("geography")
.CustomType<MsSql2008GeographyType>;
}
}
The table column is of type 'Geography`
However it errors out as
at System.Reflection.RuntimeAssembly.GetExportedTypes(RuntimeAssembly assembly, ObjectHandleOnStack retTypes)
at System.Reflection.RuntimeAssembly.GetExportedTypes()
at GeoAPI.GeometryServiceProvider.GetLoadableTypes(Assembly assembly)
at GeoAPI.GeometryServiceProvider.ReflectInstance()
at GeoAPI.GeometryServiceProvider.get_Instance()
at NetTopologySuite.Geometries.Geometry.set_SRID(Int32 value)
It says that it needs Antrl.Runtime, but a very old version. All the Antrl.Runtime nuget packages out there have a different assembly identifier.
Could not load file or assembly 'antlr.runtime, Version=2.7.6.2, Culture=neutral, PublicKeyToken=1790ba318ebc5d56' or one of its dependencies. The system cannot find the file specified.
I worked on a separate project where I used map by code convention and it works without any reference to Antrl.Runtime.
Need help to point myself in the right direction...
As a separate sample project I tried something which worked, WHICH DID NOT WORK in the actual ASP.NET MVC project.
Definition of Class
public class Location : IEntity
{
public virtual int Id { get; protected set; }
public virtual string LocationName { get; set; }
public virtual IPoint GeoLocation { get; set; }
}
Mapping
public class LocationMapping : IAutoMappingOverride<Location>
{
public void Override(AutoMapping<Location> mapping)
{
mapping.Map(x => x.LocationName).Column("Name");
mapping.Map(x => x.GeoLocation).Column("GeoLocation")
.CustomType<MsSql2008GeographyType>();
}
}
Test
public class WhenSavingGeoLocation : BasePersistanceTest
{
[Test]
public void Save()
{
this.Session
.SaveOrUpdate(new Location {
LocationName = "Category 01",
GeoLocation = new Point(-72.12, 32.2323234) { SRID = 4326 }
});
}
}
NHibernate.Spatial uses NetTopologicalSuite which itself is a derivative of GeoAPI
The GeoAPI library defines an interface GeoAPI.Geometries and while loading / searching the correct implementation in the current project was failing with the Antlr error in the MVC project.
It was however able to fetch the NetTopologySuite.NtsGeometryServices implementation in the separate sample project. The actual code in GeoAPI which searches for the implementation goes like
private static IGeometryServices ReflectInstance()
{
#if !PCL
var a = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in a)
{
// Take a look at issue 114: http://code.google.com/p/nettopologysuite/issues/detail?id=114
if (assembly is System.Reflection.Emit.AssemblyBuilder) continue;
if (assembly.GetType().FullName == "System.Reflection.Emit.InternalAssemblyBuilder") continue;
if (assembly.GlobalAssemblyCache && assembly.CodeBase == Assembly.GetExecutingAssembly().CodeBase) continue;
foreach (var t in GetLoadableTypes(assembly))
{
if (t.IsInterface) continue;
if (t.IsAbstract) continue;
if (t.IsNotPublic) continue;
if (!typeof(IGeometryServices).IsAssignableFrom(t)) continue;
var constuctors = t.GetConstructors();
foreach (var constructorInfo in constuctors)
{
if (constructorInfo.IsPublic && constructorInfo.GetParameters().Length == 0)
return (IGeometryServices)Activator.CreateInstance(t);
}
}
}
#endif
throw new InvalidOperationException("Cannot use GeometryServiceProvider without an assigned IGeometryServices class");
}
}
This gets called when setting the SRID on the geometry. I am guessing that my MVC project has a reference to an assembly which makes this look for Antlr.
There were some suggestions of adding a redirect for to newer Antlr assembly. I tried that as well but the error still kept repeating.

BizTalk Service Authorization

I'm trying to implement authorization as Seroter described here (Service Authorization section). I've GAC'ed the library, changed machine.config and able to pick the custom behavior in Select Behavior Extension dialog. But I can't set the 'WindowsGroup' value, it gives me "Object reference not set to an instance of an object" and I can't figure why. Did anybody implement service authorization?
Finally solved this problem.
using System;
using System.Configuration;
using System.ServiceModel.Configuration;
namespace Esb.Service.Authorization
{
public class EsbBehaviorElement : BehaviorExtensionElement
{
private const string _windowsgroupIndexName = "windowsgroup";
public EsbBehaviorElement()
{
if (!base.Properties.Contains(_windowsgroupIndexName))
{
base.Properties.Add(new ConfigurationProperty(_windowsgroupIndexName, typeof(string)));
}
}
[ConfigurationProperty("WindowsGroup", IsRequired = false, DefaultValue = "")]
public string WindowsGroup
{
get
{
return (string)base[_windowsgroupIndexName];
}
set
{
base[_windowsgroupIndexName] = value;
}
}
public override Type BehaviorType
{
get
{
return typeof(EsbServiceBehavior);
}
}
protected override object CreateBehavior()
{
return new EsbServiceBehavior(WindowsGroup);
}
}
}
I don't know why Seroter's solution works without ctor where one should add "windowsgroup" property to the base collection of properties.

Avoiding Service Locator with AutoFac 2

I'm building an application which uses AutoFac 2 for DI. I've been reading that using a static IoCHelper (Service Locator) should be avoided.
IoCHelper.cs
public static class IoCHelper
{
private static AutofacDependencyResolver _resolver;
public static void InitializeWith(AutofacDependencyResolver resolver)
{
_resolver = resolver;
}
public static T Resolve<T>()
{
return _resolver.Resolve<T>();
}
}
From answers to a previous question, I found a way to help reduce the need for using my IoCHelper in my UnitOfWork through the use of Auto-generated Factories. Continuing down this path, I'm curious if I can completely eliminate my IoCHelper.
Here is the scenario:
I have a static Settings class that serves as a wrapper around my configuration implementation. Since the Settings class is a dependency to a majority of my other classes, the wrapper keeps me from having to inject the settings class all over my application.
Settings.cs
public static class Settings
{
public static IAppSettings AppSettings
{
get
{
return IoCHelper.Resolve<IAppSettings>();
}
}
}
public interface IAppSettings
{
string Setting1 { get; }
string Setting2 { get; }
}
public class AppSettings : IAppSettings
{
public string Setting1
{
get
{
return GetSettings().AppSettings["setting1"];
}
}
public string Setting2
{
get
{
return GetSettings().AppSettings["setting2"];
}
}
protected static IConfigurationSettings GetSettings()
{
return IoCHelper.Resolve<IConfigurationSettings>();
}
}
Is there a way to handle this without using a service locator and without having to resort to injecting AppSettings into each and every class? Listed below are the 3 areas in which I keep leaning on ServiceLocator instead of constructor injection:
AppSettings
Logging
Caching
I would rather inject IAppSettings into every class that needs it just to keep them clean from the hidden dependency on Settings. Question is, do you really need to sprinkle that dependency into each and every class?
If you really want to go with a static Settings class I would at least try to make it test-friendly/fakeable. Consider this:
public static class Settings
{
public static Func<IAppSettings> AppSettings { get; set; }
}
And where you build your container:
var builder = new ContainerBuilder();
...
var container = builder.Build();
Settings.AppSettings = () => container.Resolve<IAppSettings>();
This would allow to swap out with fakes during test:
Settings.AppSettings = () => new Mock<IAppSettings>().Object;
Now the AppSettings class (which I assume there is only one of) you could do with regular constructor injection. I assume also that you really want to do a resolve on each call to your settings properties, thus injecting a factory delegate that retrieves an instance when needed. If this is not needed you should of course inject the IConfigurationSettings service directly.
public class AppSettings : IAppSettings
{
private readonly Func<IConfigurationSettings> _configurationSettings;
public AppSettings(Func<IConfigurationSettings> configurationSettings)
{
_configurationSettings = configurationSettings;
}
public string Setting1
{
get
{
return _configurationSettings().AppSettings["setting1"];
}
}
public string Setting2
{
get
{
return _configurationSettings().AppSettings["setting2"];
}
}
}

Castle Windsor Fluent API: Define Array with Single item as Dependency

Given this XML configuration (which works)
<component type="X.Y.Z.ActivityService, X.Y.Z.Services" id="X.Y.Z.ActivityService" lifestyle="transient">
<parameters>
<Listeners>
<array>
<item>${DefaultActivityListener}</item>
</array>
</Listeners>
</parameters>
</component>
<component type="X.Y.Z.DefaultActivityListener, X.Y.Z.Services" id="DefaultActivityListener" lifestyle="transient" />
I have converted to use the fluent API as below (which doesn't work):
Container.Register(
Component.For<X.Y.Z.ActivityService>()
.ServiceOverrides(
ServiceOverride.ForKey("Listeners").Eq(typeof(X.Y.Z.DefaultActivityListener).Name))
.LifeStyle.Transient
);
Container.Register(
Component.For<X.Y.Z.DefaultActivityListener>()
.Named("DefaultActivityListener")
.LifeStyle.Transient
);
When I now attempt to resolve an instance of X.Y.Z.ActivityService Windsor throws a NotImplementedException in Castle.MicroKernel.SubSystems.Conversion.ArrayConverter.PerformConversion(String, Type).
The implementation of the PerformConversion method is:
public override object PerformConversion(String value, Type targetType)
{
throw new NotImplementedException();
}
I should add that if I remove the ServiceOverrides call, all behaves as expected. So there is specifically something wrong in the way I am wiring up the Listeners parameter. Listeners by the way is a property as opposed to a constructor parameter.
Seeing as the XML config works as expected how do I best use the fluent API (short of implementing the PerformConversion method) in order to achieve the same result?
I am using Release 2.0.
EDIT
I will extend the question to how would you achieve this configuration in code, with or without use of the fluent API.
UPDATE
It appears the problem occurs if you attempt to assign a single element to an array property. Unit tests provided below to illustrate issue.
namespace Components
{
public class A
{
public I[] I { get; set; }
}
public interface I
{
string Name { get; }
}
public class B : I
{
public string Name { get { return "B"; } }
}
public class C : I
{
public string Name { get { return "C"; } }
}
}
[TestMethod]
public void ArrayPropertyTestApi()
{
//PASSES
using (Castle.Windsor.WindsorContainer container = new Castle.Windsor.WindsorContainer())
{
container.Register(Component.For<Components.A>().ServiceOverrides(ServiceOverride.ForKey("I").Eq(typeof(Components.B).FullName, typeof(Components.C).FullName)));
container.Register(Component.For<Components.B>());
container.Register(Component.For<Components.C>());
Components.A svc = container.Resolve<Components.A>();
Assert.IsTrue(svc.I.Length == 2);
Assert.IsTrue(svc.I[0].Name == "B");
Assert.IsTrue(svc.I[1].Name == "C");
}
}
[TestMethod]
public void ArrayPropertyTestApi2()
{
//FAILS
using (Castle.Windsor.WindsorContainer container = new Castle.Windsor.WindsorContainer())
{
container.Register(Component.For<Components.A>().ServiceOverrides(ServiceOverride.ForKey("I").Eq(typeof(Components.B).FullName)));
container.Register(Component.For<Components.B>());
container.Register(Component.For<Components.C>());
Components.A svc = container.Resolve<Components.A>(); //Throws NotImplementedException
Assert.IsTrue(svc.I.Length == 1);
Assert.IsTrue(svc.I[0].Name == "B");
}
}
Question still stands.
Thanks.
[TestFixture]
public class WindsorTests {
[Test]
public void ArrayConfig() {
var container = new WindsorContainer();
container.Register(Component.For<Listener>().Named("listener"));
container.Register(Component.For<ActivityService>()
.ServiceOverrides(ServiceOverride.ForKey("listeners").Eq(new[] {"listener"})));
var service = container.Resolve<ActivityService>();
Assert.AreEqual(1, service.Listeners.Length);
}
}
public class Listener {}
public class ActivityService {
public Listener[] Listeners { get; set; }
}
The key part here is the new[] {"listener"}. The MicroKernel needs to know that the parameter listeners is an array, if you pass just "listener" it assumes that the parameter is scalar and throws because it can't convert a scalar to an array.