First off, Nicholi's answer inspired me! Thanks Nicholi.
Second, I have a "List" solution rather than a IDictionary solution. It's not as smooth as the IDictionary solution.
This can also be dubbed "how to create a collection list for dot net core configuration"
Here we go:
first a shameless theft!
public class ConnectionStringEntry
{
public String Name { get; set; }
public String ConnectionString { get; set; }
public String ProviderName { get; set; }
public ConnectionStringEntry()
{
}
public ConnectionStringEntry(String name, String connectionString)
: this(name, connectionString, null)
{
}
public ConnectionStringEntry(String name, String connectionString, String providerName)
{
this.Name = name;
this.ConnectionString = connectionString;
this.ProviderName = providerName;
}
}
second, a "wrapper". I wanted to track a DefaultConnectionStringName...along side my List (collection) of Entries.
public class ConnectionStringWrapper
{
public string DefaultConnectionStringName { get; set; } = "";
public List<ConnectionStringEntry> ConnectionStringEntries { get; set; } = new List<ConnectionStringEntry>();
//public Dictionary<string, ConnectionStringEntry> ConnectionStringEntries { get; set; } = new Dictionary<string, ConnectionStringEntry>();
public ConnectionStringEntry GetDefaultConnectionStringEntry()
{
ConnectionStringEntry returnItem = this.GetConnectionStringEntry(this.DefaultConnectionStringName);
return returnItem;
}
public ConnectionStringEntry GetConnectionStringEntry(string name)
{
ConnectionStringEntry returnItem = null;
if (null != this.ConnectionStringEntries && this.ConnectionStringEntries.Any())
{
returnItem = this.ConnectionStringEntries.FirstOrDefault(ce => ce.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
}
if (null == returnItem)
{
throw new ArgumentOutOfRangeException(string.Format("No default ConnectionStringEntry found. (ConnectionStringEntries.Names='{0}', Search.Name='{1}')", this.ConnectionStringEntries == null ? string.Empty : string.Join(",", this.ConnectionStringEntries.Select(ce => ce.Name)), name));
}
return returnItem;
}
}
Now, my reading the json and mapping to a concrete settings object code:
IConfiguration config = new ConfigurationBuilder()
.SetBasePath(System.IO.Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.Build();
ConnectionStringWrapper settings = new ConnectionStringWrapper();
config.Bind("ConnectionStringWrapperSettings", settings);
Console.WriteLine("{0}, {1}", settings.DefaultConnectionStringName, settings.ConnectionStringEntries.Count);
ConnectionStringEntry cse = settings.GetDefaultConnectionStringEntry();
My nuget packages:
\.nuget\packages\microsoft.extensions.configuration\2.1.1
\.nuget\packages\microsoft.extensions.configuration.binder\2.1.1
\.nuget\packages\microsoft.extensions.configuration.json\2.1.1
BONUS MATERIAL BELOW:
One of the things I am (trying) to so support a code base that can be deployed as DotNet 4.x ("classic" ?? as the term now??) and dotnet core.
To that end, I've written the above to provide an abstraction from the way DotNet(Classic) handles connection strings (xml, our old long time friend) and now the new cool kid on the block : DotNetCore with json.
To that end, I've written an interface:
public interface IConnectionStringWrapperRetriever
{
ConnectionStringWrapper RetrieveConnectionStringWrapper();
}
and I have an implementation for dotnetcore:
public class ConnectionStringWrapperDotNetCoreRetriever : IConnectionStringWrapperRetriever
{
public const string ConnectionStringWrapperSettingsJsonElementName = "ConnectionStringWrapperSettings";
private readonly IConfiguration config;
public ConnectionStringWrapperDotNetCoreRetriever(IConfiguration cnfg)
{
this.config = cnfg;
}
public ConnectionStringWrapper RetrieveConnectionStringWrapper()
{
ConnectionStringWrapper settings = new ConnectionStringWrapper();
this.config.Bind(ConnectionStringWrapperSettingsJsonElementName, settings);
return settings;
}
}
Oh yeah, the all important JSON setup:
{
"ConnectionStringWrapperSettings": {
"DefaultConnectionStringName": "abc",
"ConnectionStringEntries": [
{
"Name": "abc",
"ConnectionString": "Server=myserver;Database=mydatabase;Trusted_Connection=True;MultipleActiveResultSets=true",
"ProviderName": "SomeProvider"
},
{
"Name": "def",
"ConnectionString": "server=localhost;database=db2;username=user2;password=pass2;",
"ProviderName": "SomeProvider"
}
]
}
}
.............
For DotNet(classic), all you need to do is implement a second concrete for IConnectionStringWrapperRetriever, and do the magic.
Remember the below xml? (ha ha, its not that old yet!)
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<connectionStrings>
<add name="ConnStr1" connectionString="LocalSqlServer: data source=127.0.0.1;Integrated Security=SSPI;Initial Catalog=aspnetdb"
providerName="System.Data.SqlClient" />
</connectionStrings>
</configuration>
(from https://learn.microsoft.com/en-us/dotnet/api/system.configuration.connectionstringsettingscollection?view=netframework-4.7.2)
Remember this stuff from EnterpriseLibrary?
<configSections>
<section name="dataConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=null"/>
</configSections>
<dataConfiguration defaultDatabase="ConnStr1"/>
I'll leave the DotNet(Classic) implementation to the reader.
But now I inject IConnectionStringWrapperRetriever into my DataLayer classes.
I'm using Dapper, so I can fish a connection string using IConnectionStringWrapperRetriever.
If my project is "house" DotNet(Classic), I inject one version of IConnectionStringWrapperRetriever (not seen here, left to the reader). If my project is "housed" in DotNetCore I inject a second (shown above) version of IConnectionStringWrapperRetriever.
Outside the scope of this post, but by "housed", I mean I have 2 csproj's sitting side by side.
MyApp.DataLayer.classic.csproj
and
MyApp.DataLayer.csproj
I find it easier to leave the default csproj to house the DotNetCore stuff. And I use the "classic.csproj" file to house the DotNet(classic). My assembly name and default namespace remain "MyApp.Datalayer"......the .classic is ONLY for the csrproj filename to distinquish.
I create two solution sln files too. MySolution.classic.sln and MySolution.sln.
It seems to be working.....with this ConnectionString abstraction I wrote above.
The ONLY conditional I have is on the (classic) AssemblyInfo.cs files.
#if(!NETCOREAPP2_1)
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
/* all the other stuff removed here */
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
#endif
APPEND:
Ok, here is the DotNet(Classic) version:
public class ConnectionStringWrapperDotNetClassicRetriever : IConnectionStringWrapperRetriever
{
public ConnectionStringWrapper RetrieveConnectionStringWrapper()
{
ConnectionStringWrapper returnItem = new ConnectionStringWrapper();
foreach(ConnectionStringSettings css in System.Configuration.ConfigurationManager.ConnectionStrings)
{
ConnectionStringEntry cse = new ConnectionStringEntry(css.Name, css.ConnectionString, css.ProviderName);
returnItem.ConnectionStringEntries.Add(cse);
}
if(returnItem.ConnectionStringEntries.Count == 1)
{
/* if there is only one, set the default name to that one */
returnItem.DefaultConnectionStringName = returnItem.ConnectionStringEntries.First().Name;
}
else
{
/*
<packages>
<package id="EnterpriseLibrary.Common" version="6.0.1304.0" targetFramework="net45" />
<package id="EnterpriseLibrary.Data" version="6.0.1304.0" targetFramework="net45" />
</packages>
*/
/* using Microsoft.Practices.EnterpriseLibrary.Data.Configuration; */
/* You can write you own way to handle a default database, or piggyback off of EnterpriseLibrary. You don't necessarily have to use EnterpriseLibrary.Data, you are simply piggybacking on their xml/configuration setup */
DatabaseSettings dbSettings = (DatabaseSettings)ConfigurationManager.GetSection("dataConfiguration");
returnItem.DefaultConnectionStringName = dbSettings.DefaultDatabase;
}
return returnItem;
}
}
And the app.config xml:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<configSections>
<section name="dataConfiguration" type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data"/>
</configSections>
<connectionStrings>
<clear/>
<add name="MyFirstConnectionStringName" connectionString="Server=.\MyServerOne;Database=OneDB;Trusted_Connection=True;MultipleActiveResultSets=true"
providerName="System.Data.SqlClient" />
<add name="MySecondConnectionStringName" connectionString="Server=.\MyServerTwo;Database=TwoDB;Trusted_Connection=True;MultipleActiveResultSets=true"
providerName="System.Data.SqlClient" />
</connectionStrings>
<dataConfiguration defaultDatabase="MyFirstConnectionStringName" />
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
</configuration>
Key words:
DotNet DotNet .Net Core Classic Json Configuration ICollection scalar and collection support both dotnet and dotnetcore