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"
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:
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:
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 string
s 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:
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:
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;
}
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;
}
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;
}
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);
}
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);
}
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:
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 string
s? 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