Click here to Skip to main content
15,882,152 members
Articles / Programming Languages / C#

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

Rate me:
Please Sign up or sign in to vote.
4.83/5 (7 votes)
4 Oct 2017CPOL3 min read 12.4K   114   7   6
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:

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

C#
using SMDBSupport;

Then just tell it which database you want to access:

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

C#
"Data Source=GRIFF-DESKTOP\SQLEXPRESS;Initial Catalog={0};Integrated Security=True"
C#
/// <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:

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

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

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

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

C#
/// <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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO
Wales Wales
Born at an early age, he grew older. At the same time, his hair grew longer, and was tied up behind his head.
Has problems spelling the word "the".
Invented the portable cat-flap.
Currently, has not died yet. Or has he?

Comments and Discussions

 
GeneralMy vote of 5 Pin
Prilvesh K21-Apr-23 2:00
professionalPrilvesh K21-Apr-23 2:00 
QuestionHard-coded path - And...What is GenerateTimeStampFile.exe Pin
tbim23-Jun-22 4:29
tbim23-Jun-22 4:29 
GeneralMy vote of 5 Pin
Maciej Los26-Feb-19 22:45
mveMaciej Los26-Feb-19 22:45 
QuestionEither I misunderstood the purpose, or you're trying to reinvent the wheel Pin
Jörgen Andersson5-Oct-17 0:39
professionalJörgen Andersson5-Oct-17 0:39 
AnswerRe: Either I misunderstood the purpose, or you're trying to reinvent the wheel Pin
OriginalGriff5-Oct-17 1:40
mveOriginalGriff5-Oct-17 1:40 
GeneralRe: Either I misunderstood the purpose, or you're trying to reinvent the wheel Pin
Jörgen Andersson5-Oct-17 1:44
professionalJörgen Andersson5-Oct-17 1:44 

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.