From 2040b9c1771fbcfc1e6e8701b03e65251fbb380b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kli=C5=9B?= Date: Thu, 19 Nov 2020 13:43:43 +0100 Subject: [PATCH] ORM implementation CRUD ORM implementation for directly quering database, without interacting with caching mechanics. --- .cr/personal/FavoritesList/List.xml | 6 + .gitignore | 3 +- Ado Cache Engine.sln | 6 + AdoCacheEngine/AdoCacheEngine.cs | 2 +- AdoCacheEngine/AdoCacheEngine.csproj | 5 + AdoCacheEngine/AdoCacheItem.cs | 26 ++-- AdoCacheEngine/ORM/Database.cs | 44 +++++++ AdoCacheEngine/ORM/Delete.cs | 77 ++++++++++++ AdoCacheEngine/ORM/Insert.cs | 143 ++++++++++++++++++++++ AdoCacheEngine/ORM/Select.cs | 109 +++++++++++++++++ AdoCacheEngine/ORM/Update.cs | 130 ++++++++++++++++++++ AdoCacheEngine/Properties/AssemblyInfo.cs | 4 +- 12 files changed, 538 insertions(+), 17 deletions(-) create mode 100644 .cr/personal/FavoritesList/List.xml create mode 100644 AdoCacheEngine/ORM/Database.cs create mode 100644 AdoCacheEngine/ORM/Delete.cs create mode 100644 AdoCacheEngine/ORM/Insert.cs create mode 100644 AdoCacheEngine/ORM/Select.cs create mode 100644 AdoCacheEngine/ORM/Update.cs diff --git a/.cr/personal/FavoritesList/List.xml b/.cr/personal/FavoritesList/List.xml new file mode 100644 index 0000000..a60e5ed --- /dev/null +++ b/.cr/personal/FavoritesList/List.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 77f908d..ece9703 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /.vs/ /AdoCacheEngine/bin /AdoCacheEngine/obj -/packages/ \ No newline at end of file +/packages/ +/TestConsoleApp/ \ No newline at end of file diff --git a/Ado Cache Engine.sln b/Ado Cache Engine.sln index 44290c1..d49cec9 100644 --- a/Ado Cache Engine.sln +++ b/Ado Cache Engine.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 16.0.30517.126 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AdoCacheEngine", "AdoCacheEngine\AdoCacheEngine.csproj", "{9FB34478-E895-42CD-B758-929F8DB4CBD2}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestConsoleApp", "TestConsoleApp\TestConsoleApp.csproj", "{0A8E693F-6AA8-403C-A015-917E6007382B}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {9FB34478-E895-42CD-B758-929F8DB4CBD2}.Debug|Any CPU.Build.0 = Debug|Any CPU {9FB34478-E895-42CD-B758-929F8DB4CBD2}.Release|Any CPU.ActiveCfg = Release|Any CPU {9FB34478-E895-42CD-B758-929F8DB4CBD2}.Release|Any CPU.Build.0 = Release|Any CPU + {0A8E693F-6AA8-403C-A015-917E6007382B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A8E693F-6AA8-403C-A015-917E6007382B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A8E693F-6AA8-403C-A015-917E6007382B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A8E693F-6AA8-403C-A015-917E6007382B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/AdoCacheEngine/AdoCacheEngine.cs b/AdoCacheEngine/AdoCacheEngine.cs index 7ba70a6..6908206 100644 --- a/AdoCacheEngine/AdoCacheEngine.cs +++ b/AdoCacheEngine/AdoCacheEngine.cs @@ -89,7 +89,7 @@ public AdoCacheEngine(string connectionString) { } /// - /// Get cached item. + /// Execute cached item. /// /// Cached type inheriting from AdoCacheEntity. /// Thrown when CacheItem for supplied type does not exists. diff --git a/AdoCacheEngine/AdoCacheEngine.csproj b/AdoCacheEngine/AdoCacheEngine.csproj index 4abcdb0..a0ad33e 100644 --- a/AdoCacheEngine/AdoCacheEngine.csproj +++ b/AdoCacheEngine/AdoCacheEngine.csproj @@ -59,6 +59,11 @@ + + + + + diff --git a/AdoCacheEngine/AdoCacheItem.cs b/AdoCacheEngine/AdoCacheItem.cs index cc45a03..f5136a3 100644 --- a/AdoCacheEngine/AdoCacheItem.cs +++ b/AdoCacheEngine/AdoCacheItem.cs @@ -536,7 +536,7 @@ public virtual void Delete(TEntity entity) } /// - /// Get entity from dictionary. + /// Execute entity from dictionary. /// /// Name of column. /// Value of key field. @@ -572,7 +572,7 @@ public virtual List FindInIndex(string nameOfColumn, object value) } /// - /// Get dictionary for column. + /// Execute dictionary for column. /// /// Name of column that dictionary is based on. /// Dictionary - a collection of KeyValuePair objects optimized for quick access by object's key. @@ -582,7 +582,7 @@ public virtual ConcurrentDictionary GetDictionary(string nameOf } /// - /// Get index for column. + /// Execute index for column. /// /// Name of column that index is based on. /// Index - list of references sorted by column. @@ -613,11 +613,11 @@ public virtual TEntity Insert(TEntity entity) // create instance of TEntity while passing 'true' to isManagedByCacheEngine TEntity newEntity = (TEntity) Activator.CreateInstance(typeof(TEntity), - BindingFlags.Instance | BindingFlags.NonPublic, null, - new object[] - { - true - }, null, null); + BindingFlags.Instance | BindingFlags.NonPublic, null, + new object[] + { + true + }, null, null); using (SqlConnection conn = new SqlConnection(_connectionString)) { @@ -649,7 +649,7 @@ public virtual TEntity Insert(TEntity entity) { throw new InvalidOperationException($"Column(s) {string.Join(", ", _autoIncrementColumns)} are marked with [AutoIncrement] attribute, but db engine did not return scope identity after Insert(). DATA ARE INCONSISTENT.", - ex); + ex); } } @@ -672,7 +672,7 @@ public virtual TEntity Insert(TEntity entity) else { foreach (PropertyInfo info in _columns - .Except(_keyColumns).Except(_autoIncrementColumns).Except(_readOnlyColumns)) + .Except(_keyColumns).Except(_autoIncrementColumns).Except(_readOnlyColumns)) { info.SetValue(newEntity, info.GetValue(entity)); } @@ -927,7 +927,7 @@ private static List FilterOutNotMatchingKeys(List orgList, Pro } /// - /// Get list of entities for table. + /// Execute list of entities for table. /// /// Table name. /// List of entities. @@ -1053,7 +1053,7 @@ private List GetEntities() } /// - /// Get Entities from database. + /// Execute Entities from database. /// /// Type of cached entity that is base for loading data. /// Cached item object that is used as base for loading data. @@ -1155,7 +1155,7 @@ private List GetEntitiesRelatedWith(AdoCacheItem } /// - /// Get Entities from database. + /// Execute Entities from database. /// /// Where clause. /// The list of Entities. diff --git a/AdoCacheEngine/ORM/Database.cs b/AdoCacheEngine/ORM/Database.cs new file mode 100644 index 0000000..839003a --- /dev/null +++ b/AdoCacheEngine/ORM/Database.cs @@ -0,0 +1,44 @@ +using System.Data; +using System.Data.SqlClient; + +namespace AdoCache.ORM +{ + public class Database + { + private SqlConnection _sqlConn; + + public Database(string connectionString) => ConnectionString = connectionString; + + #region API + + public Delete Delete() where TEntity : AdoCacheEntity, new() => new Delete(_sqlConn); + + public Insert Insert() where TEntity : AdoCacheEntity, new() => new Insert(_sqlConn); + + public Select Select() where TEntity : AdoCacheEntity, new() => new Select(_sqlConn); + + public Update Update() where TEntity : AdoCacheEntity, new() => new Update(_sqlConn); + + #endregion API + + #region Open/Close connection + + public string ConnectionString { get; } + + public void Close() + { + if (_sqlConn != null && (_sqlConn.State != ConnectionState.Closed || _sqlConn.State != ConnectionState.Broken)) + { + _sqlConn.Close(); + } + } + + public void Open() + { + _sqlConn = new SqlConnection(ConnectionString); + _sqlConn.Open(); + } + + #endregion Open/Close connection + } +} \ No newline at end of file diff --git a/AdoCacheEngine/ORM/Delete.cs b/AdoCacheEngine/ORM/Delete.cs new file mode 100644 index 0000000..d9aa68b --- /dev/null +++ b/AdoCacheEngine/ORM/Delete.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Linq.Expressions; +using AdoCache.Attributes; +using AdoCache.ryanohs; + +namespace AdoCache.ORM +{ + public class Delete where TEntity : AdoCacheEntity, new() + { + private readonly SqlConnection _sqlConn; + + private Expression> _clause; + + internal Delete(SqlConnection sqlConn) + { + _sqlConn = sqlConn; + + string tableName = (typeof(TEntity).GetCustomAttributes(typeof(TableNameAttribute), false)[0] as TableNameAttribute)?.TableName; + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentException("Could not deduce table name."); + } + + TableName = tableName; + } + + /// + /// Name of Table in data base. + /// + public string TableName { get; } + + #region API + + public int All() => Execute(); + + public int Execute() + { + SqlCommand command = new SqlCommand("", _sqlConn); + + if (_clause == null) + { + command.CommandText = $"DELETE FROM {TableName}"; + } + else + { + WherePart sql = new WhereBuilder().ToSql(_clause); + string whereClause = sql.Sql; + + foreach (KeyValuePair pair in sql.Parameters) + { + if (pair.Value == null) + { + whereClause = whereClause.Replace($"@{pair.Key}", "NULL"); + } + else + { + command.Parameters.AddWithValue($"@{pair.Key}", pair.Value); + } + } + + command.CommandText = $"DELETE FROM {TableName} WHERE {whereClause}"; + } + + return command.ExecuteNonQuery(); + } + + public Delete Where(Expression> clause) + { + _clause = clause; + return this; + } + + #endregion API + } +} \ No newline at end of file diff --git a/AdoCacheEngine/ORM/Insert.cs b/AdoCacheEngine/ORM/Insert.cs new file mode 100644 index 0000000..df48e91 --- /dev/null +++ b/AdoCacheEngine/ORM/Insert.cs @@ -0,0 +1,143 @@ +using System; +using System.Data.SqlClient; +using System.Linq; +using System.Reflection; +using AdoCache.Attributes; + +namespace AdoCache.ORM +{ + public class Insert where TEntity : AdoCacheEntity, new() + { + private readonly SqlConnection _sqlConn; + + /// + /// Array of columns with Auto Increment enabled. Those will be skipped during Insert and Update. + /// + protected PropertyInfo[] _autoIncrementColumns; + + /// + /// Array of columns in the Type. + /// + protected PropertyInfo[] _columns; + + /// + /// Array of columns with Read-Only property set to true. Those will be skipped during + /// Insert and Update. + /// + protected PropertyInfo[] _readOnlyColumns; + + internal Insert(SqlConnection sqlConn) + { + _sqlConn = sqlConn; + + string tableName = (typeof(TEntity).GetCustomAttributes(typeof(TableNameAttribute), false)[0] as TableNameAttribute)?.TableName; + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentException("Could not deduce table name."); + } + + TableName = tableName; + + _columns = typeof(TEntity).GetProperties().Where(p => p.CanWrite).ToArray(); + if (_columns == null || _columns.Length == 0) + { + throw new + ArgumentException($"Type {typeof(TEntity)} do not have any public Properties. Provided type needs to have at least one public property."); + } + + _autoIncrementColumns = _columns + .Where(p => p.CustomAttributes.Any(ca => ca.AttributeType == typeof(AutoIncrementAttribute))) + .ToArray(); + + _readOnlyColumns = _columns + .Where(p => p.CustomAttributes.Any(ca => ca.AttributeType == typeof(ReadOnlyAttribute))) + .Where(c => !_autoIncrementColumns.Contains(c)).ToArray(); + + if (_autoIncrementColumns != null && _autoIncrementColumns.Length > 1) + { + throw new + ArgumentException($"Type {typeof(TEntity)} have more than 1 column with auto-increment enabled. Only types with up to 1 auto-increment columns are supported."); + } + } + + /// + /// Name of Table in data base. + /// + public string TableName { get; } + + /// + /// Build Sql Command to insert entity to data base. + /// + /// Entity to insert. + /// Sql Connection that should be used. + /// + /// Should command include SCOPE_IDENTITY call at the end? + /// + /// Sql Command to insert entity. + private SqlCommand BuildInsertCommand(TEntity entity, SqlConnection conn, bool addScopeIdentity = false) + { + PropertyInfo[] cols = _columns.Where(c => !_autoIncrementColumns.Contains(c)).ToArray(); + if (_readOnlyColumns != null) + { + cols = cols.Where(c => !_readOnlyColumns.Contains(c)).ToArray(); + } + + string query = + $"INSERT INTO {TableName}({string.Join(", ", cols.Select(c => c.Name).ToArray())}) VALUES({string.Join(", ", cols.Select(c => "@" + c.Name).ToArray())});{(addScopeIdentity ? "SELECT SCOPE_IDENTITY();" : "")}"; + + SqlCommand cmd = new SqlCommand(query, conn); + foreach (PropertyInfo col in cols) + { + object value = col.GetValue(entity); + cmd.Parameters.AddWithValue("@" + col.Name, value ?? DBNull.Value); + } + + return cmd; + } + + #region API + + public int Value(TEntity entity) + { + int rowsCount = 0; + using (SqlCommand insert = BuildInsertCommand(entity, _sqlConn)) + { + rowsCount = insert.ExecuteNonQuery(); + + if (rowsCount <= 0) + { + throw new + InvalidOperationException($"There was an unexpected result while Inserting data to data base. Number of rows affected: {rowsCount}"); + } + } + + return rowsCount; + } + + public int Value(TEntity entity, out int scopeIdentity) + { + using (SqlCommand insert = BuildInsertCommand(entity, _sqlConn, true)) + { + object scalar = insert.ExecuteScalar(); + try + { + scopeIdentity = Convert.ToInt32(scalar); + if (scopeIdentity <= 0) + { + throw new + InvalidOperationException($"There was an unexpected result while Inserting data to data base. Returned index: {scopeIdentity}"); + } + } + catch (Exception e) + { + throw new + InvalidOperationException("INSERT command returned unexpected value.", e); + } + } + + return 1; + } + + #endregion API + } +} \ No newline at end of file diff --git a/AdoCacheEngine/ORM/Select.cs b/AdoCacheEngine/ORM/Select.cs new file mode 100644 index 0000000..2f590c5 --- /dev/null +++ b/AdoCacheEngine/ORM/Select.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq.Expressions; +using System.Reflection; +using AdoCache.Attributes; +using AdoCache.ryanohs; + +namespace AdoCache.ORM +{ + public class Select where TEntity : AdoCacheEntity, new() + { + private readonly SqlConnection _sqlConn; + protected Expression> _clause; + + internal Select(SqlConnection sqlConn) + { + _sqlConn = sqlConn; + + string tableName = (typeof(TEntity).GetCustomAttributes(typeof(TableNameAttribute), false)[0] as TableNameAttribute)?.TableName; + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentException("Could not deduce table name."); + } + + TableName = tableName; + } + + public string TableName { get; } + + private static List GetEntityList(DataTable table) + { + List entities = new List(); + foreach (DataRow row in table.Rows) + { + TEntity newEntity = new TEntity(); + + foreach (DataColumn column in table.Columns) + { + PropertyInfo property = typeof(TEntity).GetProperty(column.ColumnName); + if (property != null) + { + property.SetValue(newEntity, row[column.ColumnName] is DBNull ? null : row[column.ColumnName]); + } + else + { + throw new ArgumentOutOfRangeException($"Column '{column.ColumnName}' doesn't exists in model class."); + } + } + + entities.Add(newEntity); + } + + return entities; + } + + #region API + + public List All() => Execute(); + + public List Execute() + { + DataTable table = null; + + SqlCommand command = new SqlCommand("", _sqlConn); + + if (_clause == null) + { + command.CommandText = $"SELECT * FROM {TableName}"; + } + else + { + WherePart sql = new WhereBuilder().ToSql(_clause); + string whereClause = sql.Sql; + + foreach (KeyValuePair pair in sql.Parameters) + { + if (pair.Value == null) + { + whereClause = whereClause.Replace($"@{pair.Key}", "NULL"); + } + else + { + command.Parameters.AddWithValue($"@{pair.Key}", pair.Value); + } + } + + command.CommandText = $"SELECT * FROM {TableName} WHERE {whereClause}"; + } + + using (SqlDataAdapter adapter = new SqlDataAdapter(command)) + { + table = new DataTable(TableName); + adapter.Fill(table); + } + + return GetEntityList(table); + } + + public Select Where(Expression> clause) + { + _clause = clause; + return this; + } + + #endregion API + } +} \ No newline at end of file diff --git a/AdoCacheEngine/ORM/Update.cs b/AdoCacheEngine/ORM/Update.cs new file mode 100644 index 0000000..981b876 --- /dev/null +++ b/AdoCacheEngine/ORM/Update.cs @@ -0,0 +1,130 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using AdoCache.Attributes; +using AdoCache.ryanohs; + +namespace AdoCache.ORM +{ + public class Update where TEntity : AdoCacheEntity, new() + { + protected readonly SqlConnection _sqlConn; + protected Expression> _clause; + + protected Dictionary _setDict = new Dictionary(); + + internal Update(SqlConnection sqlConn) + { + _sqlConn = sqlConn; + + string tableName = (typeof(TEntity).GetCustomAttributes(typeof(TableNameAttribute), false)[0] as TableNameAttribute)?.TableName; + if (string.IsNullOrWhiteSpace(tableName)) + { + throw new ArgumentException("Could not deduce table name."); + } + + TableName = tableName; + } + + public string TableName { get; } + + /// + /// Build Sql Command to update entity in data base. + /// + /// Entity to update. + /// Sql Connection to use. + /// Sql Command with Update statement. + private SqlCommand BuildUpdateCommand(SqlConnection conn) + { + StringBuilder query = new StringBuilder($"UPDATE {TableName} "); + + SqlCommand command = new SqlCommand {Connection = conn}; + + query.Append($"SET {string.Join(", ", _setDict.Select(sd => $"{sd.Key} = @{sd.Key}"))}"); + foreach (KeyValuePair pair in _setDict) + { + command.Parameters.AddWithValue($@"{pair.Key}", pair.Value ?? DBNull.Value); + } + + if (_clause != null) + { + WherePart sql = new WhereBuilder().ToSql(_clause); + string whereClause = sql.Sql; + + foreach (KeyValuePair pair in sql.Parameters) + { + if (pair.Value == null) + { + whereClause = whereClause.Replace($"@{pair.Key}", "NULL"); + } + else + { + command.Parameters.AddWithValue($"@{pair.Key}", pair.Value); + } + } + + query.Append($" WHERE {whereClause}"); + } + + command.CommandText = query.ToString(); + return command; + } + + #region API + + public int Execute() + { + int rowsCount = 0; + using (SqlCommand update = BuildUpdateCommand(_sqlConn)) + { + rowsCount = update.ExecuteNonQuery(); + if (rowsCount <= 0) + { + throw new + InvalidOperationException($"There was an unexpected result while Updating data in data base. Number of rows affected: {rowsCount}"); + } + } + + return rowsCount; + } + + public Update Set(Expression> selector, TResult value) + { + if (selector.Body.NodeType == ExpressionType.MemberAccess) + { + if (selector.Body is MemberExpression member) + { + if (_setDict.ContainsKey(member.Member.Name)) + { + _setDict[member.Member.Name] = value; + } + else + { + _setDict.Add(member.Member.Name, value); + } + } + else + { + throw new ArgumentException("Selector's Body need to be a MemberExpression."); + } + } + else + { + throw new ArgumentException("Provided selector has incorrect structure. Only Expressions of type MemberAccess are supported."); + } + + return this; + } + + public Update Where(Expression> clause) + { + _clause = clause; + return this; + } + + #endregion API + } +} \ No newline at end of file diff --git a/AdoCacheEngine/Properties/AssemblyInfo.cs b/AdoCacheEngine/Properties/AssemblyInfo.cs index 5ef8b5d..83ca79b 100644 --- a/AdoCacheEngine/Properties/AssemblyInfo.cs +++ b/AdoCacheEngine/Properties/AssemblyInfo.cs @@ -32,5 +32,5 @@ // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.13.3.0")] -[assembly: AssemblyFileVersion("1.13.3.0")] +[assembly: AssemblyVersion("2.0.0.0")] +[assembly: AssemblyFileVersion("2.0.0.0")]