namespace Framework.DataAccessLayer { using Framework.Config; using Framework.DataAccessLayer.DatabaseMemory; using Framework.Json; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using System; using System.Collections; using System.Collections.Generic; using System.Data; using Microsoft.Data.SqlClient; using System.Linq; using System.Linq.Dynamic.Core; using System.Reflection; using System.Text; using System.Threading.Tasks; using static Framework.DataAccessLayer.UtilDalType; using System.Globalization; /// /// Linq to database or linq to memory. /// public enum DatabaseEnum { None = 0, /// /// Linq to database. Update and insert data with methods Data.Update(); and Data.Insert(); /// Database = 1, /// /// Linq to memory shared by multiple requests (singleton scope). Update and insert data with methods Data.Update(); and Data.Insert(); /// For update it assumes first field is primary key. /// Memory = 2, /// /// Linq to memory (request scope). /// // MemoryRequest = 3, // Replaced with Custom /// /// Linq to custom data source. For example list on ComponentJson. Update and insert data by overriding methods Gird.Update(); and Grid.Insert(); /// Custom = 4, } /// /// Data access layer functions. /// public static class Data // public static class UtilDal { /// /// Update or insert data row. /// public static async Task UpsertAsync(Type typeRow, Row row, string[] fieldNameKeyList) { List rowList = new List { row }; await UtilDalUpsert.UpsertAsync(typeRow, rowList, fieldNameKeyList); } /// /// Update or insert data row. /// public static async Task UpsertAsync(TRow row, string[] fieldNameKeyList) where TRow : Row { await UpsertAsync(typeof(TRow), row, fieldNameKeyList); } /// /// Returns memory where rows are stored. /// public static IList MemoryRowList(Type typeRow, DatabaseEnum databaseEnum = DatabaseEnum.Memory) { switch (databaseEnum) { case DatabaseEnum.Memory: return DatabaseMemory.DatabaseMemoryInternal.Instance.RowListGet(typeRow); default: throw new Exception("DatabaseEnum not supported!"); } } /// /// Returns linq to memory query. /// public static List MemoryRowList(DatabaseEnum databaseEnum = DatabaseEnum.Memory) where TRow : Row { switch (databaseEnum) { case DatabaseEnum.Memory: return (List)MemoryRowList(typeof(TRow)); default: throw new Exception("Scope not supported!"); } } /// /// DbContext caches Model by default by DbContext type. Framework needs /// a Model by DbContextInternal.TypeRow. /// internal class ModelCacheKeyFactory : IModelCacheKeyFactory { public object Create(DbContext context) { DbContextInternal dbContextInternal = (DbContextInternal)context; return new ModelCacheKey(dbContextInternal.TypeRow, dbContextInternal.IsQuery); } } /// /// One DbContext for DbSet and one DbContext for DbQuery for one TypeRow. /// internal class ModelCacheKey { public ModelCacheKey(Type typeRow, bool isQuery) { this.TypeRow = typeRow; this.IsQuery = isQuery; } public readonly Type TypeRow; /// /// Gets IsQuery. If true, EF Core DbQuery is used otherwise DbSet. /// DbQuery is used for SELECT. DbSet for INSERT, UPDATE and DELETE. /// public readonly bool IsQuery; public override int GetHashCode() { return TypeRow.GetHashCode() + IsQuery.GetHashCode(); } public override bool Equals(object obj) { ModelCacheKey modelCacheKey = (ModelCacheKey)obj; return TypeRow == modelCacheKey.TypeRow && IsQuery == modelCacheKey.IsQuery; } } /// /// DbContext for one TypeRow. /// internal class DbContextInternal : DbContext { public DbContextInternal(string connectionString, Type typeRow, bool isQuery) { this.ConnectionString = connectionString; this.TypeRow = typeRow; this.IsQuery = isQuery; } public readonly string ConnectionString; public readonly Type TypeRow; /// /// Gets IsQuery. If true, EF Core DbQuery is used otherwise DbSet. /// DbQuery is used for SELECT. DbSet for INSERT, UPDATE and DELETE. /// public readonly bool IsQuery; /// /// Gets Query for TypeRow. /// public IQueryable Query { get { var methodInfo = GetType().GetMethods().Where(item => item.Name == "Set" && item.IsGenericMethod).First(); return (IQueryable)methodInfo.MakeGenericMethod(TypeRow).Invoke(this, null); } } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlServer(ConnectionString); optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); optionsBuilder.ReplaceService(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { var fieldList = UtilDalType.TypeRowToFieldList(TypeRow); SqlTableAttribute tableAttribute = (SqlTableAttribute)TypeRow.GetCustomAttribute(typeof(SqlTableAttribute)); if (IsQuery == false) { // Table (DbContext.DbSet) var entityTypeBuilder = modelBuilder.Entity(TypeRow); entityTypeBuilder.ToTable(tableAttribute.TableNameSql, tableAttribute.SchemaNameSql); // By default EF maps sql table name to class name. bool isPrimaryKey = false; // Sql view foreach (var field in fieldList) { if (field.FieldNameSql == null) // Calculated column. Do not include it in sql select. { entityTypeBuilder.Ignore(field.PropertyInfo.Name); } else { if (field.IsPrimaryKey) { isPrimaryKey = true; entityTypeBuilder.HasKey(field.PropertyInfo.Name); // Prevent null exception if primary key name is not "Id". } var propertyBuilder = entityTypeBuilder.Property(field.PropertyInfo.PropertyType, field.PropertyInfo.Name); propertyBuilder.HasColumnName(field.FieldNameSql); if (UtilDalType.FrameworkTypeFromEnum(field.FrameworkTypeEnum).SqlTypeName == "datetime") { // Prevent "Conversion failed when converting date and/or time from character string." exception for // sql field type "datetime" for dynamic linq where function. See also method QueryFilter(); propertyBuilder.HasColumnType("datetime"); } } } if (isPrimaryKey == false) { throw new Exception("No primary key defined! See also property IsHandled."); // Did you set result.IsHandled? } } else { // Query (DbContext.DbQuery) var entityTypeBuilder = modelBuilder.Entity(TypeRow); entityTypeBuilder.HasNoKey(); entityTypeBuilder.ToView(tableAttribute.TableNameSql, tableAttribute.SchemaNameSql); // By default EF maps sql table name to class name. foreach (var field in fieldList) { if (field.FieldNameSql == null) // Calculated column. Do not include it in sql select. { entityTypeBuilder.Ignore(field.PropertyInfo.Name); } else { var propertyBuilder = entityTypeBuilder.Property(field.PropertyInfo.PropertyType, field.PropertyInfo.Name); propertyBuilder.HasColumnName(field.FieldNameSql); if (UtilDalType.FrameworkTypeFromEnum(field.FrameworkTypeEnum).SqlTypeName == "datetime") { // Prevent "Conversion failed when converting date and/or time from character string." exception for // sql field type "datetime" for dynamic linq where function. See also method QueryFilter(); propertyBuilder.HasColumnType("datetime"); } } } } } } /* CREATE VIEW MyDebug AS SELECT 1 AS Id, 'Blue' AS Text UNION ALL SELECT NULL AS Id, NULL AS Text */ /* public class MyDebug { public int? Id { get; set; } public string Text { get; set; } public static void Run() { foreach (var item in new DbContextDebug().MyQuery) { } } } internal class DbContextDebug : DbContext { public DbSet MyDebug { get; set; } public IQueryable MyQuery { get { var result = (IQueryable)(this.GetType().GetMethods().Where(item => item.Name == "Set" && item.IsGenericMethod).First()).MakeGenericMethod(typeof(MyDebug)).Invoke(this, null); return (IQueryable)result; } } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlServer(ConfigServer.ConnectionString(isFrameworkDb: false)); // optionsBuilder.UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking); // optionsBuilder.ReplaceService(); } protected override void OnModelCreating(ModelBuilder modelBuilder) { // Entity model // var entityBuilder = modelBuilder.Entity(typeof(MyDebug)); var entityBuilder = modelBuilder.Entity(typeof(MyDebug)).HasNoKey(); entityBuilder.ToView("MyDebug"); entityBuilder.Property("Id"); entityBuilder.Property("Text"); } } */ /// /// Returns DbContext with ConnectionString and model for one row, defined in typeRow. /// internal static DbContextInternal DbContextInternalCreate(Type typeRow, bool isQuery) { string connectionString = ConfigServer.ConnectionString(typeRow); if (connectionString == null) { throw new Exception("ConnectionString is null! (See also file: ConfigServer.json)"); // Run command ".\cli.cmd config ConnectionString=..." } if (UtilDalType.TypeRowIsTableNameSql(typeRow) == false) { throw new Exception("TypeRow does not have TableNameSql definition!"); } return new DbContextInternal(connectionString, typeRow, isQuery); } private static string ExecuteParamAddPrivate(FrameworkTypeEnum frameworkTypeEnum, string paramName, object value, List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)> paramList) { FrameworkType frameworkType = UtilDalType.FrameworkTypeFromEnum(frameworkTypeEnum); // ParamName if (paramName == null) { paramName = $"@P{ paramList.Count }"; } UtilFramework.Assert(paramName.StartsWith("@"), "Parameter does not start with @!"); UtilFramework.Assert(paramList.Where(item => item.SqlParameter.ParameterName == paramName).Count() == 0, string.Format("ParamName already exists! ({0})", paramName)); // Value if (value != null) { UtilFramework.Assert(value.GetType() == frameworkType.ValueType); } if (value is string && (string)value == "") { value = null; } if (value == null) { value = DBNull.Value; } SqlParameter parameter = new SqlParameter(paramName, frameworkType.DbType) { Value = value }; paramList.Add((frameworkTypeEnum, parameter)); if (frameworkType.DbType == DbType.Binary) { // Prevent error: Implicit conversion from data type nvarchar to varbinary(max) is not allowed. Use the CONVERT function to run this query paramName = "CONVERT(VARBINARY(MAX), " + paramName + ")"; } return paramName; } /// /// Adds sql param. /// internal static void ExecuteParamAdd(FrameworkTypeEnum frameworkTypeEnum, string paramName, object value, List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)> paramList) { ExecuteParamAddPrivate(frameworkTypeEnum, paramName, value, paramList); } /// /// Adds sql param and returns new paramName. For example "@P0". /// internal static string ExecuteParamAdd(FrameworkTypeEnum frameworkTypeEnum, object value, List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)> paramList) { return ExecuteParamAddPrivate(frameworkTypeEnum, null, value, paramList); } /// /// Replaces sql params with text params and returns full sql for debugging. /// internal static string ExecuteParamDebug(string sql, List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)> paramList) { paramList = paramList.OrderByDescending(item => item.SqlParameter.ParameterName).ToList(); // Replace first @P100, then @P10 foreach (var param in paramList) { FrameworkType frameworkType = UtilDalType.FrameworkTypeFromEnum(param.FrameworkTypeEnum); string find = param.SqlParameter.ParameterName; string replace = frameworkType.ValueToSqlParameterDebug(param.SqlParameter.Value); sql = UtilFramework.Replace(sql, find, replace); } return sql; } /// /// Execute sql statement. Can contain "GO" batch seperator. /// internal static async Task ExecuteNonQueryAsync(string sql, List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)> paramList, bool isFrameworkDb, int? commandTimeout = null, bool isExceptionContinue = false) { var sqlList = sql.Split(new string[] { "\r\nGO", "\nGO", "GO\r\n", "GO\n" }, StringSplitOptions.RemoveEmptyEntries); string connectionString = ConfigServer.ConnectionString(isFrameworkDb); using SqlConnection sqlConnection = new SqlConnection(connectionString); sqlConnection.Open(); foreach (string sqlItem in sqlList) { SqlCommand sqlCommand = new SqlCommand(sqlItem, sqlConnection); if (commandTimeout.HasValue) { sqlCommand.CommandTimeout = commandTimeout.Value; } if (paramList?.Count > 0) { sqlCommand.Parameters.AddRange(paramList.Select(item => item.SqlParameter).ToArray()); } try { await sqlCommand.ExecuteNonQueryAsync(); } catch (Exception exception) { if (isExceptionContinue) { Console.WriteLine(exception.Message); } else { throw; } } } } /// /// Execute sql statement. Can contain "GO" batch seperator. /// public static async Task ExecuteNonQueryAsync(string sql) { await ExecuteNonQueryAsync(sql, null, isFrameworkDb: false); } /// /// Returns multiple result sets from stored procedure or select. /// /// For example: "SELECT 1 AS A SELECT 2 AS B"; or with parameter: "SELECT @P0 AS A"; /// See also method ExecuteParamAdd(); /// Returns (ResultSet, Row, ColumnName, Value) value list. internal static async Task>>> ExecuteReaderMultipleAsync(string sql, List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)> paramList = null, bool isFrameworkDb = false) { List>> result = new List>>(); string connectionString = ConfigServer.ConnectionString(isFrameworkDb); using (SqlConnection sqlConnection = new SqlConnection(connectionString)) { sqlConnection.Open(); SqlCommand sqlCommand = new SqlCommand(sql, sqlConnection); if (paramList?.Count > 0) { sqlCommand.Parameters.AddRange(paramList.Select(item => item.SqlParameter).ToArray()); } using SqlDataReader sqlDataReader = await sqlCommand.ExecuteReaderAsync(); while (sqlDataReader.HasRows) { var rowList = new List>(); result.Add(rowList); while (sqlDataReader.Read()) { var row = new Dictionary(); rowList.Add(row); for (int i = 0; i < sqlDataReader.FieldCount; i++) { string columnName = sqlDataReader.GetName(i); object value = sqlDataReader.GetValue(i); if (value == DBNull.Value) { value = null; } row.Add(columnName, value); } } sqlDataReader.NextResult(); } } return result; } /// /// Execute sql statement. /// internal static async Task> ExecuteReaderAsync(Type typeRow, string sql, List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)> paramList = null, bool isFrameworkDb = false) { var valueList = await ExecuteReaderMultipleAsync(sql, paramList, isFrameworkDb); return ExecuteReaderMultipleResultCopy(typeRow, valueList, 0); } /// /// Execute sql statement. /// internal static async Task> ExecuteReaderAsync(string sql, List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)> paramList, bool isFrameworkDb = false) where TRow : Row { var valueList = await ExecuteReaderMultipleAsync(sql, paramList, isFrameworkDb); return ExecuteReaderMultipleResultCopy(valueList, 0); } /// /// Convert one sql multiple result set to typed row list. /// /// Result of method ExecuteReaderAsync(); /// Sql multiple result set index. /// Type of row to copy to. Copy one multiple result set. /// List of typed rows. internal static List ExecuteReaderMultipleResultCopy(Type typeRow, List>> valueList, int resultSetIndex) { List result = new List(); PropertyInfo[] propertyInfoList = UtilDalType.TypeRowToPropertyInfoList(typeRow); foreach (Dictionary row in valueList[resultSetIndex]) { Row rowResult = Data.RowCreate(typeRow); foreach (string columnName in row.Keys) { object value = row[columnName]; PropertyInfo propertyInfo = propertyInfoList.Where(item => item.Name == columnName).FirstOrDefault(); if (propertyInfo != null) { propertyInfo.SetValue(rowResult, value); } } result.Add(rowResult); } return result; } /// /// Convert sql multiple result set to one typed row list. /// /// Type of row to copy to. Copy one multiple result set. /// Result of method Execute(); multiple result set. /// Sql multiple result set index. /// Type of row to copy to. Copy one multiple result set. internal static List ExecuteReaderMultipleResultCopy(List>> valueList, int resultSetIndex) where T : Row { List result = ExecuteReaderMultipleResultCopy(typeof(T), valueList, resultSetIndex); return result.Cast().ToList(); } /// /// Returns linq to database query. /// public static IQueryable Query(Type typeRow, DatabaseEnum databaseEnum = DatabaseEnum.Database) { switch (databaseEnum) { case DatabaseEnum.Database: return DbContextInternalCreate(typeRow, isQuery: true).Query; case DatabaseEnum.Memory: return DatabaseMemoryInternal.Instance.RowListGet(typeRow).AsQueryable(); case DatabaseEnum.Custom: throw new Exception("Use for example ComponentJson.MyList.AsQueryable(); instead!"); default: throw new Exception("Scope not supported!"); } } /// /// Returns linq to database query. /// public static IQueryable Query(DatabaseEnum databaseEnum = DatabaseEnum.Database) where TRow : Row { return (IQueryable)Query(typeof(TRow), databaseEnum); } /// /// Returns empty query to clear data grid and keep column definition. /// public static IQueryable QueryEmpty() where TRow: Row { return Enumerable.Empty().AsQueryable(); } /// /// Copy data row. Source and dest need not to be of same type. Only cells available on /// both records are copied. /// public static void RowCopy(Row rowSource, Row rowDest, string fieldNameSourcePrefix = null) { var propertyInfoDestList = UtilDalType.TypeRowToPropertyInfoList(rowDest.GetType()); foreach (PropertyInfo propertyInfoDest in propertyInfoDestList) { string fieldNameDest = propertyInfoDest.Name; string fieldNameSource = fieldNameSourcePrefix + fieldNameDest; PropertyInfo propertyInfoSource = rowSource.GetType().GetProperty(fieldNameSource); if (propertyInfoSource != null) { object value = propertyInfoSource.GetValue(rowSource); propertyInfoDest.SetValue(rowDest, value); } } } /// /// Clone data row. /// public static Row RowCopy(Row row) { Row result = (Row)UtilFramework.TypeToObject(row.GetType()); RowCopy(row, result); return result; } /// /// Clone data row. /// public static TRow RowCopy(TRow row) where TRow : Row { return (TRow)RowCopy((Row)row); } /// /// Copy data row. Source and dest need not to be of same type. Only cells available on /// both records are copied. /// public static TRow RowCopy(Row row, string fieldNameSourcePrefix = null) where TRow : Row { TRow result = (TRow)(object)UtilFramework.TypeToObject(typeof(TRow)); RowCopy(row, result, fieldNameSourcePrefix); return result; } /// /// Returns true, if rows are identical. /// internal static bool RowEqual(Row rowA, Row rowB) { UtilFramework.Assert(rowA.GetType() == rowB.GetType()); bool result = true; var propertyInfoList = UtilDalType.TypeRowToPropertyInfoList(rowA.GetType()); foreach (PropertyInfo propertyInfo in propertyInfoList) { object valueA = propertyInfo.GetValue(rowA); object valueB = propertyInfo.GetValue(rowB); if (object.Equals(valueA, valueB) == false) { result = false; break; } } return result; } /// /// Returns new data row. /// public static Row RowCreate(Type typeRow) { return (Row)UtilFramework.TypeToObject(typeRow); } /// /// Execute query and select data from database. /// internal static List QueryExecute(this IQueryable query) { return query.ToDynamicList().Cast().ToList(); } /// /// Execute query and select data from database. /// internal static List QueryExecute(this IQueryable query) where TRow : Row { return query.ToDynamicList().Cast().ToList(); } /// /// Execute query and select data from database. /// public static Task> QueryExecuteAsync(this IQueryable query) { UtilFramework.LogDebug(string.Format("SELECT ({0})", query.ElementType.Name)); return query.ToDynamicListAsync().ContinueWith(list => list.Result.Cast().ToList()); } /// /// Execute query and select data from database. /// public static Task> QueryExecuteAsync(this IQueryable query) where TRow : Row { return ((IQueryable)query).QueryExecuteAsync().ContinueWith(list => list.Result.Cast().ToList()); } internal static IQueryable QueryFilter(IQueryable query, string fieldName, object filterValue, FilterOperator filterOperator) { string predicate = fieldName; switch (filterOperator) { case FilterOperator.Equal: predicate += " = @0"; break; case FilterOperator.Smaller: predicate += " <= @0"; break; case FilterOperator.Greater: predicate += " >= @0"; break; case FilterOperator.Like: predicate += ".Contains(@0)"; break; default: throw new Exception("Enum unknown!"); } if (filterValue is DateTime) { // In order to prevent "Conversion failed when converting date and/or time from character string." exception for sql type "datetime" // dynamic linq where parameter needs to be passed as string and not as DateTime. // // SQL datetime // 2008-09-04 00:00:00.000 // // SQL datetime2 // 1979-02-03T00:00:00.0000000 filterValue = ((DateTime)filterValue).ToString("yyyy-MM-dd HH:mmm:ss.fff"); } return query.Where(predicate, filterValue); } /// /// Sql orderby. /// internal static IOrderedQueryable QueryOrderBy(IQueryable query, string fieldName, bool isSort) { if (isSort == true) { fieldName += " DESC"; } return query.OrderBy(fieldName); } /// /// Sql orderby. /// internal static IOrderedQueryable QueryOrderByThenBy(IOrderedQueryable query, string fieldName, bool isSort) { if (isSort == true) { fieldName += " DESC"; } return query.ThenBy(fieldName); } /// /// Sql paging. /// internal static IQueryable QuerySkipTake(IQueryable query, int skip, int take) { if (skip != 0) { query = query.Skip(skip); } query = query.Take(take); return query; } /// /// Delete data record from database. /// public static async Task Delete(Row row) { DbContext dbContext = DbContextInternalCreate(row.GetType(), isQuery: false); dbContext.Remove(row); int count = await dbContext.SaveChangesAsync(); UtilFramework.Assert(count == 1, "Update failed!"); } /// /// Insert data record. Primary key needs to be 0! Row contains new primary key after insert. /// public static async Task InsertAsync(TRow row, DatabaseEnum databaseEnum = DatabaseEnum.Database) where TRow : Row { UtilFramework.LogDebug(string.Format("INSERT ({0})", row.GetType().Name)); switch (databaseEnum) { case DatabaseEnum.Database: { Row rowCopy = Data.RowCopy(row); DbContext dbContext = DbContextInternalCreate(row.GetType(), isQuery: false); dbContext.Add(row); // Throws NullReferenceException if no primary key is defined. // EF sets auto increment field to 2147482647. try { int count = await dbContext.SaveChangesAsync(); // Override method GridInsertAsync(); for sql view. UtilFramework.Assert(count == 1, "Update failed!"); // // Exception: Database operation expected to affect 1 row(s) but actually affected 0 row(s). // Cause: No autoincrement on Id column or no Id set by application // // Exception: The conversion of a datetime2 data type to a datetime data type resulted in an out-of-range value. // Cause: CSharp not nullable DateTime default value is "{1/1/0001 12:00:00 AM}" change it to nullable or set value for example to DateTime.Now } catch { Data.RowCopy(rowCopy, row); // In case of exception, auto increment id stays -2147482647. Reverse it back. throw; } break; } case DatabaseEnum.Memory: { var rowList = Data.MemoryRowList(row.GetType(), databaseEnum); rowList.Add(row); break; } case DatabaseEnum.Custom: throw new Exception("Override method Grid.Insert(); and set result.IsHandled flag for custom data source!"); default: throw new Exception("Scope not supported!"); } } /// /// Update data record on database. /// public static async Task UpdateAsync(Row rowOld, Row row, DatabaseEnum databaseEnum = DatabaseEnum.Database) { UtilFramework.LogDebug(string.Format("UPDATE ({0})", rowOld.GetType().Name)); UtilFramework.Assert(rowOld.GetType() == row.GetType()); // if (Data.RowEqual(rowOld, row) == false) // See also: EntityState.Modified { switch (databaseEnum) { case DatabaseEnum.Database: { var rowOldLocal = Data.RowCopy(rowOld); // Prevent modifications on SetValues(row); DbContext dbContext = Data.DbContextInternalCreate(rowOld.GetType(), isQuery: false); var tracking = dbContext.Attach(rowOldLocal); tracking.CurrentValues.SetValues(row); if (tracking.State == EntityState.Modified) { // Called by data grid. } if (tracking.State == EntityState.Unchanged) { // Called by method Data.UpdateAsync(); for table. tracking.State = EntityState.Modified; } int count = await dbContext.SaveChangesAsync(); // Override method GridUpdateAsync(); for sql view. UtilFramework.Assert(count == 1, "Update failed!"); break; } case DatabaseEnum.Memory: { var rowList = Data.MemoryRowList(rowOld.GetType(), databaseEnum); PropertyInfo propertyInfo = UtilDalType.TypeRowToPropertyInfoList(rowOld.GetType()).First(); // Assume first field is primary key. object idNew = propertyInfo.GetValue(rowOld); int updateCount = 0; foreach (Row rowMemory in rowList.Cast()) { object id = propertyInfo.GetValue(rowMemory); if (object.Equals(id, idNew)) { Data.RowCopy(row, rowMemory); updateCount += 1; } } if (updateCount == 0) { throw new Exception("Memory row could not be updated!"); } if (updateCount > 1) { throw new Exception("More than one memory row updated!"); } break; } case DatabaseEnum.Custom: throw new Exception("Override method Grid.Update(); and set result.IsHandled flag for custom data source!"); default: throw new Exception("Scope not supported!"); } } } /// /// Update data record on database. /// public static Task UpdateAsync(Row row, DatabaseEnum databaseEnum = DatabaseEnum.Database) { return UpdateAsync(row, row, databaseEnum); } /// /// Parse user entered cell and filter text. Text can be null. /// private static object CellTextParse(Field field, string text) { object result = field.FrameworkType().CellTextParse(text); return result; } /// /// Parse user entered cell text and write it to row. Text can be null. /// internal static void CellTextParse(Field field, string text, Row row, out string errorParse) { errorParse = null; object value = CellTextParse(field, text); bool isNullable = UtilFramework.IsNullable(field.PropertyInfo.PropertyType); // Do not write value to row if type is not nullable but text is null. bool isPrevent = (text == null) && !isNullable; if (!isPrevent) { field.PropertyInfo.SetValue(row, value); } } /// /// Default parse user entered filter text. Text can be null. /// internal static void CellTextParseFilter(Field field, string text, GridFilter filter, out string errorParse) { errorParse = null; object filterValue = CellTextParse(field, text); FilterOperator filterOperator = FilterOperator.Equal; if (field.PropertyInfo.PropertyType == typeof(string)) { filterOperator = FilterOperator.Like; } filter.ValueSet(field.PropertyInfo.Name, filterValue, filterOperator, text, isClear: text == null); } } public enum FilterOperator { None = 0, Equal = 1, Smaller = 2, Greater = 3, Like = 4 } internal class UtilDalUpsert { internal static string UpsertFieldNameToCsvList(string[] fieldNameSqlList, string prefix) { string result = null; bool isFirst = true; foreach (string fieldName in fieldNameSqlList) { if (isFirst) { isFirst = false; } else { result += ", "; } result += prefix + fieldName; } return result; } internal static string UpsertFieldNameToAssignList(string[] fieldNameSqlList, string prefixTarget, string prefixSource) { string result = null; bool isFirst = true; foreach (string fieldName in fieldNameSqlList) { if (isFirst) { isFirst = false; } else { result += ", "; } result += prefixTarget + fieldName + " = " + prefixSource + fieldName; } return result; } private static string UpsertSelect(Type typeRow, List rowList, List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)> paramList) { StringBuilder sqlSelect = new StringBuilder(); var fieldList = UtilDalType.TypeRowToFieldList(typeRow); // Row bool isFirstRow = true; foreach (Row row in rowList) { UtilFramework.Assert(row.GetType() == typeRow); if (isFirstRow) { isFirstRow = false; } else { sqlSelect.Append(" UNION ALL\r\n"); } // Field sqlSelect.Append("(SELECT "); bool isFirstField = true; foreach (var field in fieldList) { if (isFirstField) { isFirstField = false; } else { sqlSelect.Append(", "); } object value = field.PropertyInfo.GetValue(row); string paramName = Data.ExecuteParamAdd(field.FrameworkTypeEnum, value, paramList); sqlSelect.Append(string.Format("{0} AS {1}", paramName, field.FieldNameSql)); } sqlSelect.Append(")"); } return sqlSelect.ToString(); } internal static async Task UpsertAsync(Type typeRow, List rowList, string[] fieldNameKeyList) { string tableNameWithSchemaSql = UtilDalType.TypeRowToTableNameWithSchemaSql(typeRow); bool isFrameworkDb = UtilDalType.TypeRowIsFrameworkDb(typeRow); var fieldNameSqlList = UtilDalType.TypeRowToFieldList(typeRow).Where(item => item.IsPrimaryKey == false).Select(item => item.FieldNameSql).ToArray(); string fieldNameKeySourceList = UpsertFieldNameToCsvList(fieldNameKeyList, "Source."); string fieldNameKeyTargetList = UpsertFieldNameToCsvList(fieldNameKeyList, "Target."); string fieldNameAssignList = UpsertFieldNameToAssignList(fieldNameSqlList, "Target.", "Source."); string fieldNameInsertList = UpsertFieldNameToCsvList(fieldNameSqlList, null); string fieldNameValueList = UpsertFieldNameToCsvList(fieldNameSqlList, "Source."); var paramList = new List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)>(); string sqlSelect = UpsertSelect(typeRow, rowList, paramList); // string sqlDebug = Data.ExecuteParamDebug(sqlSelect, paramList); // var resultDebug = await Data.ExecuteReaderAsync(typeRow, sqlDebug); string sqlUpsert = @" MERGE INTO {0} AS Target USING ({1}) AS Source ON NOT EXISTS( SELECT {2} EXCEPT SELECT {3}) WHEN MATCHED THEN UPDATE SET {4} WHEN NOT MATCHED BY TARGET THEN INSERT ({5}) VALUES ({6}); "; sqlUpsert = string.Format(sqlUpsert, tableNameWithSchemaSql, sqlSelect, fieldNameKeySourceList, fieldNameKeyTargetList, fieldNameAssignList, fieldNameInsertList, fieldNameValueList); // string sqlDebug = Data.ExecuteParamDebug(sqlUpsert, paramList); // Upsert await Data.ExecuteNonQueryAsync(sqlUpsert, paramList, isFrameworkDb); } internal static async Task UpsertAsync(List rowList, string[] fieldNameKeyList) where TRow : Row { await UpsertAsync(typeof(TRow), rowList.Cast().ToList(), fieldNameKeyList); } internal static async Task UpsertAsync(List rowList, string fieldNameKey) where TRow : Row { await UpsertAsync(rowList, new string[] { fieldNameKey }); } /// /// Set IsDelete flag to false on sql table. /// internal static async Task UpsertIsDeleteAsync(Type typeRow) { string fieldNameSqlIsDelete = "IsDelete"; string tableNameWithSchemaSql = UtilDalType.TypeRowToTableNameWithSchemaSql(typeRow); bool isFrameworkDb = UtilDalType.TypeRowIsFrameworkDb(typeRow); // IsDeletes string sqlIsDelete = string.Format("UPDATE {0} SET {1}=CAST(0 AS BIT)", tableNameWithSchemaSql, fieldNameSqlIsDelete); await Data.ExecuteNonQueryAsync(sqlIsDelete, null, isFrameworkDb); } /// /// Overload. /// internal static async Task UpsertIsDeleteAsync() where TRow : Row { await UpsertIsDeleteAsync(typeof(TRow)); } } internal class UtilDalUpsertIntegrate { internal class FieldIntegrate { /// /// Gets or sets Field. See also method UtilDalType.TypeRowToFieldList(); /// public Field Field; /// /// Gets or sets IsKey. True, if "Id" or "IdName". /// public bool IsKey; /// /// Gets or sets IsIdName. True, if for example "TableIdName". /// public bool IsIdName; /// /// Gets or sets "IsId". True, if for example "TableId". /// public bool IsId; /// /// Gets or sets FieldNameIdSql. For example "TableId". /// public string FieldNameIdSql; /// /// Gets or sets TypeRowReference. Referenced table (or view) containing field "Id" and "Name". For example view "FrameworkTableIntegrate". /// public Type TypeRowReference; } /// /// Returns Integrate reference table. /// /// Integrate table. private static Type TypeRowReferenceIntegrate(Type typeRow, string fieldNameIdSql, List referenceList) { var result = referenceList.SingleOrDefault(item => item.TypeRowIntegrate == typeRow && item.FieldNameIdSql == fieldNameIdSql)?.TypeRowReferenceIntegrate; return result; } /// /// Defines a reference table. /// internal class Reference { public Reference(Type typeRow, string fieldNameIdCSharp, string fieldNameIdSql, Type typeRowIntegrate, string fieldNameIdNameCSharp, string fieldNameIdNameSql, Type typeRowReference, Type typeRowReferenceIntegrate) { TypeRow = typeRow; FieldNameIdCSharp = fieldNameIdCSharp; FieldNameIdSql = fieldNameIdSql; TypeRowIntegrate = typeRowIntegrate; FieldNameIdNameCSharp = fieldNameIdNameCSharp; FieldNameIdNameSql = fieldNameIdNameSql; TypeRowReference = typeRowReference; TypeRowReferenceIntegrate = typeRowReferenceIntegrate; } /// /// Gets TypeRow. For example: "LoginUserRole" /// public readonly Type TypeRow; /// /// Gets FieldNameIdCSharp. For example: "UserId" /// public readonly string FieldNameIdCSharp; public readonly string FieldNameIdSql; /// /// Gets TypeRowIntegrate. For example: "LoginUserRoleIntegrate". /// public readonly Type TypeRowIntegrate; /// /// Gets FieldNameIdNameCSharp. For example "UserIdName". /// public readonly string FieldNameIdNameCSharp; public readonly string FieldNameIdNameSql; /// /// Gets TypeRowReference. For example: "LoginUser" /// public readonly Type TypeRowReference; /// /// Gets TypeRowReferenceIntegrate. For example: "LoginUserIntegrate" /// public readonly Type TypeRowReferenceIntegrate; } /// /// Returns list of FieldIntegrate for TypeRow. /// /// Data row type. internal static List FieldIntegrateList(Type typeRow, List referenceList) { List result = new List(); var fieldList = UtilDalType.TypeRowToFieldList(typeRow); var fieldNameSqlList = fieldList.Select(item => item.FieldNameSql).ToList(); // Populate result foreach (var field in fieldList) { FieldIntegrate fieldIntegrate = new FieldIntegrate { Field = field }; result.Add(fieldIntegrate); } foreach (var fieldIntegrate in result) { string fieldNameSql = fieldIntegrate.Field.FieldNameSql; fieldIntegrate.IsKey = fieldNameSql == "Id" || fieldNameSql == "IdName"; string lastChar = ""; // Character before "IdName". if (fieldNameSql.Length > "IdName".Length) { lastChar = fieldNameSql.Substring(fieldNameSql.Length - "IdName".Length - 1, 1); } bool lastCharIsLower = lastChar == lastChar.ToLower() && lastChar.Length == 1; if (fieldNameSql.EndsWith("IdName") && lastCharIsLower) // Integrate naming convention. { string fieldNameIdSql = fieldNameSql.Substring(0, fieldNameSql.Length - "Name".Length); // Integrate naming convention. if (fieldNameSqlList.Contains(fieldNameIdSql)) { UtilDalType.TypeRowToTableNameSql(typeRow, out string schemaNameSql, out string tableNameSql); // Find reference table Type typeRowReference = TypeRowReferenceIntegrate(typeRow, fieldNameIdSql, referenceList); if (typeRowReference != null) { List propertyNameList = UtilDalType.TypeRowToPropertyInfoList(typeRowReference).Select(item => item.Name).ToList(); if (propertyNameList.Contains("Id") && propertyNameList.Contains("IdName")) // Integrate naming convention. { // IdName fieldIntegrate.IsIdName = true; fieldIntegrate.TypeRowReference = typeRowReference; fieldIntegrate.FieldNameIdSql = fieldNameIdSql; // Id var fieldIntegrateId = result.Where(item => item.Field.FieldNameSql == fieldNameIdSql).Single(); fieldIntegrateId.IsId = true; fieldIntegrateId.TypeRowReference = typeRowReference; fieldIntegrateId.FieldNameIdSql = fieldNameIdSql; } } } } } return result; } private static string UpsertSelect(Type typeRow, List rowList, List referenceList, List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)> paramList) { StringBuilder sqlSelect = new StringBuilder(); var fieldIntegrateList = FieldIntegrateList(typeRow, referenceList); // Row bool isFirstRow = true; foreach (Row row in rowList) { UtilFramework.Assert(row.GetType() == typeRow); if (isFirstRow) { isFirstRow = false; } else { sqlSelect.Append(" UNION ALL\r\n"); } // Field sqlSelect.Append("(SELECT "); bool isFirstField = true; foreach (var fieldIntegrate in fieldIntegrateList) { bool isField = (fieldIntegrate.IsId == false && fieldIntegrate.IsIdName == false && fieldIntegrate.IsKey == false) || fieldIntegrate.IsIdName; if (isField) { if (isFirstField) { isFirstField = false; } else { sqlSelect.Append(", "); } string fieldNameSql = fieldIntegrate.Field.FieldNameSql; object value = fieldIntegrate.Field.PropertyInfo.GetValue(row); string paramName = Data.ExecuteParamAdd(fieldIntegrate.Field.FrameworkTypeEnum, value, paramList); if (fieldIntegrate.IsId == false && fieldIntegrate.IsIdName == false) { sqlSelect.Append(string.Format("{0} AS {1}", paramName, fieldIntegrate.Field.FieldNameSql)); } else { if (fieldIntegrate.IsIdName) { string tableNameSql = UtilDalType.TypeRowToTableNameCSharp(fieldIntegrate.TypeRowReference); string sqlIntegrate = string.Format("(SELECT Integrate.Id FROM {0} Integrate WHERE Integrate.IdName = {1}) AS {2}", tableNameSql, paramName, fieldIntegrate.FieldNameIdSql); sqlSelect.Append(sqlIntegrate); } } } } sqlSelect.Append(")"); } return sqlSelect.ToString(); } /// /// Set IsDelete property to true on row and to false on sql table. /// private static void IsDeleteSet(Type typeRow, List rowList) { var fieldList = UtilDalType.TypeRowToFieldListDictionary(typeRow); if (fieldList.TryGetValue("IsDelete", out Field field)) { foreach (var row in rowList) { field.PropertyInfo.SetValue(row, false); } // Set sql table IsDelete to true where IsIntegrate is true (if column exists) UtilDalUpsertIntegrate.UpsertIsDeleteAsync(typeRow).Wait(); } } /// /// Sql merge into for Integrate. /// /// Type of rowList (can be empty). /// Type underlying sql table. /// Records to update. /// Key fields for record identification. /// Assemblies in which to search reference tables. private static async Task UpsertAsync(Type typeRow, Type typeRowDest, List rowList, string[] fieldNameSqlKeyList, List referenceList, List assemblyList) { bool isFrameworkDb = UtilDalType.TypeRowIsFrameworkDb(typeRow); var fieldNameSqlListAll = FieldIntegrateList(typeRow, referenceList); foreach (var rowListSplit in UtilFramework.Split(rowList, 100)) // Prevent error: "The server supports a maximum of 2100 parameters" { var paramList = new List<(FrameworkTypeEnum FrameworkTypeEnum, SqlParameter SqlParameter)>(); string sqlSelect = UpsertSelect(typeRow, rowListSplit, referenceList, paramList); // Update underlying sql table if sql view ends with "Integrate". UtilDalType.TypeRowToTableNameSql(typeRowDest, out string schemaNameSql, out string tableNameSql); string tableNameWithSchemaSql = UtilDalType.TableNameWithSchemaSql(schemaNameSql, tableNameSql); var fieldDestList = UtilDalType.TypeRowToFieldListDictionary(typeRowDest); var fieldNameSqlList = fieldNameSqlListAll .Where(item => item.IsIdName == false && item.Field.IsPrimaryKey == false && item.IsKey == false && fieldDestList.ContainsKey(item.Field.FieldNameCSharp)) .Select(item => item.Field.FieldNameSql).ToArray(); string fieldNameKeySourceList = UtilDalUpsert.UpsertFieldNameToCsvList(fieldNameSqlKeyList, "Source."); string fieldNameKeyTargetList = UtilDalUpsert.UpsertFieldNameToCsvList(fieldNameSqlKeyList, "Target."); string fieldNameAssignList = UtilDalUpsert.UpsertFieldNameToAssignList(fieldNameSqlList, "Target.", "Source."); string fieldNameInsertList = UtilDalUpsert.UpsertFieldNameToCsvList(fieldNameSqlList, null); string fieldNameValueList = UtilDalUpsert.UpsertFieldNameToCsvList(fieldNameSqlList, "Source."); string sqlUpsert = @" MERGE INTO {0} AS Target USING -- Source (sqlSelect) ({1}) AS Source ON NOT EXISTS( SELECT {2} EXCEPT SELECT {3}) WHEN MATCHED THEN UPDATE SET {4} WHEN NOT MATCHED BY TARGET THEN INSERT ({5}) VALUES ({6}); "; sqlUpsert = string.Format(sqlUpsert, tableNameWithSchemaSql, sqlSelect, fieldNameKeySourceList, fieldNameKeyTargetList, fieldNameAssignList, fieldNameInsertList, fieldNameValueList); // string sqlDebug = Data.ExecuteParamDebug(sqlUpsert, paramList); // Upsert await Data.ExecuteNonQueryAsync(sqlUpsert, paramList, isFrameworkDb); // See also method AppCli.CommandGenerateIntegrate(); } } /// /// List of rows to insert or update. /// internal class UpsertItem { private UpsertItem(Type typeRow, List rowList, string[] fieldNameSqlKeyList, List referenceList) { this.TypeRow = typeRow; this.RowList = rowList; this.FieldNameSqlKeyList = fieldNameSqlKeyList; this.ReferenceList = referenceList; foreach (var item in RowList) { UtilFramework.Assert(item.GetType() == TypeRow); } } public static UpsertItem Create(List rowList, string[] fieldNameSqlKeyList, List referenceList) { return new UpsertItem(typeof(TRow), rowList.Cast().ToList(), fieldNameSqlKeyList, referenceList); } /// /// Gets TypeRow. For example: "[dbo].[LoginUser]" or "[dbo].[LoginUserIntegrate]". /// public readonly Type TypeRow; /// /// Returns underlying sql table if sql view ends with "Integrate". For example "[dbo].[LoginUser]". /// public Type TypeRowDest(List assemblyList) { UtilDalType.TypeRowToTableNameSql(TypeRow, out string schemaNameSql, out string tableNameSql); if (tableNameSql.EndsWith("Integrate")) { tableNameSql = tableNameSql.Substring(0, tableNameSql.Length - "Integrate".Length); } Type typeRowDest = UtilDalType.TypeRowFromTableNameSql(schemaNameSql, tableNameSql, assemblyList); return typeRowDest; } /// /// Gets RowList. Rows to insert or update. /// public readonly List RowList; /// /// Gets FieldNameSqlKeyList. Sql unique index for upsert to identify record. For example (UserId, RoleId). /// public readonly string[] FieldNameSqlKeyList; public readonly List ReferenceList; /// /// Gets IsDeploy. True, if RowList is deployed to database. /// public bool IsDeploy { get; internal set; } } /// /// Sql merge into for Integrate. /// /// List of rows to insert or update. /// Assemblies in which to search reference tables. internal static async Task UpsertAsync(List upsertList, List assemblyList, Action progressBar = null) { upsertList = upsertList.Where(item => item.IsDeploy == false).ToList(); // Group by TypeRow Dictionary> typeRowToRowList = new Dictionary>(); foreach (var item in upsertList) { if (!typeRowToRowList.ContainsKey(item.TypeRow)) { typeRowToRowList[item.TypeRow] = new List(); } typeRowToRowList[item.TypeRow].AddRange(item.RowList); } foreach (var item in typeRowToRowList) { Type typeRow = item.Key; List rowList = item.Value; // IsDeleteSet IsDeleteSet(typeRow, rowList); // One call for hierarchical Integrate which needs multiple upsert. // Upsert foreach (var itemUpsert in upsertList.Where(item => item.TypeRow == typeRow)) { Type typeRowDest = itemUpsert.TypeRowDest(assemblyList); progressBar?.Invoke(typeRowDest); await UpsertAsync(itemUpsert.TypeRow, typeRowDest, itemUpsert.RowList, itemUpsert.FieldNameSqlKeyList, itemUpsert.ReferenceList, assemblyList); } } // Set IsDeploy foreach (var item in upsertList) { item.IsDeploy = true; } } /// /// Overload. /// internal static Task UpsertAsync(UpsertItem upsertList, List assemblyList) { return UpsertAsync(new List(new UpsertItem[] { upsertList }), assemblyList); } /// /// Set IsDelete flag to false on sql table. If sql table contains IsIntegrate column set IsDelete flag to false only on IsIntegrate data rows. /// private static async Task UpsertIsDeleteAsync(Type typeRow) { var fieldList = UtilDalType.TypeRowToFieldListDictionary(typeRow); if (!fieldList.ContainsKey("IsIntegrate")) { await UtilDalUpsert.UpsertIsDeleteAsync(typeRow); } else { string fieldNameSqlIsDelete = "IsDelete"; string tableNameWithSchemaSql = UtilDalType.TypeRowToTableNameWithSchemaSql(typeRow); bool isFrameworkDb = UtilDalType.TypeRowIsFrameworkDb(typeRow); // IsDeletes string sqlIsDelete = string.Format("UPDATE {0} SET {1}=CAST(1 AS BIT) WHERE IsIntegrate = 1", tableNameWithSchemaSql, fieldNameSqlIsDelete); await Data.ExecuteNonQueryAsync(sqlIsDelete, null, isFrameworkDb); } } } /// /// Framework type system. /// public enum FrameworkTypeEnum { None = 0, Int = 1, Smallint = 2, Tinyint = 3, Bigint = 4, Uniqueidentifier = 5, Datetime = 6, Datetime2 = 7, Date = 8, Char = 9, NChar = 23, Nvarcahr = 10, Varchar = 11, Text = 12, Ntext = 13, Bit = 14, Money = 15, Smallmoney = 24, Decimal = 16, Real = 17, Float = 18, Varbinary = 19, Sqlvariant = 20, Image = 21, Numeric = 22, // 24 } internal static class UtilDalType { /// /// Returns true if typeRow is declared if Framework assembly. /// internal static bool TypeRowIsFrameworkDb(Type typeRow) { return typeRow.Assembly == UtilFramework.AssemblyFramework; // Type is declared in Framework assembly. } /// /// Returns TybleNameCSharp declared in assembly. /// /// (TypeRow, TybleNameCSharp) internal static Dictionary TableNameCSharpList(params Assembly[] assemblyList) { var result = new Dictionary(); foreach (Assembly assembly in assemblyList) { if (assembly != null) { foreach (Type type in assembly.GetTypes()) { if (type.IsSubclassOf(typeof(Row))) // TypeRow { Type typeRow = type; string tableNameCSharp = TypeRowToTableNameCSharp(typeRow); result.Add(typeRow, tableNameCSharp); } } } } return result; } /// /// Returns true if typeRow has database table. /// internal static bool TypeRowIsTableNameSql(Type typeRow) { bool result = false; SqlTableAttribute tableAttribute = (SqlTableAttribute)typeRow.GetCustomAttribute(typeof(SqlTableAttribute)); if (tableAttribute != null && (tableAttribute.SchemaNameSql != null || tableAttribute.TableNameSql != null)) { result = true; } return result; } /// /// Returns rows defined in "Database" namespace in assemblies. /// /// Use method AppCli.AssemblyList(); when running in cli mode or method UtilServer.AssemblyList(); when running in web mode. internal static List TypeRowList(List assemblyList) { Dictionary result = new Dictionary(); foreach (Assembly assembly in assemblyList) { if (assembly != null) { foreach (Type type in assembly.GetTypes()) { if (type.IsSubclassOf(typeof(Row))) { string name = UtilFramework.TypeToName(type); if (name.StartsWith("Database.")) { string tableNameCSharp = UtilDalType.TypeRowToTableNameCSharp(type); if (result.ContainsKey(tableNameCSharp)) { throw new Exception(string.Format("TableNameCSharp exists already in different assembly! ({0})", tableNameCSharp)); } result.Add(tableNameCSharp, type); } else { throw new Exception(string.Format("Row class not defined in Database namespace! ({0})", UtilFramework.TypeToName(type))); } } } } } return result.Values.ToList(); } /// /// Returns row type as string. For example: "dbo.User". Omits "Database" namespace prefix. /// internal static string TypeRowToTableNameCSharp(Type typeRow) { UtilFramework.Assert(UtilFramework.IsSubclassOf(typeRow, typeof(Row)), "Wrong type!"); string result = UtilFramework.TypeToName(typeRow); UtilFramework.Assert(result.StartsWith("Database."), string.Format("Move calculated row to different namespace! For example Database.Calculated. ({0})", UtilFramework.TypeToName(typeRow))); // If it is a calculated row which does not exist on database move it for example to namespace "Database.Calculated". UtilFramework.Assert(typeRow.BaseType == typeof(Row), string.Format("Calculated row has to derive from type Row! ({0})", UtilFramework.TypeToName(typeRow))); result = result.Substring("Database.".Length); // Remove "DatabaseFramework" namespace. return result; } /// /// Returns row type as string. For example: "User". Omits "Database" namespace prefix and schema. /// internal static string TypeRowToTableNameCSharpWithoutSchema(Type typeRow) { string result = TypeRowToTableNameCSharp(typeRow); int index = result.IndexOf("."); if (index != -1) { result = result.Substring(index + ".".Length); } return result; } /// /// Returns SchemaNameCSharp. For example: "dbo". /// internal static string TypeRowToSchemaNameCSharp(Type typeRow) { string result = TypeRowToTableNameCSharp(typeRow); int index = result.IndexOf("."); if (index == -1) { result = null; // Sql table without schema } else { result = result.Substring(0, index); } return result; } /// /// Returns (TypeRow, TableNameWithSchemaSql) list. /// internal static Dictionary TableNameSqlList(List assemblyList) { Dictionary result = new Dictionary(); List typeRowList = TypeRowList(assemblyList); foreach (Type typeRow in typeRowList) { string tableNameWithSchemaSql = TypeRowToTableNameWithSchemaSql(typeRow); result.Add(typeRow, tableNameWithSchemaSql); } return result; } /// /// Returns (TypeRow, TableNameWithSchemaSql) list. /// internal static Dictionary TableNameWithSchemaSqlList(List assemblyList) { var result = new Dictionary(); List typeRowList = TypeRowList(assemblyList); foreach (Type typeRow in typeRowList) { TypeRowToTableNameSql(typeRow, out string schemaNameSql, out string tableNameSql); result.Add(typeRow, (schemaNameSql, tableNameSql)); } return result; } /// /// Returns true, if typeRow contains sql information. /// internal static bool TypeRowToTableNameSql(Type typeRow, out string schemaNameSql, out string tableNameSql) { SqlTableAttribute tableAttribute = (SqlTableAttribute)typeRow.GetCustomAttribute(typeof(SqlTableAttribute)); schemaNameSql = tableAttribute?.SchemaNameSql; tableNameSql = tableAttribute?.TableNameSql; return tableAttribute != null; } /// /// Returns TypeRow. /// /// Assemblies to scan for TypeRow. internal static Type TypeRowFromTableNameSql(string schemaNameSql, string tableNameSql, List assemblyList) { List result = new List(); foreach (Assembly assembly in assemblyList) { if (assembly != null) { foreach (Type type in assembly.GetTypes()) { if (UtilFramework.IsSubclassOf(type, typeof(Row))) // TypeRow { if (TypeRowToTableNameSql(type, out string schemaNameSqlLocal, out string tableNameSqlLocal)) { if (schemaNameSqlLocal == schemaNameSql && tableNameSqlLocal == tableNameSql) { result.Add(type); } } } } } } return result.Single(); } /// /// Returns (TypeRow, TableNameCSharp) from TableNameCSharp if declared in assembly. /// /// For example: "dbo.FrameworkScript" /// Assemblies in which to search for TypeRow. internal static Dictionary TypeRowFromTableNameCSharpList(List tableNameCSharpList, List assemblyList) { var result = new Dictionary(); tableNameCSharpList = tableNameCSharpList.Distinct().ToList(); foreach (Assembly assembly in assemblyList) { foreach (Type type in assembly.GetTypes()) { if (UtilFramework.IsSubclassOf(type, typeof(Row))) { string tableNameCSharp = UtilDalType.TypeRowToTableNameCSharp(type); if (tableNameCSharpList.Contains(tableNameCSharp)) { result.Add(type, tableNameCSharp); } } } } return result; } /// /// Returns FieldNameCSharp declared in assembly. /// /// (TableNameCSharp, FieldNameCSharp) internal static List<(string TableNameCSharp, string FieldNameCSharp)> FieldNameCSharpList(params Assembly[] assemblyList) { var result = new List<(string TableNameCSharp, string FieldNameCSharp)>(); foreach (var item in TableNameCSharpList(assemblyList)) { Type typeRow = item.Key; string tableNameCSharp = item.Value; var fieldList = UtilDalType.TypeRowToFieldList(typeRow); foreach (var field in fieldList) { result.Add((tableNameCSharp, field.FieldNameCSharp)); } } return result; } /// /// Returns sql table name with schema name. /// internal static string TableNameWithSchemaSql(string schemaNameSql, string tableNameSql) { string result = string.Format("[{0}].[{1}]", schemaNameSql, tableNameSql); return result; } /// /// See also method TypeRowIsTableNameSql(); /// internal static string TypeRowToTableNameWithSchemaSql(Type typeRow) { TypeRowToTableNameSql(typeRow, out string schemaNameSql, out string tableNameSql); string result = TableNameWithSchemaSql(schemaNameSql, tableNameSql); return result; } /// /// See also method TypeRowToFieldList(); /// internal static PropertyInfo[] TypeRowToPropertyInfoList(Type typeRow) { if (typeRow == null) { return new PropertyInfo[] { }; } else { return typeRow.GetProperties(BindingFlags.Public | BindingFlags.Instance); } } internal class Field { public Field(PropertyInfo propertyInfo, int sort, string fieldNameSql, bool isPrimaryKey, FrameworkTypeEnum frameworkTypeEnum) { this.PropertyInfo = propertyInfo; this.Sort = sort; this.FieldNameSql = fieldNameSql; this.IsPrimaryKey = isPrimaryKey; this.FrameworkTypeEnum = frameworkTypeEnum; } public readonly PropertyInfo PropertyInfo; public string FieldNameCSharp { get { return PropertyInfo.Name; } } /// /// Gets Sort. (FieldNameCSharpSort). /// public readonly int Sort; public readonly string FieldNameSql; public readonly bool IsPrimaryKey; public readonly FrameworkTypeEnum FrameworkTypeEnum; public FrameworkType FrameworkType() { if (FrameworkTypeEnum == FrameworkTypeEnum.None) { return UtilDalType.FrameworkTypeFromValueType(PropertyInfo.PropertyType); } return UtilDalType.FrameworkTypeFromEnum(FrameworkTypeEnum); } } /// /// Returns CSharp fields. Sequence (FieldNameCSharpSort) is identical to CSharp code typeRow property declarations. /// internal static List TypeRowToFieldList(Type typeRow) { var result = new List(); var propertyInfoList = TypeRowToPropertyInfoList(typeRow); int sort = 1; foreach (PropertyInfo propertyInfo in propertyInfoList) { SqlFieldAttribute fieldAttribute = (SqlFieldAttribute)propertyInfo.GetCustomAttribute(typeof(SqlFieldAttribute)); string fieldNameSql = null; FrameworkTypeEnum frameworkTypeEnum = FrameworkTypeEnum.None; bool isPrimaryKey = false; if (fieldAttribute != null) { fieldNameSql = fieldAttribute.FieldNameSql; frameworkTypeEnum = fieldAttribute.FrameworkTypeEnum; isPrimaryKey = fieldAttribute.IsPrimaryKey; } result.Add(new Field(propertyInfo, sort, fieldNameSql, isPrimaryKey, frameworkTypeEnum)); sort += 1; } return result; } /// /// Returns CSharp field list as Dictionary. /// /// (FieldNameCSharp, Field) internal static Dictionary TypeRowToFieldListDictionary(Type typeRow) { var fieldList = TypeRowToFieldList(typeRow); var result = new Dictionary(); foreach (Field field in fieldList) { result.Add(field.PropertyInfo.Name, field); } return result; } public static Type SqlTypeToType(int sqlType) { Type type = FrameworkTypeList().Where(item => item.Value.SqlType == sqlType).Single().Value.ValueType; return type; } public static FrameworkTypeEnum SqlTypeToFrameworkTypeEnum(int sqlType) { var result = FrameworkTypeList().Where(item => item.Value.SqlType == sqlType).SingleOrDefault().Value?.FrameworkTypeEnum; if (result == null) { return FrameworkTypeEnum.None; } else { return (FrameworkTypeEnum)result; } } public static FrameworkType FrameworkTypeFromEnum(FrameworkTypeEnum frameworkTypeEnum) { UtilFramework.Assert(frameworkTypeEnum != FrameworkTypeEnum.None, "FrameworkTypeEnum not defined!"); return FrameworkTypeList().Where(item => item.Value.FrameworkTypeEnum == frameworkTypeEnum).Single().Value; } public static FrameworkType FrameworkTypeFromValueType(Type valueType) { valueType = UtilFramework.TypeUnderlying(valueType); // int? to int. if (valueType == typeof(int)) { return FrameworkTypeFromEnum(FrameworkTypeEnum.Int); } if (valueType == typeof(Guid)) { return FrameworkTypeFromEnum(FrameworkTypeEnum.Uniqueidentifier); } if (valueType == typeof(DateTime)) { return FrameworkTypeFromEnum(FrameworkTypeEnum.Datetime); } if (valueType == typeof(char)) { return FrameworkTypeFromEnum(FrameworkTypeEnum.Char); } if (valueType == typeof(string)) { return FrameworkTypeFromEnum(FrameworkTypeEnum.Nvarcahr); } if (valueType == typeof(bool)) { return FrameworkTypeFromEnum(FrameworkTypeEnum.Bit); } if (valueType == typeof(decimal)) { return FrameworkTypeFromEnum(FrameworkTypeEnum.Decimal); } if (valueType == typeof(float)) { return FrameworkTypeFromEnum(FrameworkTypeEnum.Float); } throw new Exception("Type not found!"); } [ThreadStatic] private static Dictionary frameworkTypeList; private static Dictionary FrameworkTypeList() { // See also: https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/sql-server-data-type-mappings // See also: SELECT * FROM sys.types // See also: https://docs.microsoft.com/en-us/sql/t-sql/data-types/data-type-conversion-database-engine if (frameworkTypeList == null) { Dictionary result = new Dictionary(); FrameworkType frameworkType; frameworkType = new FrameworkTypeInt(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeSmallint(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeTinyint(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeBigint(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeUniqueidentifier(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeDatetime(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeDatetime2(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeDate(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeChar(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeNChar(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeNvarcahr(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeVarchar(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeText(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeNtext(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeBit(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeMoney(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeSmallmoney(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeDecimal(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeReal(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeFloat(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeVarbinary(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new SqlTypeSqlvariant(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeImage(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkType = new FrameworkTypeNumeric(); result.Add(frameworkType.FrameworkTypeEnum, frameworkType); frameworkTypeList = result; } return frameworkTypeList; } } internal class FrameworkType { public FrameworkType(FrameworkTypeEnum frameworkTypeEnum, string sqlTypeName, int sqlType, Type valueType, DbType dbType, bool isNumber) { this.FrameworkTypeEnum = frameworkTypeEnum; this.SqlTypeName = sqlTypeName; this.SqlType = sqlType; this.ValueType = valueType; this.DbType = dbType; this.IsNumber = isNumber; } public readonly FrameworkTypeEnum FrameworkTypeEnum; /// /// Gets or sets SqlTypeName. For example: "int", "datetime", "datetime". /// public readonly string SqlTypeName; public readonly int SqlType; public readonly Type ValueType; public readonly DbType DbType; public readonly bool IsNumber; /// /// Convert database value to front end cell text for bool. /// private void CellTextFromValueBool(object value, ref string result) { if (value is bool valueBool) { if (valueBool) { result = "Yes"; } else { result = "No"; } } } /// /// Convert database value to front end cell text. /// /// Value is never null when this method is called. /// Returns text to display in cell. protected virtual internal string CellTextFromValue(object value) { string result = value.ToString(); CellTextFromValueBool(value, ref result); return result; } /// /// Parse user entered text to database value for bool. Text is never null. /// private object CellTextParseBool(string text) { object result = null; string textUpper = text?.ToUpper(); if (textUpper.StartsWith("Y") == true) { result = true; } if (textUpper.StartsWith("N") == true) { result = false; } if (result == null) { throw new Exception(string.Format("Text was not recognized as a valid yes, no value! ({0})", text)); } return result; } /// /// Parse user entered text to database value. Text can be null. /// protected virtual internal object CellTextParse(string text) { object result = null; if (text != null) { Type type = UtilFramework.TypeUnderlying(ValueType); if (type == typeof(bool)) { result = CellTextParseBool(text); } else { result = Convert.ChangeType(text, type, CultureInfo.InvariantCulture); } } return result; } protected virtual internal string ValueToSqlParameterDebug(object value) { string result = null; if (value == DBNull.Value) { value = null; } if (value != null) { result = value.ToString(); } if (IsNumber == false) { result = "'" + result + "'"; } if (value == null) { result = "NULL"; } return result; } /// /// Convert value to CSharp code. Value can be null. /// protected virtual internal string ValueToCSharp(object value) { string result = null; if (value != null) { result = value.ToString(); } if (IsNumber == false) { result = "\"" + result + "\""; } if (value == null) { result = "null"; } return result; } } internal class FrameworkTypeInt : FrameworkType { public FrameworkTypeInt() : base(FrameworkTypeEnum.Int, "int", 56, typeof(Int32), DbType.Int32, true) { } } internal class FrameworkTypeSmallint : FrameworkType { public FrameworkTypeSmallint() : base(FrameworkTypeEnum.Smallint, "smallint", 52, typeof(Int16), DbType.Int16, true) { } } internal class FrameworkTypeTinyint : FrameworkType { public FrameworkTypeTinyint() : base(FrameworkTypeEnum.Tinyint, "tinyint", 48, typeof(byte), DbType.Byte, true) { } } internal class FrameworkTypeBigint : FrameworkType { public FrameworkTypeBigint() : base(FrameworkTypeEnum.Bigint, "bigint", 127, typeof(Int64), DbType.Int64, true) { } } internal class FrameworkTypeUniqueidentifier : FrameworkType { public FrameworkTypeUniqueidentifier() : base(FrameworkTypeEnum.Uniqueidentifier, "uniqueidentifier", 36, typeof(Guid), DbType.Guid, false) { } protected internal override string ValueToCSharp(object value) { string result = "null"; if (value is Guid guid) { result = $"Guid.Parse(\"{guid.ToString(null, CultureInfo.InvariantCulture)}\")"; } return result; } } internal class FrameworkTypeDatetime : FrameworkType { public FrameworkTypeDatetime() : base(FrameworkTypeEnum.Datetime, "datetime", 61, typeof(DateTime), DbType.DateTime, false) { } public static string CellTextFromValue(DateTime value, bool isTime = true) { string result = null; result = UtilFramework.DateTimeToText(value, isTime); return result; } public static DateTime? CellTextParse(string text, bool isTime = true) { return UtilFramework.DateTimeFromText(text, isTime); } public static string ValueToCSharpUtil(object value) { var result = "null"; if (value != null) { DateTime dateTime = (DateTime)value; string dateTimeString = UtilFramework.DateTimeToText(dateTime); result = $"DateTime.Parse(\"{dateTimeString}\", CultureInfo.InvariantCulture)"; } return result; } protected internal override string CellTextFromValue(object value) { return FrameworkTypeDatetime.CellTextFromValue((DateTime)value); } protected internal override object CellTextParse(string text) { return FrameworkTypeDatetime.CellTextParse(text); } protected internal override string ValueToCSharp(object value) { return FrameworkTypeDatetime.ValueToCSharpUtil(value); } } internal class FrameworkTypeDatetime2 : FrameworkType { public FrameworkTypeDatetime2() : base(FrameworkTypeEnum.Datetime2, "datetime2", 42, typeof(DateTime), DbType.DateTime2, false) { } protected internal override string CellTextFromValue(object value) { return FrameworkTypeDatetime.CellTextFromValue((DateTime)value); } protected internal override object CellTextParse(string text) { return FrameworkTypeDatetime.CellTextParse(text); } protected internal override string ValueToCSharp(object value) { return FrameworkTypeDatetime.ValueToCSharpUtil(value); } } internal class FrameworkTypeDate : FrameworkType { public FrameworkTypeDate() : base(FrameworkTypeEnum.Date, "date", 40, typeof(DateTime), DbType.Date, false) { } protected internal override string CellTextFromValue(object value) { return FrameworkTypeDatetime.CellTextFromValue((DateTime)value, isTime: false); } protected internal override object CellTextParse(string text) { return FrameworkTypeDatetime.CellTextParse(text, isTime: false); } protected internal override string ValueToCSharp(object value) { return FrameworkTypeDatetime.ValueToCSharpUtil(value); } } internal class FrameworkTypeChar : FrameworkType { public FrameworkTypeChar() : base(FrameworkTypeEnum.Char, "char", 175, typeof(string), DbType.String, false) { } } internal class FrameworkTypeNChar : FrameworkType { public FrameworkTypeNChar() : base(FrameworkTypeEnum.NChar, "nchar", 239, typeof(string), DbType.StringFixedLength, false) { } } internal class FrameworkTypeNvarcahr : FrameworkType { public FrameworkTypeNvarcahr() : base(FrameworkTypeEnum.Nvarcahr, "nvarcahr", 231, typeof(string), DbType.String, false) { } } internal class FrameworkTypeVarchar : FrameworkType { public FrameworkTypeVarchar() : base(FrameworkTypeEnum.Varchar, "varchar", 167, typeof(string), DbType.String, false) { } } internal class FrameworkTypeText : FrameworkType // See also: https://stackoverflow.com/questions/564755/sql-server-text-type-vs-varchar-data-type { public FrameworkTypeText() : base(FrameworkTypeEnum.Text, "text", 35, typeof(string), DbType.String, false) { } } internal class FrameworkTypeNtext : FrameworkType { public FrameworkTypeNtext() : base(FrameworkTypeEnum.Ntext, "ntext", 99, typeof(string), DbType.String, false) { } } internal class FrameworkTypeBit : FrameworkType { public FrameworkTypeBit() : base(FrameworkTypeEnum.Bit, "bit", 104, typeof(bool), DbType.Boolean, false) { } protected internal override string ValueToSqlParameterDebug(object value) { string result = null; if (value == DBNull.Value) { value = null; } if (value != null) { UtilFramework.Assert(value.GetType() == ValueType); if ((bool)value == false) { result = "CAST(0 AS BIT)"; } else { result = "CAST(1 AS BIT)"; } } if (value == null) { result = "NULL"; } return result; } protected internal override object CellTextParse(string text) { if (text?.ToLower() == "false") { return false; } if (text?.ToLower() == "true") { return true; } return base.CellTextParse(text); } protected internal override string ValueToCSharp(object value) { string result = "null"; if ((bool?)value == false) { result = "false"; } if ((bool?)value == true) { result = "true"; } return result; } } internal class FrameworkTypeMoney : FrameworkType { public FrameworkTypeMoney() : base(FrameworkTypeEnum.Money, "money", 60, typeof(decimal), DbType.Decimal, true) { } } internal class FrameworkTypeSmallmoney : FrameworkType { public FrameworkTypeSmallmoney() : base(FrameworkTypeEnum.Smallmoney, "smallmoney", 122, typeof(decimal), DbType.Decimal, true) { } } internal class FrameworkTypeDecimal : FrameworkType { public FrameworkTypeDecimal() : base(FrameworkTypeEnum.Decimal, "decimal", 106, typeof(decimal), DbType.Decimal, true) { } } internal class FrameworkTypeReal : FrameworkType { public FrameworkTypeReal() : base(FrameworkTypeEnum.Real, "real", 59, typeof(Single), DbType.Single, true) { } } internal class FrameworkTypeFloat : FrameworkType { public FrameworkTypeFloat() : base(FrameworkTypeEnum.Float, "float", 62, typeof(double), DbType.Double, true) { } protected internal override string ValueToCSharp(object value) { string result = null; if (value != null) { result = ((Double)value).ToString(System.Globalization.CultureInfo.InvariantCulture); // value.ToString(); returns for example 9,5 instead of 9.5 } if (value == null) { result = "null"; } return result; } } internal class FrameworkTypeVarbinary : FrameworkType { public FrameworkTypeVarbinary() : base(FrameworkTypeEnum.Varbinary, "varbinary", 165, typeof(byte[]), DbType.Binary, false) // DbType.Binary? { } protected internal override string CellTextFromValue(object value) { return null; // return UtilFramework.IntToText(((byte[])value).Length) + " bytes"; // When user changes this text it gets saved to db. // return Convert.ToBase64String((byte[])value); } protected internal override object CellTextParse(string text) { throw new Exception("Can not parse binary!"); // object result = null; // if (text != null) // { // return Encoding.Unicode.GetBytes(text); // } // return result; } protected internal override string ValueToCSharp(object value) { string result = "null"; if (value != null) { string text = Convert.ToBase64String(((byte[])value)); StringBuilder stringBuilder = new StringBuilder(); String.Join(null, "", ""); stringBuilder.Append("Convert.FromBase64String(String.Join(null, \"\""); var textList = UtilFramework.SplitChunk(text, 320); foreach (var item in textList) { stringBuilder.AppendLine(","); stringBuilder.Append(" "); stringBuilder.Append($"\"{item}\""); } stringBuilder.Append("))"); result = stringBuilder.ToString(); } return result; } } internal class SqlTypeSqlvariant : FrameworkType { public SqlTypeSqlvariant() : base(FrameworkTypeEnum.Sqlvariant, "sql_variant", 98, typeof(object), DbType.Object, false) { } } internal class FrameworkTypeImage : FrameworkType { public FrameworkTypeImage() : base(FrameworkTypeEnum.Image, "image", 34, typeof(byte[]), DbType.Binary, false) // DbType.Binary? { } } internal class FrameworkTypeNumeric : FrameworkType { public FrameworkTypeNumeric() : base(FrameworkTypeEnum.Numeric, "numeric", 108, typeof(decimal), DbType.Decimal, true) { } } }