add AddRange, updates to ToDataTable, GetColumnAttributes, sqltypemap

This commit is contained in:
Ryan Peters 2021-04-20 18:01:10 -04:00
parent d12750e1c4
commit 8a8c205855
4 changed files with 156 additions and 19 deletions

View File

@ -11,7 +11,7 @@
<TargetFramework>netstandard2.0</TargetFramework> <TargetFramework>netstandard2.0</TargetFramework>
<Description>A set of common utilities and extension methods for .NET.</Description> <Description>A set of common utilities and extension methods for .NET.</Description>
<LangVersion>7.3</LangVersion> <LangVersion>7.3</LangVersion>
<Version>21.4.20.1</Version> <Version>21.4.20.2</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild> <GeneratePackageOnBuild>true</GeneratePackageOnBuild>
</PropertyGroup> </PropertyGroup>

View File

@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations.Schema;
using System.Data; using System.Data;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection;
namespace BinaryDad.Extensions namespace BinaryDad.Extensions
{ {
@ -24,6 +25,20 @@ namespace BinaryDad.Extensions
items.Add(item); items.Add(item);
} }
/// <summary>
/// Adds the elements of the specified collection to the end of the <see cref="ICollection{T}"/>
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="source"></param>
/// <param name="items"></param>
public static void AddRange<T>(this ICollection<T> source, IEnumerable<T> items)
{
foreach (var item in items)
{
source.Add(item);
}
}
/// <summary> /// <summary>
/// Returns a distinct list of elements using the first-matched item on a specific property /// Returns a distinct list of elements using the first-matched item on a specific property
/// </summary> /// </summary>
@ -59,31 +74,82 @@ namespace BinaryDad.Extensions
/// </summary> /// </summary>
/// <typeparam name="T"></typeparam> /// <typeparam name="T"></typeparam>
/// <param name="collection"></param> /// <param name="collection"></param>
/// <param name="useColumnAttributeName">Specifies whether the data column should use the name from <see cref="ColumnAttribute"/></param> or <see cref="PropertyAliasAttribute"/>, if bound to a property. /// <param name="columnNameModifier">If a non-null string is returned, the column name is overridden</param>
/// <param name="useColumnAttributeName">Specifies whether the data column should use the name from <see cref="ColumnAttribute"/></param>, if bound to a property.
/// <param name="tableName"></param> /// <param name="tableName"></param>
/// <returns></returns> /// <returns></returns>
public static DataTable ToDataTable<T>(this IEnumerable<T> collection, bool useColumnAttributeName = false, string tableName = null) where T : class public static DataTable ToDataTable(this IEnumerable collection, Func<PropertyInfo, string> 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)) using (var table = new DataTable(tableName))
{ {
#region Build Table Schema #region Build Table Schema
var propertyInfo = typeof(T) var propertyInfo = type
.GetProperties() .GetProperties()
.Select(p => new .Where(p => !p.HasCustomAttribute<NotMappedAttribute>())
.Select(p =>
{ {
Property = p, string columnName = p.Name;
string columnTypeName = null;
// set column name to be either the property name // set column name to be either the property name
// or, if specified, based on the attribute // or, if specified, based on the attribute
ColumnName = useColumnAttributeName if (useColumnAttributeName)
? p.GetDataColumnNames().FirstOrDefault() {
: p.Name, var columnAttributes = p.GetColumnAttributes();
// include the column if [NotMapped] is NOT attached columnName = p.GetDataColumnNames(columnAttributes).FirstOrDefault();
IncludeColumn = !p.HasCustomAttribute<NotMappedAttribute>() 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(); .ToList();
foreach (var info in propertyInfo) foreach (var info in propertyInfo)
@ -95,6 +161,11 @@ namespace BinaryDad.Extensions
columnType = columnType.GetGenericArguments()[0]; columnType = columnType.GetGenericArguments()[0];
} }
if (info.ColumnTypeName.IsNotEmpty())
{
columnType = SqlTypeMap.GetType(info.ColumnTypeName);
}
table.Columns.Add(info.ColumnName, columnType); table.Columns.Add(info.ColumnName, columnType);
} }
@ -109,6 +180,12 @@ namespace BinaryDad.Extensions
foreach (var info in propertyInfo) foreach (var info in propertyInfo)
{ {
var value = info.Property.GetValue(item, null); 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) if (value != null)
{ {

View File

@ -83,6 +83,19 @@ namespace BinaryDad.Extensions
/// <param name="property"></param> /// <param name="property"></param>
/// <returns></returns> /// <returns></returns>
internal static IEnumerable<string> GetDataColumnNames(this PropertyInfo property) internal static IEnumerable<string> GetDataColumnNames(this PropertyInfo property)
{
var attributes = property.GetColumnAttributes();
return GetDataColumnNames(property, attributes);
}
/// <summary>
/// Retrieves a list of available property binding aliases using <see cref="ColumnAttribute"/>.
/// </summary>
/// <param name="property"></param>
/// <param name="attributes"></param>
/// <returns></returns>
internal static IEnumerable<string> GetDataColumnNames(this PropertyInfo property, ICollection<ColumnAttribute> attributes)
{ {
#region Null checks #region Null checks
@ -95,14 +108,9 @@ namespace BinaryDad.Extensions
var dataRowFieldNames = new List<string>(); var dataRowFieldNames = new List<string>();
var attributes = property foreach (var attribute in attributes)
.GetCustomAttributes(true);
var columnAttribute = attributes.FirstOfType<ColumnAttribute>();
if (columnAttribute != null)
{ {
dataRowFieldNames.Add(columnAttribute.Name); dataRowFieldNames.Add(attribute.Name);
} }
// add the property's name at the end, so it's the last in the lookup // 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)); .FirstOrDefault(f => columns.Contains(f));
} }
internal static ICollection<ColumnAttribute> GetColumnAttributes(this PropertyInfo property)
{
return property
.GetCustomAttributes<ColumnAttribute>()
.OrderBy(c => !c.IsDefaultAttribute())
.ToList();
}
/// <summary> /// <summary>
/// Retrieves an instance of the <see cref="TypeConverter"/> associated with the property's <see cref="TypeConverterAttribute"/> /// Retrieves an instance of the <see cref="TypeConverter"/> associated with the property's <see cref="TypeConverterAttribute"/>
/// </summary> /// </summary>

View File

@ -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<SqlDbType, Type> typeMap = new Dictionary<SqlDbType, Type>
{
[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<SqlDbType>());
}
public static Type GetType(SqlDbType sqlDbType)
{
// I'm aware this is backward
return typeMap.FirstOrDefault(t => t.Key == sqlDbType).Value;
}
}
}