Story
For a school project, we had to create an app that communicates with a database in my case, MySQL. Since I didn't want to make something complex, I've made something even more complex. I made a class that helps with the communication with the server:
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace SQLManager
{
public class SQLManager
{
private readonly SqlConnection _sqlConnection;
private readonly Action<string> _loggerAction;
private static readonly Type[] _SQLObjects;
static SQLManager()
{
List<Type> sqlObjects = new List<Type>(); // new();
// https://stackoverflow.com/a/851287
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (var assembly in assemblies)
{
// https://stackoverflow.com/a/1315668
foreach (var @class in assembly.GetTypes())
{
// https://stackoverflow.com/a/11227373
if (!@class.UnderlyingSystemType.IsSubclassOf(typeof(SQLObject)))
continue;
// Found a SQLObject
SQLObjectAttribute sQLObjectAttribute = @class.GetCustomAttribute<SQLObjectAttribute>();
if (sQLObjectAttribute.IsNameValid())
{
sqlObjects.Add(@class.UnderlyingSystemType);
}
else
throw new Exception($"Class named \'{@class.Name}\' was found, but the attribute {nameof(SQLObjectAttribute)} has an invalid Name.");
}
}
_SQLObjects = sqlObjects.ToArray();
}
/// <summary>
/// Connects the user to the database with the given ID and the given password
/// </summary>
/// <param name="id">ID of the account</param>
/// <param name="password">Password of the account</param>
public SQLManager(string dataSourceName, string databaseName, string id, string password, Action<string> loggerAction)
{
_sqlConnection = new SqlConnection($"Data source ={dataSourceName}; Initial Catalog ={databaseName}; User Id= {id};password ={password}");
_loggerAction = loggerAction;
TryOpenConnection(caller: nameof(SQLManager));
OnLog($"SQL Manager Object created on '{dataSourceName}' ({databaseName}) by '{id}'", nameof(SQLManager));
}
enum CommandType
{
Scalar,
NonQuery,
Reader
}
/// <summary>
/// Executes the given command with the correct method
/// </summary>
/// <typeparam name="T">Type of the expected value</typeparam>
/// <param name="sqlCmd">Command to execute</param>
/// <param name="type">Type of the command</param>
/// <returns>Value of the given command</returns>
private T ExecuteSQLCommand<T>(string sqlCmd, CommandType type, [CallerMemberName] string caller = "")
{
if (_sqlConnection != null)
return ExecCommand<T>(new SqlCommand(sqlCmd, _sqlConnection), type, caller);
OnLog("SQL Connection is not defined.");
return default;
}
private T ExecCommand<T>(SqlCommand cmd, CommandType type, [CallerMemberName] string caller = "")
{
OnLog($"Request to execute the command '{cmd.CommandText}' ({type}).", caller);
try
{
TryOpenConnection(true);
object value = default;
switch (type)
{
case CommandType.Scalar:
value = cmd.ExecuteScalar(); // First column of first row (min, max, count, avg)
break;
case CommandType.NonQuery:
value = cmd.ExecuteNonQuery(); // DML Request (Update, insert, delete)
break;
case CommandType.Reader:
value = cmd.ExecuteReader(); // Table
break;
default:
throw new NotImplementedException();
}
if (type != CommandType.Reader) // Needs to stay opened
TryCloseConnection(true);
return (T)value;
}
catch (Exception e)
{
OnLog(e.Message);
}
return default;
}
// TODO: Make a single method that takes the command, checks if it's valid, then execute the correct commad type
private T ExecuteScalarCommand<T>(string sqlCmd, [CallerMemberName] string caller = "")
=> ExecuteSQLCommand<T>(sqlCmd, CommandType.Scalar, caller);
private SqlDataReader ExecuteReaderCommand(string sqlCmd, [CallerMemberName] string caller = "")
=> ExecuteSQLCommand<SqlDataReader>(sqlCmd, CommandType.Reader, caller);
private int ExecuteNonQueryCommand(string sqlCmd, [CallerMemberName] string caller = "")
=> ExecuteSQLCommand<int>(sqlCmd, CommandType.NonQuery, caller);
public object[] ExecuteStoredProcedure(string procName, Dictionary<string, (object, ParameterDirection)> parameters)
{
object[] outputsValue = null;
try
{
SqlCommand cmdInsert = new SqlCommand(procName, _sqlConnection)
{
CommandType = System.Data.CommandType.StoredProcedure
};
var parms = parameters.ToArray();
var outputs = new List<SqlParameter>();
bool hasReturnValue = false;
foreach (var item in parms)
{
var (value, dir) = item.Value;
var sqlParameter = new SqlParameter(item.Key.StartsWith("@") ? item.Key : $"@{item.Key}", value)
{
Direction = dir
};
if (sqlParameter.Direction == ParameterDirection.ReturnValue)
hasReturnValue = true;
if (sqlParameter.Direction == ParameterDirection.Output || sqlParameter.Direction == ParameterDirection.ReturnValue)
outputs.Add(sqlParameter);
cmdInsert.Parameters.Add(sqlParameter);
}
TryOpenConnection();
OnLog($"Executing the stored procedure '{procName}'.");
if (hasReturnValue)
cmdInsert.ExecuteScalar();
else
cmdInsert.ExecuteNonQuery();
TryCloseConnection();
outputsValue = new object[outputs.Count];
for (int i = 0; i < outputs.Count; ++i)
outputsValue[i] = outputs[i].Value;
}
catch (Exception e)
{
OnLog(e.Message);
}
TryCloseConnection();
return outputsValue;
}
public SqlDataReader ExecuteFunction(string funcName)
{
return ExecuteReaderCommand($"SELECT * FROM {funcName}()");
}
/// <returns>Value of the given column</returns>
public object GetValue(SqlDataReader reader, string[] headers, string propertyName, Type propertyType)
{
try
{
int index = Array.IndexOf(headers, propertyName);
if (index == -1)
{
OnLog($"Column {propertyName} not found.");
return default;
}
return reader.GetValue(index);
}
catch (Exception e)
{
OnLog(e.Message);
}
return default;
}
/// <summary>
/// Gives the name of the SQL Object linked to the given class
/// </summary>
/// <returns>Name of the SQL Object</returns>
public string GetTableName<T>()
{
try
{
int index = Array.IndexOf(_SQLObjects, typeof(T));
if (index != -1)
return _SQLObjects[index].GetCustomAttribute<SQLObjectAttribute>().GetName();
}
catch (Exception e)
{
OnLog(e.Message);
return null; // ERROR
}
OnLog($"Type {typeof(T)} is not supported.");
return null; // INVALID TYPE
}
/// <summary>
/// Loads all the object of the given class
/// </summary>
/// <remarks>If the given class has</remarks>
public void LoadItems<T>(out List<T> loadedItems, bool sortItems = true) where T : SQLObject
{
string nomTable = GetTableName<T>();
loadedItems = new List<T>();
if (string.IsNullOrEmpty(nomTable))
{
OnLog($"No SQL Object has the type {typeof(T)}.");
return;
}
SqlDataReader sqlRead = this.ExecuteReaderCommand($"SELECT * FROM {nomTable}");
if (sqlRead == null)
{
OnLog($"No table or view named \'{nomTable}\' exists.");
return;
}
LoadItems(sqlRead, out loadedItems);
if (sortItems && Array.IndexOf(typeof(T).GetInterfaces(), typeof(IComparable)) != -1)
{
loadedItems.Sort();
OnLog("Items were sorted.");
}
}
public void LoadItems<T>(SqlDataReader sqlRead, out List<T> loadedItems) where T : SQLObject
{
loadedItems = new List<T>(); // new();
if (sqlRead == null)
{
OnLog("Given SqlDataReader is not defined.");
return;
}
bool headersInitialized = false;
string[] headers = null;
// Get fields with the attribute SQLField
var fields = typeof(Form1.Restaurant).GetFields().ToList();
var names = new List<string>();
for (int i = fields.Count - 1; i >= 0; --i)
{
var attribute = fields[i].GetCustomAttribute<SQLFieldAttribute>();
if (attribute == null)
fields.RemoveAt(i);
else
names.Add(attribute.GetName());
}
try
{
while (sqlRead.Read())
{
if (!headersInitialized)
{
headers = new string[sqlRead.FieldCount];
for (int i = 0; i < headers.Length; i++)
{
headers[i] = sqlRead.GetName(i);
}
headersInitialized = true;
}
T instance = (T)Activator.CreateInstance(typeof(T));
for (int i = 0; i != fields.Count; ++i)
fields[i].SetValue(instance, GetValue(sqlRead, headers, names[fields.Count - i - 1], fields[i].FieldType));
loadedItems.Add(instance);
}
}
catch (Exception e)
{
OnLog(e.Message);
}
//sqlRead.Close();
//sqlRead.Dispose();
TryCloseConnection();
}
public int GetItemsCount<T>([CallerMemberName] string caller = "")
{
string nomTable = GetTableName<T>();
OnLog($"Request to get the items count of '{nomTable}'.", caller);
if (string.IsNullOrEmpty(nomTable))
{
OnLog($"No SQL Object has the type {typeof(T)}.");
return -1;
}
return this.ExecuteScalarCommand<int>($"SELECT COUNT(*) FROM {nomTable}");
}
/// <summary>
/// Updates the entry that has the same ID
/// </summary>
public void UpdateItem<T>(T item) where T : SQLObject
{
try
{
// https://stackoverflow.com/a/4144817
PropertyInfo[] properties = typeof(T).GetProperties();
PropertyInfo firstProperty = properties[0];
// TODO: Make an abstract SQLObject and search for the ID property
KeyValuePair<string, object> IDCLASSE = new KeyValuePair<string, object>(firstProperty.Name, firstProperty.GetValue(item));
string tableName = GetTableName<T>();
SqlDataReader reader = this.ExecuteReaderCommand($"SELECT * FROM {tableName} WHERE {IDCLASSE.Key} = {IDCLASSE.Value}");
reader.Read();
foreach (PropertyInfo prop in properties)
{
object newValue = prop.GetValue(item);
object oldValue = reader[prop.Name];
// Optimisation (Limitation des packages envoyés)
if (!oldValue.Equals(newValue))
{
this.ExecuteNonQueryCommand($"UPDATE {tableName} SET {prop.Name} = {newValue} WHERE {IDCLASSE.Key} = {IDCLASSE.Value}");
OnLog($"Updated \'{prop.Name}\' from {oldValue} to {newValue}");
}
}
}
catch (Exception e)
{
OnLog(e);
}
}
public void OnLog(object value, [CallerMemberName] string caller = "")
{
string message = "";
if (!string.IsNullOrEmpty(caller))
message += $"[{caller}] ";
_loggerAction?.Invoke(message + value.ToString());
}
/// <summary>
/// Try to open the SQL connection
/// </summary>
/// <param name="silentCheck">The error message will show if the connection is already opened</param>
private void TryOpenConnection(bool silentCheck = false, [CallerMemberName] string caller = "")
{
if (_sqlConnection.State != System.Data.ConnectionState.Closed && _sqlConnection.State != System.Data.ConnectionState.Broken)
{
if (!silentCheck)
OnLog($"The connection is already opened ({_sqlConnection.State}).", caller);
return;
}
try
{
_sqlConnection.Open();
OnLog("Successfully opened the SQL Connection.", caller);
}
catch (Exception e)
{
OnLog(e.Message, caller);
}
}
public void CloseConnection([CallerMemberName] string caller = "") => TryCloseConnection(false, caller);
/// <summary>
/// Try to close the SQL connection
/// </summary>
/// <param name="silentCheck">The error message will show if the connection is already closed</param>
private void TryCloseConnection(bool silentCheck = false, [CallerMemberName] string caller = "")
{
if (_sqlConnection.State == System.Data.ConnectionState.Broken ||
_sqlConnection.State == System.Data.ConnectionState.Closed)
{
if (!silentCheck)
OnLog($"The connection is already closed ({_sqlConnection.State}).", caller);
return;
}
try
{
_sqlConnection.Close();
OnLog("Successfully closed the SQL Connection.", caller);
}
catch (Exception e)
{
OnLog(e.Message, caller);
}
}
~SQLManager()
{
TryCloseConnection(caller: "SQLManager");
}
/// <summary>
/// Determines what class can be converted into a SQL object
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class SQLObjectAttribute : Attribute
{
string Name { get; set; }
public SQLObjectAttribute(string name)
{
Name = name;
}
public string GetName() => Name;
public bool IsNameValid() => !string.IsNullOrEmpty(Name);
}
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public class SQLFieldAttribute : Attribute
{
string Name { get; set; }
public SQLFieldAttribute(string name)
{
Name = name;
}
public string GetName() => Name;
}
[SQLObject(null)]
public abstract class SQLObject
{
/// <summary>
/// Field that has an unique value for every object
/// </summary>
public abstract object ID_OBJECT { get; }
}
}
}Conclusion
It is not the best, but it works if you are trying to find something "small" that does the job. I'm not the greatest at T-SQL so feel free to modify it for your needs.