From 8a8c2058555339cc5d7e1805d9748d194a90f73e Mon Sep 17 00:00:00 2001 From: binarydad Date: Tue, 20 Apr 2021 18:01:10 -0400 Subject: [PATCH] add AddRange, updates to ToDataTable, GetColumnAttributes, sqltypemap --- .../BinaryDad.Extensions.csproj | 2 +- .../Extensions/CollectionExtensions.cs | 99 ++++++++++++++++--- .../Extensions/ReflectionExtensions.cs | 30 ++++-- BinaryDad.Extensions/Extensions/SqlTypeMap.cs | 44 +++++++++ 4 files changed, 156 insertions(+), 19 deletions(-) create mode 100644 BinaryDad.Extensions/Extensions/SqlTypeMap.cs diff --git a/BinaryDad.Extensions/BinaryDad.Extensions.csproj b/BinaryDad.Extensions/BinaryDad.Extensions.csproj index 33e62a3..bb68b57 100644 --- a/BinaryDad.Extensions/BinaryDad.Extensions.csproj +++ b/BinaryDad.Extensions/BinaryDad.Extensions.csproj @@ -11,7 +11,7 @@ netstandard2.0 A set of common utilities and extension methods for .NET. 7.3 - 21.4.20.1 + 21.4.20.2 true diff --git a/BinaryDad.Extensions/Extensions/CollectionExtensions.cs b/BinaryDad.Extensions/Extensions/CollectionExtensions.cs index af23fb5..75732e7 100644 --- a/BinaryDad.Extensions/Extensions/CollectionExtensions.cs +++ b/BinaryDad.Extensions/Extensions/CollectionExtensions.cs @@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Data; using System.Linq; using System.Linq.Expressions; +using System.Reflection; namespace BinaryDad.Extensions { @@ -24,6 +25,20 @@ namespace BinaryDad.Extensions items.Add(item); } + /// + /// Adds the elements of the specified collection to the end of the + /// + /// + /// + /// + public static void AddRange(this ICollection source, IEnumerable items) + { + foreach (var item in items) + { + source.Add(item); + } + } + /// /// Returns a distinct list of elements using the first-matched item on a specific property /// @@ -59,31 +74,82 @@ namespace BinaryDad.Extensions /// /// /// - /// Specifies whether the data column should use the name from or , if bound to a property. + /// If a non-null string is returned, the column name is overridden + /// Specifies whether the data column should use the name from , if bound to a property. /// /// - public static DataTable ToDataTable(this IEnumerable collection, bool useColumnAttributeName = false, string tableName = null) where T : class + public static DataTable ToDataTable(this IEnumerable collection, Func columnNameModifier, bool useColumnAttributeName = false, string tableName = null) { + if (collection == null) + { + return null; + } + + #region Get type of first record + + // NOTE: + // We assume all values are the same, so use the type from the first record. + // This allows us to use the actual instance type instead of the generic version (useful for anonymous type collections) + + Type type = null; + var genericArguments = collection.GetType().GetGenericArguments(); + + if (genericArguments.Any()) + { + // handle anonymous types, where there are multiple generic arguments (the first is an integer) + type = genericArguments.FirstOrDefault(g => g.IsClass); + } + else + { + var enumerator = collection.GetEnumerator(); + + enumerator.MoveNext(); + + type = enumerator.Current.GetType(); + } + + #endregion + using (var table = new DataTable(tableName)) { #region Build Table Schema - var propertyInfo = typeof(T) + var propertyInfo = type .GetProperties() - .Select(p => new + .Where(p => !p.HasCustomAttribute()) + .Select(p => { - Property = p, + string columnName = p.Name; + string columnTypeName = null; // set column name to be either the property name // or, if specified, based on the attribute - ColumnName = useColumnAttributeName - ? p.GetDataColumnNames().FirstOrDefault() - : p.Name, + if (useColumnAttributeName) + { + var columnAttributes = p.GetColumnAttributes(); - // include the column if [NotMapped] is NOT attached - IncludeColumn = !p.HasCustomAttribute() + columnName = p.GetDataColumnNames(columnAttributes).FirstOrDefault(); + columnTypeName = columnAttributes.FirstOrDefault()?.TypeName; + } + + if (columnNameModifier != null) + { + var modifiedColumnName = columnNameModifier.Invoke(p); + + if (modifiedColumnName.IsNotEmpty()) + { + columnName = modifiedColumnName; + } + } + + return new + { + Property = p, + ColumnTypeName = columnTypeName, + Converter = p.GetAttributeTypeConverter(), + ColumnName = columnName + }; }) - .Where(p => p.IncludeColumn) .ToList(); foreach (var info in propertyInfo) @@ -95,6 +161,11 @@ namespace BinaryDad.Extensions columnType = columnType.GetGenericArguments()[0]; } + if (info.ColumnTypeName.IsNotEmpty()) + { + columnType = SqlTypeMap.GetType(info.ColumnTypeName); + } + table.Columns.Add(info.ColumnName, columnType); } @@ -109,6 +180,12 @@ namespace BinaryDad.Extensions foreach (var info in propertyInfo) { var value = info.Property.GetValue(item, null); + var columnType = row.Table.Columns[info.ColumnName].DataType; + + if (info.Converter != null && info.Converter.CanConvertTo(columnType)) + { + value = info.Converter.ConvertTo(value, columnType); + } if (value != null) { diff --git a/BinaryDad.Extensions/Extensions/ReflectionExtensions.cs b/BinaryDad.Extensions/Extensions/ReflectionExtensions.cs index 58ae021..63a32b3 100644 --- a/BinaryDad.Extensions/Extensions/ReflectionExtensions.cs +++ b/BinaryDad.Extensions/Extensions/ReflectionExtensions.cs @@ -83,6 +83,19 @@ namespace BinaryDad.Extensions /// /// internal static IEnumerable GetDataColumnNames(this PropertyInfo property) + { + var attributes = property.GetColumnAttributes(); + + return GetDataColumnNames(property, attributes); + } + + /// + /// Retrieves a list of available property binding aliases using . + /// + /// + /// + /// + internal static IEnumerable GetDataColumnNames(this PropertyInfo property, ICollection attributes) { #region Null checks @@ -95,14 +108,9 @@ namespace BinaryDad.Extensions var dataRowFieldNames = new List(); - var attributes = property - .GetCustomAttributes(true); - - var columnAttribute = attributes.FirstOfType(); - - if (columnAttribute != null) + foreach (var attribute in attributes) { - dataRowFieldNames.Add(columnAttribute.Name); + dataRowFieldNames.Add(attribute.Name); } // add the property's name at the end, so it's the last in the lookup @@ -169,6 +177,14 @@ namespace BinaryDad.Extensions .FirstOrDefault(f => columns.Contains(f)); } + internal static ICollection GetColumnAttributes(this PropertyInfo property) + { + return property + .GetCustomAttributes() + .OrderBy(c => !c.IsDefaultAttribute()) + .ToList(); + } + /// /// Retrieves an instance of the associated with the property's /// diff --git a/BinaryDad.Extensions/Extensions/SqlTypeMap.cs b/BinaryDad.Extensions/Extensions/SqlTypeMap.cs new file mode 100644 index 0000000..43feae4 --- /dev/null +++ b/BinaryDad.Extensions/Extensions/SqlTypeMap.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; + +namespace BinaryDad.Extensions +{ + public static class SqlTypeMap + { + public static Dictionary typeMap = new Dictionary + { + [SqlDbType.NVarChar] = typeof(string), + [SqlDbType.VarChar] = typeof(string), + [SqlDbType.Char] = typeof(char[]), + [SqlDbType.NChar] = typeof(char[]), + [SqlDbType.TinyInt] = typeof(byte), + [SqlDbType.SmallInt] = typeof(short), + [SqlDbType.Int] = typeof(int), + [SqlDbType.BigInt] = typeof(long), + [SqlDbType.Bit] = typeof(bool), + [SqlDbType.DateTime] = typeof(DateTime), + [SqlDbType.DateTime2] = typeof(DateTime), + [SqlDbType.SmallDateTime] = typeof(DateTime), + [SqlDbType.Time] = typeof(TimeSpan), + [SqlDbType.DateTimeOffset] = typeof(DateTimeOffset), + [SqlDbType.Decimal] = typeof(decimal), + [SqlDbType.Money] = typeof(decimal), + [SqlDbType.SmallMoney] = typeof(decimal), + [SqlDbType.Float] = typeof(double), + [SqlDbType.Real] = typeof(float) + }; + + public static Type GetType(string sqlDbTypeName) + { + return GetType(sqlDbTypeName.ToEnum()); + } + + public static Type GetType(SqlDbType sqlDbType) + { + // I'm aware this is backward + return typeMap.FirstOrDefault(t => t.Key == sqlDbType).Value; + } + } +} \ No newline at end of file