using System.Collections.Concurrent;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data;
using System.Data.Common;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.CompilerServices;
using EasyDevCore.Common;
using Microsoft.EntityFrameworkCore.Storage;
namespace EasyDevCore.Database
{
///
/// Mapper to model of type />
///
/// Model type
public class DbDataReaderMapper where T : class, new()
{
private struct Prop
{
public int ColumnOrdinal;
public string ColumnName;
public Action Setter { get; set; }
}
///
/// Contains different columns set information mapped to type .
///
private static readonly ConcurrentDictionary PropertiesCache = new ConcurrentDictionary();
private readonly DbDataReader _reader;
private readonly Prop[] _properties;
///
/// Initializes a new instance of the class.
///
/// The reader.
public DbDataReaderMapper(RelationalDataReader reader) : this(reader.DbDataReader)
{
}
///
/// Initializes a new instance of the class.
///
/// The reader.
public DbDataReaderMapper(DbDataReader reader)
{
_reader = reader;
_properties = MapColumnsToProperties();
}
///
/// Gets the rows.
///
///
public IEnumerable GetRows()
{
while (_reader.Read())
{
yield return MapNextRow();
}
}
///
/// Gets the rows asynchronous.
///
/// The cancellation token.
///
public async IAsyncEnumerable GetRowsAsync([EnumeratorCancellation] CancellationToken cancellationToken = default)
{
while (await _reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
yield return await MapNextRowAsync().ConfigureAwait(false);
}
}
///
/// Map to a T and apply an action on it for each row
///
/// Action to apply to each row
public void Map(Action action)
{
while (_reader.Read())
{
T row = MapNextRow();
action(row);
}
}
///
/// Map to a T and apply an action on it for each row
///
/// Action to apply to each row
public Task MapAsync(Action action)
{
return MapAsync(action, CancellationToken.None);
}
///
/// Map to a T and apply an action on it for each row
///
/// Action to apply to each row
/// The cancellation instruction, which propagates a notification that operations should be canceled
public async Task MapAsync(Action action, CancellationToken cancellationToken)
{
while (await _reader.ReadAsync(cancellationToken).ConfigureAwait(false))
{
T row = await MapNextRowAsync(cancellationToken).ConfigureAwait(false);
action(row);
}
}
///
/// Maps the next row.
///
///
public T MapNextRow()
{
T row = new T();
for (int i = 0; i < _properties.Length; ++i)
{
object value = _reader.IsDBNull(_properties[i].ColumnOrdinal) ? null : _reader.GetValue(_properties[i].ColumnOrdinal);
try
{
_properties[i].Setter(row, value);
}
catch (Exception ex)
{
ex.Data["Mapping"] = $"Model[{_properties[i].ColumnName}] = Column[{_reader.GetName(_properties[i].ColumnOrdinal)}]";
throw;
}
}
return row;
}
///
/// Maps the next row asynchronous.
///
///
public Task MapNextRowAsync()
{
return MapNextRowAsync(CancellationToken.None);
}
///
/// Maps the next row asynchronous.
///
/// The cancellation token.
///
public async Task MapNextRowAsync(CancellationToken cancellationToken)
{
T row = new T();
for (int i = 0; i < _properties.Length; ++i)
{
object value = await _reader.IsDBNullAsync(_properties[i].ColumnOrdinal, cancellationToken).ConfigureAwait(false)
? null
: _reader.GetValue(_properties[i].ColumnOrdinal);
try
{
_properties[i].Setter(row, value);
}
catch (Exception ex)
{
ex.Data["Mapping"] = $"Column[{_reader.GetName(_properties[i].ColumnOrdinal)}] = Model[{_properties[i].ColumnName}]";
throw;
}
}
return row;
}
private Prop[] MapColumnsToProperties()
{
Type modelType = typeof(T);
string[] columns = new string[_reader.FieldCount];
Type[] colTypes = new Type[_reader.FieldCount];
for (int i = 0; i < _reader.FieldCount; ++i)
{
columns[i] = _reader.GetName(i);
colTypes[i] = _reader.GetFieldType(i);
}
ulong propKey = CheckSumHelper.CRC64(modelType.FullName + "|" + string.Join("|", columns) + "|" + string.Join("|", colTypes.Select(c => c.FullName)));
if (PropertiesCache.TryGetValue(propKey, out var s))
{
return s;
}
var propertyInfos = modelType.GetProperties(BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance).Where(p => p.CanWrite && !p.GetCustomAttributes().Any())
.Select(p => new { Property = p, Mapping = p.GetCustomAttribute() });
var mappingFields = (from c in columns.Select((v, i) => new { Index = i, Name = v, ColType = colTypes[i] })
from p in propertyInfos
where c.Name.Equals(p.Mapping != null ? p.Mapping.Name : p.Property.Name, StringComparison.InvariantCultureIgnoreCase)
select new { c.Index, p.Property, p.Mapping, c.ColType });
var properties = new List();
ParameterExpression instance = Expression.Parameter(typeof(object), "instance");
ParameterExpression value = Expression.Parameter(typeof(object), "value");
//MethodInfo changeTypeMethod = typeof(Convert).GetMethod("ChangeType", new[] { typeof(object), typeof(Type) });
foreach (var fi in mappingFields)
{
PropertyInfo prop = fi.Property;
// "x as T" is faster than "(T) x" if x is a reference type
UnaryExpression instanceCast = prop.DeclaringType.IsValueType ? Expression.Convert(instance, prop.DeclaringType) : Expression.TypeAs(instance, prop.DeclaringType);
UnaryExpression valueCast = prop.PropertyType.IsValueType ? Expression.Convert(value, prop.PropertyType) : Expression.TypeAs(value, prop.PropertyType); MethodCallExpression setterCall = Expression.Call(instanceCast, prop.GetSetMethod(), valueCast);
var setter = (Action)Expression.Lambda(setterCall, instance, value).Compile();
Action setterConvert = null;
if (prop.PropertyType != fi.ColType)
{
setterConvert = ((Expression>)((o, v) => setter(o, v == null ? null : Convert.ChangeType(v, fi.Property.PropertyType)))).Compile();
}
properties.Add(new Prop
{
ColumnOrdinal = fi.Index,
ColumnName = prop.Name,
Setter = setterConvert == null ? setter : setterConvert
});
}
Prop[] propertiesArray = properties.ToArray();
PropertiesCache[propKey] = propertiesArray;
return propertiesArray;
}
}
}