Click here to Skip to main content
15,881,172 members
Articles / Programming Languages / C#

Simple Use Case for Leveraging Visual Studio Solution Configurations

Rate me:
Please Sign up or sign in to vote.
4.25/5 (4 votes)
11 May 2016CPOL15 min read 17.6K   6   7
Automatically switch between client solution's Test or Prod app.config file, for injecting environment settings into class library solution

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:

  1. 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.
  2. 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.
  3. 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).
  4. 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.

XML
<?xml version="1.0" encoding="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.

C#
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.

C#
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.

C#
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.

C#
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.

C#
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:

XML
<?xml version="1.0" encoding="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:

XML
<?xml version="1.0" encoding="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:

C#
<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.

C#
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”.

C#
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.

C#
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.

C#
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionBad solution to already solved problem Pin
Sacha Barber20-Apr-16 20:29
Sacha Barber20-Apr-16 20:29 
AnswerRe: Bad solution to already solved problem Pin
PilotProgrammer21-Apr-16 12:31
PilotProgrammer21-Apr-16 12:31 
General[My vote of 1] Sorry, but this seams horrible Pin
Juy Juka3-Apr-16 23:20
Juy Juka3-Apr-16 23:20 
GeneralRe: [My vote of 1] Sorry, but this seams horrible Pin
PilotProgrammer21-Apr-16 12:38
PilotProgrammer21-Apr-16 12:38 
Hi Juy,

Thank you for the feedback! I rewrote the article to utilize .config files, and it is much better to make the class library unaware of the environment as you suggested. Could you let me know your thoughts on the updated article? I tried to take the step further to utilize an interface for the configuration as well. Did I do that right so far as declaring the interface within the class library?

Thanks!
PilotProgrammer
GeneralRe: [My vote of 1] Sorry, but this seams horrible Pin
Juy Juka22-Apr-16 0:44
Juy Juka22-Apr-16 0:44 
PraiseRe: [My vote of 1] Sorry, but this seams horrible Pin
PilotProgrammer14-May-16 6:59
PilotProgrammer14-May-16 6:59 
PraiseRe: [My vote of 1] Sorry, but this seams horrible Pin
Juy Juka18-May-16 22:07
Juy Juka18-May-16 22:07 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.