Multi-Environment Developing

By Ansel Pineiro on November 19, 2015

Multi-Environment Developing

Often there is a need to develop for multiple environments. A common example would be a test environment and a production environment. Sometimes there can be small differences in the environment that result in the need for different code. It can get pretty messy to have to change that code when you are done testing and ready to deploy to production. In this post, I will demonstrate how to develop for two very different environments, while keeping 99% of the code the same.

Starting from Scratch

Let’s say you want to make a web application that makes an API call to an external service, and processes that data. It can be easier to start developing it as a Console Application before integrating it with a CMS like Kentico. Testing and debugging new features can also be more efficient from a Console App as it does not have the overhead the CMS has. Not to mention we want to keep the test environment separate from the production environment.

In this example, we have an app that pulls Time Entry data from Toggl, syncs it to different databases, and logs it in different ways depending on the environment. Note that it is best to start off planning for multiple environments, instead of developing it first and recoding later to work across environments. It may not always be possible to know all the differences in the environments from the start, but it’s best to know as many of them as possible and plan ahead for them at the start as much as you can.

Organize your Code

Now I mentioned that this app will be logging events in different ways, and storing data in different databases depending on the environment. To better organize code, I recommend creating folders for the things that will work differently on different environments.





 

Create an Interface

Right click on the appropriate folder and choose Add -> New Item… and choose Interface. It is best practice to begin the filenames of interfaces with an “I” to indicate that they are an interface and not a class.



 

Within the interface, define the methods that will be used. In this log manager we have three methods. One each for logging an event, logging an error, and logging a warning.  All methods take string parameters for “message”, “code” and “source.” Error requires an Exception parameter. For Warning an Exception parameter is optional.

public interface ILogManager
{
    void LogEvent(string message, string code, string source);
    void LogError(string message, string code, string source, Exception ex);
    void LogWarning(string message, string code, string source, Exception ex = null);
}

Create the Console Implementation of that Interface

Make the class that will be used by the Console.  The class needs to implement the interface. and implement those methods from the interface.

public class LocalLogManager : ILogManager
{
    public void LogEvent(string message, string code, string source)
    {
        MakeEventLog("Event", message, code, source);
    }
    public void LogError(string message, string code, string source, Exception ex)
    {
        MakeEventLog("Error", message, code, source, ex);
    }
    public void LogWarning(string message, string code, string source, Exception ex = null)
    {
        MakeEventLog("Warning", message, code, source, ex);
    }
}

Notice how inside those methods it calls another method called “MakeEventLog.”  This was to not have to repeat the code and break things up into separate methods. The real unique code for this environment's class is in the MakeEventLog method and the DisplayOnConsole class.

This method logs in two ways. First it writes the log into a text file. Then it displays it in the console via the DisplayOnConsole Method.

//Local MakeEventLog

private static void MakeEventLog(string eventType, string message, string code, string source, Exception ex = null)
{
    CreateIfNotExists(); //Create the file if it does not already exist

    using (StreamWriter sw = File.AppendText(Path))
    {
        sw.WriteLine(DateTime.Now.ToString());
        sw.WriteLine(eventType);
        sw.WriteLine(code);
        sw.WriteLine(source);
        sw.WriteLine(message);
        sw.WriteLine(Environment.NewLine);

        if (ex != null)
            sw.WriteLine(ex);
    }

    DisplayOnConsole(eventType, message, code, source, ex);
}

Create the CMS Implementation of the Interface

It is time to make the class that the CMS will use.  The CMS in this example is Kentico.  Again, make a new class, and have it implement the interface, and add the methods of the interface.

public class LocalLogManager : ILogManager
{
    public void LogEvent(string message, string code, string source)
    {
        MakeEventLog("I", message, code, source);
    }
    public void LogError(string message, string code, string source, Exception ex)
    {
        MakeEventLog("E", message + Environment.NewLine + ex, code, source);  
    }
    public void LogWarning(string message, string code, string source, Exception ex = null)
    {
        if (ex != null)
            message += Environment.NewLine + ex;
        
        MakeEventLog("W", message, code, source);
    }
}

These methods look mostly the same as that in the local, with just a few string formatting differences to display a little better on the Kentico logging system.  Most of the differences can be found in this version’s MakeEventLog.

//Base Event Log Info creator
private static void MakeEventLog(string eventType, string description, string code, string source, Exception ex = null)
{
    //Create the EventLogInfo object
    EventLogInfo newEvent = new EventLogInfo();

    newEvent.EventType = eventType;
    newEvent.EventDescription = description;
    newEvent.EventCode = code;
    newEvent.Source = source;
    newEvent.SiteID = CMSContext.CurrentSiteID;

    //Create the EventLogProvider object
    EventLogProvider eventLog = new EventLogProvider();
    eventLog.LogEvent(newEvent); //Log the event

    if (ex != null)
    {
        eventLog.LogEvent(source, code, ex);
    }                
}

The code above instead of logging to a text file and displaying it on the console, it calls the Kentico API to use Kentico’s built in logging system.

Database Interface

Just like how you created an interface for the Logging, create one too for the database under the Sql Folder.

public interface IDBUtils
{
    void CloseConnection(); // Closes database connection or connections.  Run when done with the class.
    void SetLastSyncDateTime(DateTime dateTime);

    string GetAuthString();
    string GetWorkspaceID();

    TimeEntry[] GetAllTimeEntries();
    TogglProject[] GetAllTogglProjects();
    string[] GetProjectClients();
    string[] GetAPIKeys();
    DateTime GetLastSyncDateTime();

    void AddAndRestoreTimeEntries(TimeEntry[] newEntries, TimeEntry[] entriesToRestore);
    void AddTogglProjects(TogglProject[] newProjects);
    void ExecuteNonQuery(string commandString, int timeOut = 0);       
}

This interface contains quite a bit more methods than the logging one did.  Instead of going into detail on what each of those methods do (as that can be a blog post unto itself), I will cover some differences between the environments.

In the Console Application, the connection string is hardcoded to keep it simple.

private void OpenConnection()
{
    togglConnection = CreateAndOpenConnection("server=localhost\\SQLExpress;database=toggl;Integrated Security=true;");
}

While in the CMS version the connection string is pulled from the Web.config file to follow best practices.

private void OpenConnection()
{
    string TogglConnectionString = null;

    try
    {
        Configuration rootWebConfig = WebConfigurationManager.OpenWebConfiguration("/intranet.domainname.com");

        TogglConnectionString = rootWebConfig.ConnectionStrings.ConnectionStrings["TogglConnectionString"].ConnectionString;
    }
    catch (Exception e)
    {
        string message = "Error while getting connection strings";
        _logManager.LogError(message, LoggingTools.TogglDbUtils, "OpenConnection(...)", e);
        throw e;
    }

    togglConnection = CreateAndOpenConnection(TogglConnectionString);
}

More to do on CMS?

Now it’s possible that you may want the CMS to do something else that you don’t want or need done on the console.  It’s easy to do, you just code those in the right class.  If the code is in different methods, that method will either need to be called by the constructor, or by a method defined in the interface.  If defined in the interface, you will need to be sure the method in that interface is called by an outer part of your code.

Share the code!

Now it’s possible that of the code on one environment’s class can be reused on the other’s. Especially if the case of database classes where the different databases are structured the same way.  

Instead of having the same code be in both classes, it is better to make an abstract class that the other classes inherit from.

//Gets all Time Entries from the database since the start DateTime
public virtual TimeEntry[] GetAllTimeEntries()
{
    string where = " WHERE xStart > '" + start + "'";

    try
    {
        List<TimeEntry> entries = new List<TimeEntry>();

        SqlCommand command = new SqlCommand(PullTimeEntrySelect+ where, togglConnection);
        SqlDataReader reader = command.ExecuteReader();

        while (reader.Read())
        {
            //Add Time Entry to the list
            entries.Add(PullTimeEntryFromDb(reader));
        }

        reader.Close();
        return entries.ToArray<TimeEntry>();
    }
    catch (SqlException e)
    {
        string message = "SqlException while pulling Time Entry Array from Database.";
        _logManager.LogError(message, LoggingTools.TogglDbUtils, "getTimeEntry(...)", e);
        throw e;
    }
    catch (Exception e2)
    {
        string message = "Error while pulling Time Entry Array from Database.";
        _logManager.LogError(message, LoggingTools.TogglDbUtils, "getTimeEntry(...)", e2);
        throw e2;
    }
}

The above code for instance can be used by both environments.  It’s better to keep this and other reusable things in an abstract class, and called from the different environment’s classes like the code below.

public override TimeEntry[] GetAllTimeEntries()
{
    return base.GetAllTimeEntries();
}

Dependency Injection

“Now how do we switch between these classes?” you might ask.  The answer lies in Dependency Injection.  I recommend Ninject as that is lightweight, free, and open source. It's easy to install with Nuget.  Just right click the Class Library Project, and choose “Manage Nuget Packages”.  Then search Ninject in the menu.  Click Ninject and choose Install.

Now that Ninject is installed, you will need to create a class that will Inject the right classes depending on the environment.  This class will be the only thing that should ever need code changed between environments.

Create a new class.  Give it any name you want.  I use TogglEnvironment for this.  It needs to inherit from Ninejct.Modules.NinjectModule

public class TogglEnvironment : Ninject.Modules.NinjectModule
{

}

Overload the Load() method

In the overload method, the classes need to be binded to for the different environments like in the code below.

//#define Intranet
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace TogglV2
{
    public class TogglEnvironment : Ninject.Modules.NinjectModule
    {
        public override void Load()
        {
#if Intranet
            Bind<ILogManager>().To<LogManager>();
            Bind<IDBUtils>().To<DBUtils>();
#else
            Bind<ILogManager>().To<LocalLogManager>();
            Bind<IDBUtils>().To<LocalDBUtils>();
#endif
        }
    }
}

In that code, I have C# Preprocessor directives for the different environments.  Switching between the two environments is as simple as commenting or uncommenting the #define statement at the top.

No other change of code in the program should be necessary when using this on a different environment. You don’t even need to delete the classes used by the other environment, unless those classes reference a dll not available in that environment.  I recommend adding the CMS dlls needed in the console application to make the test build process happy. A Console Application likely will not have an DLLs that will need moved to the CMS bin directory.

Inject the Dependencies

The class above explained how to bind the dependencies for the different environments.  Now it is time to actually inject the dependencies. In the class where you inject the dependencies, you will need to add references to “Ninject” and “Ninject.Parameters”

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Ninject;
using Ninject.Parameters;

In my app, I have the dependencies stored as readonly instance variables.

private readonly ILogManager _logManager;
private readonly IDBUtils _dbUtils;

I recommend making them readonly as that will make sure that nothing in them is changed once defined, and makes passing them around to other classes and methods more stable.

The dependencies need injected now.  The code below is contained in the constructor of my class, but it can be put in whatever method you feel it’s needed in. When creating the StandardKernel, the parameter is the class that controls the binding. In this example the class is named TogglEnvironment.

//Inject the dependencies
IKernel _kernel = new StandardKernel(new TogglEnvironment());           
_logManager = _kernel.Get<ILogManager>();
_DBUtils = _kernel.Get<IDBUtils>( new  ConstructorArgument("logManager", _logManager), new ConstructorArgument("start", since));

After the kernel is created, you can inject the dependencies.  The kernel will get the correct implementation of the interfaces, depending on what’s in TogglEnvironment. The ILogManager one is more simple as it does not need a parameter. The IDBUtils class is a little more complicated, as the constructor needs a parameter for the ILogManager.  All that needs to be done is adding the ConstructorArgument, and the name of the ILogManager variable in the constructor, and the ILogManager itself. In this case, the order in which the dependencies are injected matter, as you cannot create an IDBUtils implementation without there already being a ILogManager implemented.

Final Steps

Once the application is complete and ready to be put on the other environment, there are two ways this can be done. The simplest just involves building the other environment’s version, and adding the output DLL to that environment, as well as the Ninject dll and any other dlls that the app is dependant on. In cases where the other environment is dependant on a different version of the dll that your app uses, this will not work.  In that case, you will need to copy all the project files and directories (aside from some listed below) to that other environment. Directories to not copy include bin, obj.  Files to not copy is packages.config, and anything with an .suo and .csproj extension.

Final Words

Keep in mind that the code in the classes that implement the interfaces will probably only be a very small portion of the overall code of the app.  This blog only included code on how to build an app in a way that it can be used on multiple environments. The actual app has a lot more code and classes than described here.  Only the two interfaces and classes that implement those interfaces were included here.  What does most of the actual work is code that is shared and used on all environments. It’s that shared code that the majority of the development and testing time will be focused on.  While developing it is ok to start with just the Console version of the classes, and add the other environment’s classes later. Early on it is best to think of what things will be different on the different environments, and come up with the interfaces early, so as to keep recoding to a minimum.

To make it not require even changing a single line, you can do something like mapping the path and using if else statements to decide which classes to bind.  As of the time of the writing, I have not tried this, and that could have problems if the paths are the same on different machines. If done right, switching which environment the project is for is as simple as commenting or uncommenting a single line of code on a single file.

 

 

Share This Post:

Twitter Pinterest Facebook Google+
Click here to read more Tutorials posts
Start a Project with Us

About the author

Ansel is the Hotfixer. Whenever a site needs to be hotfixed or upgraded, he gets it done lightning fast. He always seeks to find the right balance between performance and readability in all the code he writes. Between code writing and hotfixes, he makes the office a more enjoyable place to work by finding new music and desktop wallpapers for everyone to enjoy. Some think he’s crazy, but Bizstream knows him as The Hotfixer!

View other posts by Ansel

Subscribe to Email

Enter your email address to subscribe to the BizStream Newsletter and receive updates by email.