I'm struggling to understand how to implement principles of encapsulation, inheritance and polymorphism to break this code into logical parts. My background is in VBS/Classic ASP and database development, not OOP.
I'm not asking anyone to write the code, but rather to point me in the right direction. I have this class, which I suppose is something I've heard referred to as a God Object.
My code works, but it's unmanageable and will only get worse.
This class:
- Receives information about a data source, like the path to an Access database or the name of a db server.
- Determines which engine can process the file... Access, MSSQL, MySQL...
- Select a Provider to connect to the DBMS
- Set db specific settings, like whether to use single or double quotes around object names, etc.
- Generate a connection string to connect to the db.
Is it Ok to have all this stuff in a single class?
I was thinking it would be easier to understand later, if I separated it into a few different classes:
- A class to figure out what kind of data source it will connect to
- A class to set db specific settings and generate a connection string
Does that sound right? If so, would you recommend maybe pulling all of these enums and their accompanying methods into a separate static class so that they don't need to be instantiated?
using System;
using System.Diagnostics;
using System.IO;
namespace RickRoll
{
public class Db
{
public string DataSource { get; }
public string Database { get; }
public string Username { get; }
public string Password { get; }
public string ConnectionString { get; private set; }
public string ExtendedProperties { get; }
private WrapColumn wrapcol;
private WrapObject wrapobj;
public DataProvider Provider { get; set; }
public DataFileType FileType { get; private set; }
public DataEngine Engine { get; private set; }
public WrapColumn WrapCol { get => wrapcol; private set => wrapcol = value; }
public WrapObject WrapObj { get => wrapobj; private set => wrapobj = value; }
// This can't be right, but I needed to do it to get the enun values (temporary fix)
public Db()
{
// only used for public enum access
}
// For file databases without password set
public Db(string filepath)
{
FileType = GetDbFileType (filepath);
Engine = GetDbEngine (FileType);
Provider = GetDbProvider (FileType);
DataSource = filepath;
Database = Path.GetFileName (filepath);
Username = null;
Password = null;
ConnectionString = GetConnectionString (Provider);
GetWrapCol (Engine);
GetWrapObj (Engine);
}
// For file databases with password set
public Db(string filepath, string password)
{
FileType = GetDbFileType (filepath);
Engine = GetDbEngine (FileType);
Provider = GetDbProvider (FileType);
DataSource = filepath;
Database = Path.GetFileName (filepath);
Username = null;
Password = password;
ConnectionString = GetConnectionString (Provider);
GetWrapCol (Engine);
GetWrapObj (Engine);
}
// For all connections that use Windows AUthentication or no password
public Db(string path, string server , string database)
{
FileType = GetDbFileType (path);
Engine = GetDbEngine (FileType);
Provider = GetDbProvider (FileType);
DataSource = GetDataSource (Engine , path , server);
Database = database;
Username = null;
Password = null;
ConnectionString = GetConnectionString (Provider);
GetWrapCol (Engine);
GetWrapObj (Engine);
Debug.WriteLine (ConnectionString);
Debug.WriteLine (Environment.UserName);
}
// For all connection types
public Db(string path, string server, string database , string username , string password)
{
FileType = GetDbFileType (path);
Engine = GetDbEngine (FileType);
Provider = GetDbProvider (FileType);
DataSource = GetDataSource (Engine, path, server);
Database = database;
Username = username;
Password = password;
ConnectionString = GetConnectionString (Provider);
GetWrapCol (Engine);
GetWrapObj (Engine);
}
// For instantiating from data stored in SQL Server using Windows Authentication or no password
public Db(int filetype_value, string server , string database)
{
FileType = GetDbFileType (filetype_value);
Engine = GetDbEngine (FileType);
Provider = GetDbProvider (FileType);
DataSource = server;
Database = database;
Username = null;
Password = null;
ConnectionString = GetConnectionString (Provider);
GetWrapCol (Engine);
GetWrapObj (Engine);
Debug.WriteLine (ConnectionString);
Debug.WriteLine (Environment.UserName);
}
// For instantiating from data stored in SQL Server using SQL Authentication or JET DB password
public Db(int filetype_value , string server , string database , string username , string password)
{
FileType = GetDbFileType (filetype_value);
Engine = GetDbEngine (FileType);
Provider = GetDbProvider (FileType);
DataSource = server;
Database = database;
Username = username;
Password = password;
ConnectionString = GetConnectionString (Provider);
GetWrapCol (Engine);
GetWrapObj (Engine);
}
/// <summary>
/// This changes the database connection string to use a different Provider.
/// </summary>
/// <param name="p"></param>
public void ReSetConnectionString(DataProvider p)
{
Provider = p;
ConnectionString = GetConnectionString ( Provider );
}
// Gets file extension from file name or an ext can be passed in place of path
private DataFileType GetDbFileType(string path)
{
DataFileType ft;
string ext;
if (path.Contains ("."))
{
// allows for attached databases
string fpath = path.Replace ("|DataDirectory|" , AppDomain.CurrentDomain.BaseDirectory);
ext = Path.GetExtension (fpath).Replace ("." , "").ToUpper ( );
if (Enum.TryParse<DataFileType> (ext , out ft) == true) return ft;
}
else
{
ft = (DataFileType) Enum.Parse (typeof (DataFileType) , path.ToUpper()); /// throw exception if extention is not in DataFileType enum or is null
}
return ft;
}
// Returns the path to a file for MS Access, etc or a db server name
private string GetDataSource(DataEngine e, string path, string server)
{
if (DataSourceIsFile (e)) return path;
return server;
}
// I think this is unnecessary. Use this instead: DataFileType dft = (DataFileType) value;
private DataFileType GetDbFileType(int value)
{
DataFileType ft = (DataFileType) Enum.Parse (typeof (DataFileType) , value.ToString ( )); // throws and exception if int isn't in the DataFileType enum.
return ft;
}
// This is the db engine
public enum DataEngine
{
None,
ACCESS = 1, // 0 = not applicable or not determined
MSSQL,
EXCEL,
ORACLE,
MYSQL,
TEXT
}
// <summary>
/// These are the int enum values that get stored in the database for each type of data file
/// </summary>
public enum DataFileType
{
None,
MDB = 1, // 0 = not a configured data file
ACCDB,
MDF, // Primary Data FIle
NDF, // File Group (secondary data files)
XLS, // Excel 97-2003 worksheet
XLSX, // Excel 2007 workbook
XLSXM, // Macro enabled workbook
XLTM, // Binary worksheet (BIFF12)
XLW, // Excel works space, previously known as workbook
CSV, // Comma separated values
TAB, // Tab separated values
TSV, // Tab separated values
TXT // Delimited Text file
}
/// <summary>
/// These are the int values that get stored in the database for each db connection
/// </summary>
public enum DataProvider
{
None,
Microsoft_ACE_OLEDB_12_0 = 1, // Microsoft.ACE.OLEDB.12.0 - MS OLEDB DataProvider for MDB or ACCDB or EXCEL
Microsoft_ACE_OLEDB, // Microsoft.ACE.OLEDB VersionIndependentProgID
Microsoft_Jet_OLEDB_4_0, // MS Access - Does not work with ACCDB or any SQL Server version
Microsoft_Jet_OLEDB, // Version Independent ProgID
SQLNCLI11, // SQL Server Native Client for OleDb
SQLNCLI, // Version Independent ProgID
SQLOLEDB_1, // SQL Server OleDb - Does not work with SQL Server Express
SQLOLEDB, // VersionIndependentProgID
SQL__Server__Native__Client__11_0, // SQL Server Native Client using ODbC
SQL__Server__Native__Client, // Version Independent ProgID
MSDASQL_1, // Microsoft OleDb Data Access Components using ODbC
MSDASQL // Version Independent ProgID
}
// This can be simplified
private DataEngine GetDbEngine(int enumvalue)
{
DataEngine result = (DataEngine) Enum.Parse (typeof (DataEngine) , enumvalue.ToString ( )); // throws and exception if int isn't in the DataEngine enum.
return result;
}
// Gets the Database Engine from type of file
private DataEngine GetDbEngine(DataFileType ft)
{
switch (ft)
{
case DataFileType.MDB:
return DataEngine.ACCESS;
case DataFileType.ACCDB:
return DataEngine.ACCESS;
case DataFileType.MDF:
return DataEngine.MSSQL;
case DataFileType.NDF:
return DataEngine.MSSQL;
case DataFileType.XLS:
return DataEngine.EXCEL;
case DataFileType.XLSX:
return DataEngine.EXCEL;
case DataFileType.CSV:
return DataEngine.TEXT;
case DataFileType.TAB:
return DataEngine.TEXT;
case DataFileType.TSV:
return DataEngine.TEXT;
case DataFileType.TXT:
return DataEngine.TEXT;
default:
throw new ArgumentException ($"* * * DataFileType is not a supported data file format. Database DataEngine could not be determined.");
}
}
// Returns the database provider to use for each supported file type
private DataProvider GetDbProvider(DataFileType ft)
{
switch (ft)
{
case DataFileType.MDB:
case DataFileType.ACCDB:
return DataProvider.Microsoft_ACE_OLEDB_12_0;
case DataFileType.MDF:
return DataProvider.SQLNCLI11; // SQLOLEDB_1 and SQLOLEDB did not work with SQL Server Express
case DataFileType.NDF:
return DataProvider.SQLNCLI11;
case DataFileType.XLS:
case DataFileType.XLSX:
case DataFileType.CSV:
case DataFileType.TAB:
case DataFileType.TSV:
case DataFileType.TXT:
return DataProvider.Microsoft_ACE_OLEDB_12_0;
default:
throw new ArgumentException ($"* * * DataFileType is not a supported data file format. Database DataProvider could not be determined.");
}
}
// Sets the character used to 'wrap' column names in query strings in case they have spaces or are reserved words
private void GetWrapCol(DataEngine e)
{
switch (e)
{
case DataEngine.ACCESS:
WrapCol = new WrapColumn { left = "`" , middle = "`,`" , right = "`" };
break;
case DataEngine.MSSQL:
WrapCol = new WrapColumn { left = "[" , middle = "],[" , right = "]" };
break;
case DataEngine.EXCEL:
WrapCol = new WrapColumn { left = "`" , middle = "`,`" , right = "`" };
break;
case DataEngine.ORACLE:
WrapCol = new WrapColumn { left = @"""" , middle = @""",""" , right = @"""" };
break;
case DataEngine.MYSQL:
WrapCol = new WrapColumn { left = "`" , middle = "`,`" , right = "`" }; // might be brackets for sysnames and ` for columns
break;
case DataEngine.TEXT:
WrapCol = new WrapColumn { left = @"""" , middle = @""",""" , right = @"""" }; // not sure how to handle this yet
break;
default:
throw new ArgumentException ($"* * * DataEngine was not valid. Could not determine a COLUMN escape character.");
}
}
// Sets the character used to 'wrap' object names (table names, schema names, etc.) in query strings in case they have spaces or are reserved words
private void GetWrapObj(DataEngine e)
{
switch (e)
{
case DataEngine.ACCESS:
WrapObj = new WrapObject { left = "`" , middle = "`,`" , right = "`" };
break;
case DataEngine.MSSQL:
WrapObj = new WrapObject { left = "[" , middle = "],[" , right = "]" };
break;
case DataEngine.EXCEL:
WrapObj = new WrapObject { left = "`" , middle = "`,`" , right = "`" };
break;
case DataEngine.ORACLE:
WrapObj = new WrapObject { left = @"""" , middle = @""",""" , right = @"""" };
break;
case DataEngine.MYSQL:
WrapObj = new WrapObject { left = "[" , middle = "],[" , right = "]" }; // might be brackets for sysnames and ` for columns
break;
case DataEngine.TEXT:
WrapObj = new WrapObject { left = @"""" , middle = @""",""" , right = @"""" }; // not sure how to handle this yet
break;
default:
throw new ArgumentException ($"* * * DataEngine was not valid. Could not determine a OBJECT escape character.");
}
}
// Returns false for server bases DBMS and true for file based Db's like Access
private bool DataSourceIsFile(DataEngine e)
{
switch (e)
{
case DataEngine.ACCESS:
return true;
case DataEngine.MSSQL:
return false;
case DataEngine.EXCEL:
return true;
case DataEngine.ORACLE:
return false;
case DataEngine.MYSQL:
return false;
case DataEngine.TEXT:
return true;
default:
throw new ArgumentException ($"* * * DataEngine was not valid. Could not determine if this is a file or server DataSource.");
}
}
// Connection strings for any of the supported Providers
//https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/connection-string-syntax?view=netframework-4.7.1
private string GetConnectionString(DataProvider Provider)
{
string ProgId = Provider.ToString ( ).Replace ("_" , ".");
string result = "";
switch (Provider)
{
case DataProvider.Microsoft_ACE_OLEDB_12_0: // Microsoft MS OLEDB for MDB or ACCDB or EXCEL
case DataProvider.Microsoft_ACE_OLEDB: // VersionIndependentProgID
return $@"Provider={ProgId};Data Source={DataSource};Persist Security Info=False;Jet OLEDB:Database Password={Password.EmptyIfNull ( )};";
case DataProvider.Microsoft_Jet_OLEDB_4_0: // MS Access Jet OLEDB - Does not work with ACCDB or any SQL Server version
case DataProvider.Microsoft_Jet_OLEDB: // VersionIndependentProgID
// Jet db with user-lvel security requires a Workgroup information file designation:
// Jet OLEDB:System Database=|DataDirectory|\System.mdw;" <--- that could be stored in the Server field
// Jet user-level security uses the User ID and Password setting. A new constructer that includes the Username field will need to be added to support this
if (Engine == DataEngine.ACCESS && Password.NullIfEmpty() == null)
return $@"Provider={ProgId};Data Source={DataSource};User ID=Admin;Password=;";
else
return $@"Provider={ProgId};Data Source={DataSource};Persist Security Info=False;Jet OLEDB:Database Password={Password.EmptyIfNull ( )};";
case DataProvider.SQLNCLI11: // SQL Server OleDb Native Client
case DataProvider.SQLNCLI: // VersionIndependentProgID
result = $@"Provider={ProgId};Server={DataSource};Database={Database};";
if (Password != null) result += $"Uid={Username.EmptyIfNull ( )};Pwd={Password.EmptyIfNull ( )};";
if (Password == null) result += "Integrated Security=SSPI;";
return result;
case DataProvider.SQLOLEDB_1: // Microsoft OLE DB DataProvider for SQL Server (I've seen this work for ACCESS also) - DID NOT work for SQL Server 2016 Express, but DID work for SQL Server 2016 Developer Edition
case DataProvider.SQLOLEDB: // VersionIndependentProgID
result = $@"Provider={ProgId};Data Source={DataSource};Initial Catalog={Database};";
if (Password != null) result += $"User Id={Username.EmptyIfNull()};Password={Password.EmptyIfNull ( )};";
if (Password == null) result += "Integrated Security=SSPI;";
return result;
case DataProvider.MSDASQL_1: // Microsoft Data Access OleDb using ODbC
case DataProvider.MSDASQL: // VersionIndependentProgID
if (Engine == DataEngine.ACCESS)
return $@"Driver={{Microsoft Access Driver (*.mdb, *.accdb)}};DbQ={DataSource}";
if (Engine == DataEngine.EXCEL)
return $@"Driver={{Microsoft Excel Driver (*.xls, *.xlsx, *.xlsm, *.xlsb)}};DbQ={DataSource}";
if (Engine == DataEngine.MSSQL)
if (string.IsNullOrEmpty (Username))
return $@"[DataProvider = MSDASQL;] DRIVER={{SQL Server}}; SERVER={DataSource}; DATABASE={Database};UID=;Integrated Security=SSPI";
else
return $@"[DataProvider = MSDASQL;] DRIVER={{SQL Server}}; SERVER={DataSource}; DATABASE={Database};UID={Username};PWD={Password}";
else
throw new ArgumentException ($" * * * The MSDASQL Provider has only been set up for MS Access or Excel or MSSQL connections. Could not create Connection String");
case DataProvider.SQL__Server__Native__Client__11_0: // SQl Server OleDb using ODbC
if (Engine == DataEngine.MSSQL && (Username.NullIfEmpty ( ) != null))
return $@"Driver={{{ProgId}}};Server={DataSource};Database={Database};Uid={Username};Pwd={Password};";
else
return $@"Driver={{{ProgId}}};Server={DataSource};Database={Database};Trusted_Connection=yes;";
default:
throw new ArgumentException ($" * * * DataProvider is not valid. Could not create Connection String");
}
}
public string Columns(string value)
{
string[ ] c = value.Split (','); // if col names have "," in them, maybe replace it with another char and modify this method to replace it back.
return $"{WrapCol.left}{string.Join(WrapCol.middle , c)}{WrapCol.right}";
}
}
public static class StringExt
{
public static string NullIfEmpty(this string value)
{
return string.IsNullOrEmpty (value) ? null : value;
}
public static string EmptyIfNull(this string value)
{
if (string.IsNullOrEmpty (value))
return string.Empty;
else
return value;
}
}
}
if (reader.GetSqlInt32(datafiletype) == (int) Db.DataFileType.MDB)
for example, without calling any of the constructors that actually create the database connection. I just let that in there to illustrate how poorly conceived the class was. \$\endgroup\$