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.EntityFrameworkCore { /// /// 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; } } }