Introduction
Recently I needed to modify a solution that contained a Windows service. This service utilized a DLL that was created from a class library project in an entirely separate solution. I wanted to be able to automatically provide the environment configuration to the class library based on the solution configuration of the client application (service in my case, but a console app in this article), as the class library needed to communicate with different instances of a database and web service based on the environment. This article will walk through a use case that has the following points:
- We will have a class library that will communicate with a database and web service. To allow the database connection and web service url to be defined by the client, we will create an interface that clients of the class library may optionally implement to provide said database connection and web service url.
- We will use the Project Settings of the class library, which will create an app.config file in the class library. This will be the structure that the client solution will need to mimic in its app.config file as well.
- The client will define two app.config files, one for Release and one for Debug. We will also edit the .csproj file to automatically utilize the correct app.config file based on the solution configuration (whether the client is run in Debug or Release).
- The class library will contain a default implementation of the interface mentioned in point 1. If the client does not create a class that implements the interface, then the default implementation of the interface in the class library will pick up the database/web service settings, so long as the client follows the correct format in its app.config file(s).
The code for this article is available via the attached zip file, or here on GitHub.
A big thank you goes to Juy Juka for his guidance in writing this article.
Background
The class library that was providing the API, communicated with both a web service and a database, in both test and production environments. At first, updating the client solution required changing the database connection string and web services url to test in the class library solution, compiling the class library DLLs, making the code changes to the client solution, and then compiling/running the client solution to utilize the new class library DLL. After the testing was done, I would have to switch the database connection string and web service url back to production, and basically repeat that same compiling process, before committing the changes to the repository. If I forgot to compile the class library solution after changing it to the test environments (database/web service settings), and then ran the client solution, then bad things might happen -- generally not a good situation when you think you’re in test mode, but really aren’t!
By following the steps defined in the Introduction, the workload described in the immediately above paragraph can essentially be eliminated. In the rest of this article, we will build both a class library solution and a client solution. So, here we go!
Using the Code
The Class Library Solution
First, we’ll create the class library. In Visual Studio, go to File > New > Project… in the Visual C# Templates, create a new Class Library project. Name both the project and the solution “SolutionConfigurationsClassLibrary
”.
We'll eventually create the classes as shown in the screenshot of the Solution Explorer below. The FancyCalculator
gets a scalar multiplier from the DatabaseConnection
, multiplies it by a value provided by a client of this class library, and then reports that result back to the client solution, as well as a WebServiceClient
object (which we’ll pretend forwards that onto the actual web service). The IExternalDataAccessSetting
interface describes the expected functionality of the class that will encapsulate the environment configuration for the DatabaseConnection
and WebServiceClient
objects. We also create a DefaultExternalDataAccessSettings
class, so that a client can simply provide an app.config file of the appropriate structure, and not need to also provide an implementation of IExternalDataAccessSetting
.
Before we get started coding, let's add the reference to System.Configuration
that we'll need in a minute while constructing the class library. Right click on the References node underneath the SolutionConfigurationsClassLibrary
project. Select Add Reference. On the left side of the windows that appears, go to Assemblies > Framework, find System.Configuration
in the list, click the checkbox next to it, and click OK.
Next, we need to create the Project settings (which in turn sets up the app.config file) within the class library. The values (NOT_DEFINED
) we enter for these settings won’t actually be used to communicate with anything, but will allow us to create the code for the DefaultExternalDataAccessSettings
class within the class library. Right click on the SolutionConfigurationsClassLibrary
project, and then click Properties. On this screen, click on the Settings tab, and then click the text that says “This project does not contain a default settings file. Click here to create one.” -- which will do just that. In the grid that appears, enter the following property name, types, scope, and values.
According to this MSDN article, the difference between Application and User scoped settings, is that the former is pretty much read-only, while the latter is read/write. (With IntelliSense, you'll notice user-scope settings have "get; set;
", but application-scope settings just have "get;
"). Notice that by selecting "(Connection string)" for PrimaryDatabase
, it only allows for the Application scope. We also want the PrimaryWebService
to be read-only, so set its Scope to Application. More information, including encrypting the connection string in the app.config file, is available here.
Click on the SolutionConfigurationsClassLibrary
's app.config file that was generated. You'll see the code listed below. Notice that both the connectionString
of the PrimaryDatabase
, and the value of the PrimaryWebService
, are NOT_DEFINED
. Copy this code to an empty text editor (or come back to this file), as we'll utilize this XML when creating the client solution.
="1.0"="utf-8"
<configuration>
<configSections>
<sectionGroup name="applicationSettings"
type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="SolutionConfigurationsClassLibrary.Properties.Settings"
type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
</configSections>
<connectionStrings>
<add name="SolutionConfigurationsClassLibrary.Properties.Settings.PrimaryDatabase"
connectionString="NOT_DEFINED" />
</connectionStrings>
<applicationSettings>
<SolutionConfigurationsClassLibrary.Properties.Settings>
<setting name="PrimaryWebService" serializeAs="String">
<value>NOT_DEFINED</value>
</setting>
</SolutionConfigurationsClassLibrary.Properties.Settings>
</applicationSettings>
</configuration>
Now to the code. First up is the IExternalDataAccessSetting
interface. The class implementing this interface must be able to provide us the database name and web service url. These two requirements/properties could arguably be in different interfaces, but we’re keeping them together for the sake of demonstration.
public interface IExternalDataAccessSettings
{
string DatabaseName
{ get; }
string WebServiceUrl
{ get; }
}
Next, we will code the DefaultExternalDataAccessSettings
class. In the Project Settings pane, we had set the connection string and web service url to NOT_DEFINED
. The client may choose to provide its own implementation of IExternalDataAccessSettings
, but if it doesn't, then it still must provide an app.config file with the PrimaryDatabase
and PrimaryWebService
settings. If the client doesn't provide those settings in an app.config file, then this DefaultExternalDataAccessSettings
implementation will throw an exception.
public class DefaultExternalDataAccessSettings : IExternalDataAccessSettings
{
public virtual string DatabaseName
{
get
{
string databaseName = null;
var connectionName =
"SolutionConfigurationsClassLibrary.Properties.Settings.PrimaryDatabase";
if (ConfigurationManager.ConnectionStrings[connectionName] != null)
databaseName =
ConfigurationManager.ConnectionStrings[connectionName].ConnectionString;
else
throw new Exception("Client solution must define connection for " +
connectionName);
return databaseName;
}
}
public virtual string WebServiceUrl
{
get
{
var webserviceUrl = Properties.Settings.Default.PrimaryWebService;
if (webserviceUrl == null || (webserviceUrl == "NOT_DEFINED"))
throw new Exception("Client solution must define web service url");
return webserviceUrl;
}
}
}
This is what the code for our DatabaseConnection
looks like (see below). An implementor of the IExternalDataAccessSettings
interface is provided in the first constructor, and this implementer provides the database name that we need to use to connect to the database. Should the client not provide an implementation of IExternalDataAccessSettings
, then the second (parameterless) constructor will be called, which utilizes the DefaultExternalDataAccessSettings
we created above. In a real application, the GetValueToUseForCalculation
method wouldn’t have the conditional that returns a different value based on the database name, but we are simulating that different values might be returned in test and production environments.
public class DatabaseConnection
{
protected string DatabaseName;
public DatabaseConnection(IExternalDataAccessSettings settings)
{
this.DatabaseName = settings.DatabaseName;
}
public DatabaseConnection()
: this(new DefaultExternalDataAccessSettings())
{ }
public void Connect()
{
Console.WriteLine("Just connected to " + this.DatabaseName + " database!");
}
public int GetValueToUseForCalculation()
{
int value = 0;
if (this.DatabaseName == "TEST")
value = 5;
else if (this.DatabaseName == "PROD")
value = 10;
return value;
}
}
The code below shows our WebServiceClient
. The purpose of this class in our example class library is to report a message (the result of the calculation) to a web service. We don’t actually connect to a web service in this example -- here, we just report that a simulated message was sent, and to what url it was sent. The actual url is provided by the implementor of IExternalDataAccessSettings
, which is passed-in as a constructor parameter. And like the DatabaseConnection
, we also have a parameterless constructor, where the client can opt to not provide its own implentation of IExternalDataAccessSettings
. In that case, the DefaultExternalDataAccessSettings
will again be used.
public class WebServiceClient
{
protected string WebServiceUrl;
public WebServiceClient(IExternalDataAccessSettings settings)
{
this.WebServiceUrl = settings.WebServiceUrl;
}
public WebServiceClient()
: this(new DefaultExternalDataAccessSettings())
{ }
public void SendMessage(string messageToSend)
{
if (string.IsNullOrEmpty(messageToSend) == true)
throw new Exception("Can't send an empty message. Sorry 'bout that!");
Console.WriteLine("Following message sent to " +
this.WebServiceUrl + " web service: " + messageToSend);
}
}
Last, add the following code to implement the FancyCalculator
itself. This class takes two dependencies, the database connection and web service client, as constructor parameters. Then, the DoFancyStuffWithANumber
method is meant to be called by the client with a number. The FancyCalculator
gets a different number from the database connection, makes a trivial calculation for the sake of demonstration, and reports the result of that calculation to the web service.
public class FancyCalculator
{
protected DatabaseConnection DbConn;
protected WebServiceClient WsClient;
public FancyCalculator(DatabaseConnection dbConn, WebServiceClient wsClient)
{
this.DbConn = dbConn;
this.WsClient = wsClient;
}
public int DoFancyStuffWithANumber(int aNumber)
{
var multiplier = this.GetMultiplierValueFromDatabase();
var result = this.MakeCalculation(multiplier);
this.ReportResultsToWebService(result);
return result;
}
protected int GetMultiplierValueFromDatabase()
{
this.DbConn.Connect();
return this.DbConn.GetValueToUseForCalculation();
}
protected int MakeCalculation(int multiplierValue)
{
int result = multiplierValue * 10;
return result;
}
protected void ReportResultsToWebService(int result)
{
this.WsClient.SendMessage("Result of FancyCalculator: " + result.ToString());
}
}
The Client Solution
Now we move on to building the application that will use the class library solution. We want to create an entirely new solution for the client. So in Visual Studio, go to File > New > Project… in the Visual C# Templates, create a new Console Application project. Name both the project and the solution “SolutionConfigurationsClassLibraryClient
”. In our case and for simplicity’s sake, the class library solution (SolutionConfigurationsClassLibrary
) and the client solution (SolutionConfigurationsClassLibraryClient
) will be in the same Projects directory, as shown in the next image.
Next, we want to add a Reference
from the SolutionConfigurationsClassLibraryClient
solution to the SolutionConfigurationsClassLibrary
class library solution. In the Solution Explorer of the SolutionConfigurationsClassLibraryClient
solution, right click on the References node for the SolutionConfigurationsClassLibraryClient
project in the Solution Explorer. In the dialog that appears, we want to click on the Browse tab on the left, and then click the “Browse…” button on that screen. An explorer window will appear, and we want to find the SolutionConfigurationsClassLibrary.dll file, select it, and click Add. This should be located at \Projects\SolutionConfigurationsClassLibrary\SolutionConfigurationsClassLibrary\bin\Release, where “Projects
” would be the location of your Visual Studio Projects directory.
We need a way to get the database name and web service url settings into the SolutionConfigurationsClassLibrary
code in order to use the FancyCalculator
from the SolutionConfigurationsClassLibraryClient
’s code. To do this, create a new directory called “Config” in the Solution Explorer. Then, locate the App.config file, copy it to the new Config folder, and rename that file to App.debug.config. Paste the XML that you copied from the SolutionConfigurationsClassLibrary
's app.config file in the App.debug.config
file. Then update the PrimaryDatabase
's connectionString
and PrimaryWebService
's value to be TEST
. The App.debug.config
file should read as follows:
="1.0"="utf-8"
<configuration>
<configSections>
<sectionGroup name="applicationSettings"
type="System.Configuration.ApplicationSettingsGroup, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="SolutionConfigurationsClassLibrary.Properties.Settings"
type="System.Configuration.ClientSettingsSection, System, Version=4.0.0.0,
Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</sectionGroup>
</configSections>
<connectionStrings>
<add name="SolutionConfigurationsClassLibrary.Properties.Settings.PrimaryDatabase"
connectionString="TEST" providerName="" />
</connectionStrings>
<applicationSettings>
<SolutionConfigurationsClassLibrary.Properties.Settings>
<setting name="PrimaryWebService" serializeAs="String">
<value>TEST</value>
</setting>
</SolutionConfigurationsClassLibrary.Properties.Settings>
</applicationSettings>
</configuration>
Now, copy the App.config file to the Config directory again, but this time, name it App.release.config
instead. Edit that new file and do the same thing (Paste the XML that you copied from the SolutionConfigurationsClassLibrary
's app.config file), except set the PrimaryDatabase
's connectionString
and PrimaryWebService
's value to PROD
this time. Your App.release.config file read as follows:
="1.0"="utf-8"
<configuration>
<configSections>
<sectionGroup name="applicationSettings"
type="System.Configuration.ApplicationSettingsGroup, System,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" >
<section name="SolutionConfigurationsClassLibrary.Properties.Settings"
type="System.Configuration.ClientSettingsSection, System,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
requirePermission="false" />
</sectionGroup>
</configSections>
<connectionStrings>
<add name="SolutionConfigurationsClassLibrary.Properties.Settings.PrimaryDatabase"
connectionString="PROD" providerName="" />
</connectionStrings>
<applicationSettings>
<SolutionConfigurationsClassLibrary.Properties.Settings>
<setting name="PrimaryWebService" serializeAs="String">
<value>PROD</value>
</setting>
</SolutionConfigurationsClassLibrary.Properties.Settings>
</applicationSettings>
</configuration>
You should have two files in your Config folder now: App.debug.config and App.release.config. As you probably figured out, these two files contain the database and web service setting for our test and production environments. The Solution Explorer, after creating the new Config directory and two config files, should appear as follows:
Now we need to set up the client project to automatically pick the right App.config file (one of the ones we just created in the Config directory). In the Solution Explorer, right click on the SolutionConfigurationsClassLibraryClient
console application project, and select “Unload Project”. Right click on the project again and select “Edit SolutionConfigurationsClassLibraryClient.csproj”. This is an XML file that contains some configurations for your console application project. Scroll to the bottom, where you’ll see a commented out section of XML with a “Target
” node with this code “<Target Name="AfterBuild"></Target>
”. Adjust the comments such that this is uncommented, and modify it to contain the additional Delete/Copy sub-nodes that you see below:
<Target Name="AfterBuild">
<Delete Files="$(TargetDir)$(TargetFileName).config" />
<Copy SourceFiles="$(ProjectDir)\Config\App.$(Configuration).config"
DestinationFiles="$(TargetDir)$(TargetFileName).config" />
</Target>
In the build directory, the above XML removes the existing SolutionConfigurationsClassLibraryClient.config file, and copies the App.debug.config or App.release.config (depending on our client solution configuration) file in its place. “$(Configuration)
” is essentially a variable that dynamically contains the value of whatever Configuration
is selected in Visual Studio when you run the Build process for the SolutionConfigurationsClassLibraryClient
solution. When done, save the .csproj file, right click on the project again, and click “Reload Project”. With that step, the proper database name and web service url will be provided to the class library -- the TEST settings will be provided when we are in the Debug configuration, and the PROD setting will be provided when we are in the Release configuration… awesome!
In the reloaded project, edit the Program.cs file in the client console application and enter the code shown below. This instantiates DatabaseConnection
and WebServiceClient
objects via constructor dependency injection using the ExternalDataAccessSettings
, which then does the same thing for the FancyCalculator
using the just-created DatabaseConnection
and WebServiceClient
objects. Then it runs the calculator. You’ll also have to add a “using SolutionConfigurationsClassLibrary;
” statement at the top of this file.
class Program
{
static void Main(string[] args)
{
DatabaseConnection conn = new DatabaseConnection();
WebServiceClient ws = new WebServiceClient();
FancyCalculator calc = new FancyCalculator(conn, ws);
int result = calc.DoFancyStuffWithANumber(5);
Console.ReadLine();
}
}
Now, let’s set the Configuration
of the SolutionConfigurationsClassLibraryClient
console application to Debug
, and run it. Because the Config\App.debug.config file was copied to the build directory (and because the SolutionConfigurationsClassLibrary
's DefaultExternalDataAccessSettings
class is set to app.config settings), the TEST database and web service settings are used.
Finally, we’ll set the Configuration
of the SolutionConfigurationsClassLibraryClient
console application to Release
, and run it again. This time, because the Config\App.release.config file was copied to the build directory (and still because of the SolutionConfigurationsClassLibrary
's DefaultExternalDataAccessSettings
class), the PROD database and web service settings are used.
When you run the SolutionConfigurationsClassLibraryClient
console app in Release, you may get a message that says “You are debugging a Release build of SolutionConfigurationsClassLibraryClient.exe …
”. You can solve this by right clicking on the created SolutionConfigurationsClassLibraryClient
console application project in the Solution Explorer, and going to Properties. Then click on the Build tab on the left side, and uncheck the “Optimize code” checkbox as shown in the Figure below. At this point, you should be able to run the console app in Release, and see the results in the Figure above.
Non-App.config Flexibility
Though the app.config file is the best way to approach application settings, the structure that we've set up in these two solutions allows us the ability to provide the PrimaryDatabase
or PrimaryWebService
settings in any other fashion we would like.
In the client solution, let's create a class that implements the IExternalDataAccessSettings
interface defined in the SolutionConfigurationsClassLibrary
. Create a new class and name it ExternalDataAccessSettings
. The code for ExternalDataAccessSettings
is shown below. In each of the methods required by the IExternalDataAccessSettings
interface, we provide "override
" (not to be confused with methods marked with the override
keyword, which we'll get to in a second) settings that substitute the app.config settings. At the top of this file, you’ll also need to add a using
statement for “SolutionConfigurationsClassLibrary
”.
public class ExternalDataAccessSettings : IExternalDataAccessSettings
{
public string DatabaseName
{
get
{ return "CONNECTION_STRING_OVERRIDE"; }
}
public string WebServiceUrl
{
get
{ return "URL_OVERRIDE"; }
}
}
Change the client solution's Program class to use the following code. This instantiates the ExternalDataAccessSettings
class, and provides it to the constructor's of the DatabaseConnection
and WebServiceClient
class library objects. Because we have the parameterized versions of the constructor for both of these classes, the client's ExternalDataAccessSettings
will be used instead of the class library's DefaultExternalDataAccessSettings
.
class Program
{
static void Main(string[] args)
{
var dataAccessSettings = new ExternalDataAccessSettings();
DatabaseConnection conn = new DatabaseConnection(dataAccessSettings);
WebServiceClient ws = new WebServiceClient(dataAccessSettings);
FancyCalculator calc = new FancyCalculator(conn, ws);
int result = calc.DoFancyStuffWithANumber(5);
Console.ReadLine();
}
}
As expected, the output of this changes uses the overridden settings for the connection string and web service url, regardless of whether we run the client in Debug or Release.
If we only wanted to provide a non-app.config setting for EITHER DatabaseName
OR WebServiceUrl
, then our ExternalDataAccessSettings
can inherit from DefaultExternalDataAccessSettings
, instead of declaring itself as an implementor of IExternalDataAccessSettings
. Then, we would declare the property that we wanted to override, either DatabaseName
or WebServiceUrl
, and add the override
keyword to the associated property getter's signature. This is possible because we marked the property getter's with the virtual
keyword on the DefaultExternalDataAccessSettings
implementation. An example of this is shown below. "CONNECTION_STRING_OVERRIDE
" will be shown for the connection string regardless of the Debug/Release solution configuration, but the web service url will still switch between TEST
and PROD
, since we didn't override it. All in all, though, it would probably be easier to just separate the interface and implentation of the database settings from the web service settings and provide each implementation to their respective DatabaseConnection
and WebServiceClient
classes.
public class ExternalDataAccessSettings : DefaultExternalDataAccessSettings
{
public override string DatabaseName
{
get
{ return "CONNECTION_STRING_OVERRIDE"; }
}
}
Conclusion
We’ve covered a fairly simple use case of setting up a client application to reference a class library that needs to be configurable for running in multiple environments. By using environment-specific client configuration files, and by editing the client .csproj file to dynamically change between which config file's settings are provided to the class library, the client application can operate in both test and production environments by just switching the client’s solution configuration between Debug and Release.
This was a big help for me personally, because I didn’t have to worry about whether I had remembered to rebuild the class library after switching the database and web service connections from test to prod, or vice versa. All I had to do was switch the client solution configuration to what I needed, and it was off to the races!
I'd also like to add, that when I thought of Dependency Injection in the past, I had always thought that it was used in the case of providing a required object instance to dependent object. However, you can also think of configuration as a dependency that can be provided via Dependency Injection. This StackExchange post I though provided some additional insight -- we can just wrap our configuration in a class, and then it's the exact same scenario.
Further Reading / References
The following are resources that I used in writing this article, or that I came across and thought would be useful:
History
- 1st April, 2016: Initial version