Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Instance Storage - A Simple Way to Share Configuration Data among Applications

0.00/5 (No votes)
5 Oct 2017 1  
We all know database connection strings and suchlike should not be hard coded - but then there is the problem of getting the right string into the right PC when you release software.

Introduction

We all know that hardcoding configuration data is a bad idea : heck we tell newbies that often enough, and we still see code like this:

using(SqlConnection conn = new SqlConnection("Server=[server_name];
      Database=[database_name]; password=[password_name];user = [user_name]")) 
    {
    con.Open();
    ...
    }

And while it's easy to create a config file entry to hold the data, it still gives us a problem: multiple apps need multiple configuration strings. I really noticed this when I started adding apps to the WookieTab, and again when I changed the servername of my PC: suddenly loads of apps failed and I needed to edit all the connections strings.

That's nasty, and it's prone to error - so let's see if we can do something about it.

Background

Think about it, and most of your apps on the same PC will access the same database system, using the same user name and password combo - it's just the DB name that changes. So why not have a system where a specific class is responsible for storing, updating, and retrieving "common" config info?

So I wrote one.

Before I get to the details, let's have a quick look at how you use it in your code - it's pretty simple.

Add a reference to the assembly as usual, then add a using line:

using SMDBSupport;

Then just tell it which database you want to access:

strConnection = SMInstanceStorage.GetInstanceConnectionString("SMStandardReplies");

And you know that if it works for application A, it'll work without any effort or changes for application B when you release it.

So, How Does It Work?

It's not complicated! In fact, I think it took me longer to come up with the idea that I needed this, than it took me to write the code - I can be a slow learner sometimes...

The string stored in the "common config" is a format suitable for String.Format - and that's what the GetInstanceConnectionString method uses:

"Data Source=GRIFF-DESKTOP\SQLEXPRESS;Initial Catalog={0};Integrated Security=True"
/// <summary>
/// Fetch the correct connection string for this instance and database
/// </summary>
/// <param name="databaseName"></param>
/// <returns></returns>
public static string GetInstanceConnectionString(string databaseName)
    {
    return string.Format(strConnectFormat, databaseName);
    }

The format string is stored in the "Local Application Data" folder, under a GUID folder name that is assigned to the project that controls the assembly build process for the SMInstanceStorage class - this means that the location of the file is variable from system to system, but the folder is available to every app in the user login space, and that different users on the same machine can have different database logins. It's all fetched by a static constructor when the class is first used:

/// <summary>
/// Default constructor
/// </summary>
static SMInstanceStorage()
    {
    assemblyGuid = AssemblyGuid.ToString("B").ToUpper();
    strConnectFormat = "None assigned";
    string folderBase = Path.Combine(Environment.GetFolderPath
             (Environment.SpecialFolder.LocalApplicationData), assemblyGuid);
    strDataPath = string.Format(@"{0}\InstanceStorage.dat", folderBase);
    if (!Directory.Exists(folderBase))
        {
        Directory.CreateDirectory(folderBase);
        }
    if (!File.Exists(strDataPath))
        {
        File.WriteAllText(strDataPath, "");
        }
    string[] lines = File.ReadAllLines(strDataPath);
    foreach (string line in lines)
        {
        string[] parts = GetParts(line);
        if (parts.Length == 2)
            {
            switch (parts[0])
                {
                case scConnect: strConnectFormat = parts[1]; break;
                }
            }
        }
    }

The folder comes out like this for my system:

C:\Users\PaulG\AppData\Local\{97BCDB46-E0BA-4DEB-B23A-07227BAB5354}

If it doesn't exist, it's created, and the same applies for the file.

Then it's processed, using a simple helper method on each line:

/// <summary>
/// Like string.Split, but only on the first instance.
/// </summary>
/// <param name="line"></param>
/// <returns></returns>
private static string[] GetParts(string line)
    {
    int index = line.IndexOf('=');
    if (index < 0) return new string[0];
    return new string[] { line.Substring(0, index), line.Substring(index + 1) };
    }

You can't use string.Split to do this directly, as it splits on all the "=" characters, and database connections strings use equals as well. "Bolting the bits back together" just wouldn't be efficient.

At the moment, the static constructor only knows about "Connection" objects:

/// <summary>
/// Connection storage class
/// </summary>
private const string scConnect = "Connection";

But the class includes other code to support common "user" storage for items which are common to several apps, but not all:

// <summary>
/// Read a storage class from the instance store
/// </summary>
/// <param name="storageClass"></param>
/// <returns>null if no such class found</returns>
public static string ReadInstanceData(string storageClass)
    {
    string result = null;
    string[] lines = File.ReadAllLines(strDataPath);
    foreach (string line in lines)
        {
        string[] parts = GetParts(line);
        if (parts.Length == 2 && parts[0] == storageClass)
            {
            result = parts[1];
            break;
            }
        }
    return result;
    }
/// <summary>
/// Read storage classes from the instance store
/// </summary>
/// <returns>null if no such class found</returns>
public static Dictionary<string, string> ReadAllInstanceData()
    {
    Dictionary<string, string> results = new Dictionary<string, string>();
    string[] lines = File.ReadAllLines(strDataPath);
    foreach (string line in lines)
        {
        string[] parts = GetParts(line);
        if (parts.Length == 2) results.Add(parts[0], parts[1]);
        }
    return results;
    }
/// <summary>
/// Read storage classes from the instance store
/// </summary>
/// <param name="storageClasses"></param>
/// <returns></returns>
public static Dictionary<string, string> ReadInstanceData(params string[] storageClasses)
    {
    Dictionary<string, string> results = new Dictionary<string, string>();
    string[] lines = File.ReadAllLines(strDataPath);
    if (storageClasses == null || storageClasses.Length == 0)
        {
        foreach (string line in lines)
            {
            string[] parts = GetParts(line);
            if (parts.Length == 2) results.Add(parts[0], parts[1]);
            }
        }
    else
        {
        foreach (string line in lines)
            {
            string[] parts = GetParts(line);
            if (parts.Length == 2)
                {
                int index = Array.IndexOf(storageClasses, (parts[0]));
                if (index >= 0) results.Add(parts[0], parts[1]);
                }
            }
        }
    return results;
    }
/// <summary>
/// Save a new instance storage value
/// </summary>
/// <param name="storeageClass"></param>
/// <param name="value"></param>
public static void SaveInstanceData(string storeageClass, string value)
    {
    string[] lines = File.ReadAllLines(strDataPath);
    for (int i = 0; i < lines.Length; i++)
        {
        string line = lines[i];
        string[] parts = GetParts(line);
        if (parts.Length == 2 && parts[0] == storeageClass)
            {
            lines[i] = string.Join("=", parts[0], value);
            break;
            }
        }
    File.WriteAllLines(strDataPath, lines);
    }
/// <summary>
/// Save all storage classes to the instance store.
/// This overwrites all existing data.
/// </summary>
/// <param name="storageData"></param>
/// <returns>null if no such class found</returns>
public static void SaveAllInstanceData(Dictionary<string, string> storageData)
    {
    List<string> lines = new List<string>();
    foreach (string key in storageData.Keys)
        {
        lines.Add(string.Join("=", key, storageData[key]));
        }
    File.WriteAllLines(strDataPath, lines);
    }
/// <summary>
/// Updates storage classes to the instance store
/// </summary>
/// <param name="storageData"></param>
/// <returns>null if no such class found</returns>
public static void UpdateInstanceData(Dictionary<string, string> storageData)
    {
    string[] lines = File.ReadAllLines(strDataPath);
    int changes = 0;
    for (int i = 0; i < lines.Length; i++)
        {
        string line = lines[i];
        string[] parts = GetParts(line);
        if (parts.Length == 2)
            {
            if (storageData.ContainsKey(parts[0]))
                {
                string newLine = string.Join("=", parts[0], storageData[parts[0]]);
                if (line != newLine)
                    {
                    line = newLine;
                    changes++;
                    }
                }
            }
        }
    if (changes > 0) File.WriteAllLines(strDataPath, lines);
    }

All that's left is a simple method to update the connection string:

/// <summary>
/// Save the correct connection string for this instance and database
/// </summary>
/// <param name="connectionString"></param>
/// <param name="databaseName"></param>
/// <returns></returns>
public static void SetInstanceConnectionString(string connectionString, string databaseName)
    {
    strConnectFormat = connectionString.Replace(databaseName, "{0}");
    SaveInstanceData(scConnect, strConnectFormat);
    }

This app doesn't encrypt the data, so username and password information is exposed - but it wouldn't be difficult to add encryption, if that's a concern in your environment. The hassle is finding a reliable way to keep the encryption key secure when multiple applications need to supply it ... which is why it doesn't support encryption at the moment.

What else do you need to get using this? You want a support program to help you edit the strings? I'm too good to you, I really am - there's one in the downloads: StorageClassMaintenance is a simple WinForms app to handle the file.

History

  • 2017-09-01: First version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here