/// <summary> /// Gets a POCO from the data record. /// </summary> /// <param name="dataRequest"> /// The data request. /// </param> /// <param name="entityName"> /// The entity name. /// </param> /// <param name="record"> /// The data record to read. /// </param> /// <typeparam name="T"> /// The type of POCO to generate. /// </typeparam> /// <returns> /// The POCO object as a type of <typeparamref name="T"/>. /// </returns> private static T GetPocoFromReader <T>(PocoDataRequest dataRequest, string entityName, IDataRecord record) { var baseKey = new Tuple <string, PocoDataRequest>(entityName, dataRequest); var baseDirectAttributes = dataRequest.AttributeDefinitions.Where(x => x.IsReferencedDirect).ToList(); var basePocoDelegate = PocoFactories.GetOrAdd( baseKey, key => new Lazy <PocoDelegateInfo>(() => FlatPocoFactory.CreateDelegate(dataRequest, typeof(T), baseDirectAttributes))) .Value; T poco; #if DEBUG try { ////var recordItems = Enumerable.Range(0, record.FieldCount).Select(i => $"{record.GetName(i)}='{record.GetValue(i)}'"); ////Trace.WriteLine($"Getting data from record {string.Join(",", recordItems)}"); #endif poco = (T)basePocoDelegate.MappingDelegate.DynamicInvoke(record); #if DEBUG } catch (TargetInvocationException ex) { throw new OperationException(basePocoDelegate, $"Error creating a POCO for type {typeof(T)}: {ex.Message}", ex); } #endif return(poco); }
/// <summary> /// Creates a delegate that returns a POCO from the provided data reader. TODO: Make private or coalesce into caller? /// </summary> /// <param name="dataRequest"> /// The data request to create a POCO for. /// </param> /// <param name="type"> /// The type of the POCO to create. /// </param> /// <returns> /// A <see cref="Delegate"/> that creates a POCO from the reader. /// </returns> public static PocoDelegateInfo CreateDelegate([NotNull] PocoDataRequest dataRequest, [NotNull] Type type) { if (dataRequest == null) { throw new ArgumentNullException(nameof(dataRequest)); } return(CreateDelegate(dataRequest, type, dataRequest.AttributeDefinitions)); }
/// <summary> /// Creates a delegate that returns a POCO from the provided data reader. /// </summary> /// <typeparam name="T"> /// The type of POCO expected by the return statement. /// </typeparam> /// <param name="dataRequest"> /// The data request to create a POCO for. /// </param> /// <returns> /// A <see cref="Delegate"/> that creates a POCO from the reader. /// </returns> public static PocoDelegateInfo CreateDelegate <T>([NotNull] PocoDataRequest dataRequest) { if (dataRequest == null) { throw new ArgumentNullException(nameof(dataRequest)); } return(CreateDelegate(dataRequest, typeof(T))); }
/// <summary> /// Fills the return list with POCOs hydrated from the reader. /// </summary> /// <param name="reader"> /// The reader to read. /// </param> /// <param name="entityDefinition"> /// The entity definition for the POCOs. /// </param> /// <exception cref="OperationException"> /// The <see cref="FlatPocoFactory"/> did not return a delegate of the expected type. /// </exception> private T FillReturnList(IDataReader reader, IEntityDefinition entityDefinition) { var pocoDataRequest = new PocoDataRequest(reader, entityDefinition, this.DatabaseContext); var mappingDelegate = FlatPocoFactory.CreateDelegate <T>(pocoDataRequest).MappingDelegate; if (mappingDelegate is Func <IDataReader, T> pocoDelegate) { var poco = pocoDelegate.Invoke(reader); return(poco); } else { throw new OperationException( pocoDataRequest, string.Format(CultureInfo.CurrentCulture, ErrorMessages.DelegateCouldNotBeCreatedWithReader, pocoDataRequest)); } }
/// <summary> /// Gets a related entity POCO from the reader. /// </summary> /// <param name="entityAggregateName"> /// The entity aggregate name. /// </param> /// <param name="dataRequest"> /// The data request. /// </param> /// <param name="reader"> /// The reader. /// </param> /// <param name="entityReference"> /// The entity reference. /// </param> /// <returns> /// The related POCO as an <see cref="object"/>. /// </returns> private object GetRelatedEntity(string entityAggregateName, PocoDataRequest dataRequest, IDataRecord reader, EntityReference entityReference) { var relatedEntityLocation = this.definitionProvider.GetEntityLocation(entityReference); var relatedQualifiedName = string.IsNullOrWhiteSpace(relatedEntityLocation.Alias) ? $"{relatedEntityLocation.Container}.{relatedEntityLocation.Name}" : relatedEntityLocation.Alias; var relatedKey = new Tuple <string, PocoDataRequest>($"{entityAggregateName}:{relatedQualifiedName}", dataRequest); // TODO: Cache attributes with their locations, or build explicitly. ////var relatedAttributes = entityDefinition.ReturnableAttributes.Where(x => x.ReferenceNode?.Value == relatedEntityLocation).ToList(); var relatedAttributes = dataRequest.AttributeDefinitions.Where(x => x.ReferenceNode?.Value == relatedEntityLocation).ToList(); var relatedType = relatedEntityLocation.EntityType; var relatedPocoDelegate = PocoFactories.GetOrAdd( relatedKey, key => new Lazy <PocoDelegateInfo>(() => FlatPocoFactory.CreateDelegate(dataRequest, relatedType, relatedAttributes))) .Value; object relatedEntity; #if DEBUG try { #endif relatedEntity = relatedPocoDelegate.MappingDelegate.DynamicInvoke(reader); #if DEBUG } catch (TargetInvocationException ex) { throw new OperationException(relatedPocoDelegate, $"Error creating a POCO for type {relatedType}: {ex.Message}", ex); } #endif return(relatedEntity); }
/// <summary> /// Creates a POCO from the data reader. /// </summary> /// <param name="dataRequest"> /// The data request. /// </param> /// <typeparam name="T"> /// The type of POCO to return. /// </typeparam> /// <returns> /// A new POCO of the specified type. /// </returns> public T CreatePoco <T>([NotNull] PocoDataRequest dataRequest) { if (dataRequest == null) { throw new ArgumentNullException(nameof(dataRequest)); } var reader = dataRequest.DataReader; if (typeof(T) == typeof(object)) { // TODO: See if we can infer the object graph from the included attributes vs. the flatness. return(GetPocoFromReader <dynamic>(dataRequest, typeof(T).FullName, dataRequest.DataReader)); } else { var entityDefinition = this.definitionProvider.Resolve <T>(); var pocoKey = GetPocoKey(entityDefinition, reader, entityDefinition.PrimaryKeyAttributes.OrderBy(x => x.Ordinal).ToList()); var poco = (T)this.pocoMemoryCache.GetOrAdd( pocoKey, key => new Lazy <object>(() => GetPocoFromReader <T>(dataRequest, entityDefinition.QualifiedName, reader))) .Value; var relationAttributes = entityDefinition.AllAttributes.Where(x => x.AttributeTypes == EntityAttributeTypes.Relation); foreach (var relationAttribute in relationAttributes) { // TODO: Change this brittle code. We need to match on aliases to get the right IDs. var relatedDefinition = this.definitionProvider.Resolve(relationAttribute.PropertyInfo.PropertyType); var relationReferenceName = string.IsNullOrWhiteSpace(relationAttribute.Alias) ? $"{relatedDefinition.EntityContainer}.{relatedDefinition.EntityName}" : relationAttribute.Alias; var keyDefinitions = entityDefinition.ReturnableAttributes.Where( x => (string.IsNullOrWhiteSpace(x.Entity.Alias) ? $"{x.Entity.Container}.{x.Entity.Name}" : x.Entity.Alias) == relationReferenceName && x.IsPrimaryKey) .OrderBy(x => x.Ordinal); var relatedPocoKey = GetPocoKey(relatedDefinition, reader, keyDefinitions.ToList()); var entityReference = new EntityReference { ContainerType = typeof(T), EntityType = relationAttribute.PropertyInfo.PropertyType, EntityAlias = relationAttribute.Alias }; var relatedEntity = this.pocoMemoryCache.GetOrAdd( relatedPocoKey, key => new Lazy <object>( () => this.GetRelatedEntity(entityDefinition.QualifiedName, dataRequest, reader, entityReference))) .Value; // Prevents "ghost" POCO properties when resolving second-order or higher relations. if (relatedEntity == null) { continue; } // Check whether this is a first-order relation. if (relationAttribute.EntityNode == relationAttribute.EntityNode.List.First) { relationAttribute.SetValueDelegate.DynamicInvoke(poco, relatedEntity); } else { var relationContainer = NavigateToEntity(relationAttribute.ReferenceNode, poco); relationAttribute.SetValueDelegate.DynamicInvoke(relationContainer, relatedEntity); } } return(poco); } }
/// <summary> /// Creates a delegate that returns a POCO from the provided data reader. TODO: Make internal? /// </summary> /// <param name="dataRequest"> /// The data request to create a POCO for. /// </param> /// <param name="type"> /// The type of the POCO to create. /// </param> /// <param name="attributeDefinitions"> /// The attribute definitions. /// </param> /// <returns> /// A <see cref="Delegate"/> that creates a POCO from the reader. /// </returns> public static PocoDelegateInfo CreateDelegate( [NotNull] PocoDataRequest dataRequest, [NotNull] Type type, [NotNull] IEnumerable <EntityAttributeDefinition> attributeDefinitions) { if (dataRequest == null) { throw new ArgumentNullException(nameof(dataRequest)); } if (type == null) { throw new ArgumentNullException(nameof(type)); } if (attributeDefinitions == null) { throw new ArgumentNullException(nameof(attributeDefinitions)); } var definitions = attributeDefinitions.ToList(); // Create the method var name = $"poco_factory_{Guid.NewGuid()}"; var method = new DynamicMethod(name, type, new[] { typeof(IDataReader) }, true); var generator = method.GetILGenerator(); var reader = dataRequest.DataReader; if (type == typeof(object)) { // var poco=new T() var constructorInfo = typeof(ExpandoObject).GetConstructor(Type.EmptyTypes); if (constructorInfo == null) { throw new InvalidOperationException( string.Format(CultureInfo.CurrentCulture, ErrorMessages.ConstructorInfoDoesNotExist, typeof(ExpandoObject))); } generator.Emit(OpCodes.Newobj, constructorInfo); // obj var addMethod = typeof(IDictionary <string, object>).GetMethod("Add"); // Enumerate all fields generating a set assignment for the column for (var i = dataRequest.FirstColumn; i < dataRequest.FirstColumn + dataRequest.FieldCount; i++) { var sourceType = reader.GetFieldType(i); #if NET472 var fieldName = reader.GetName(i).Replace(".", string.Empty); // Remove period from dot-qualified names. #else var fieldName = reader.GetName(i).Replace(".", string.Empty, true, CultureInfo.InvariantCulture); // Remove period from dot-qualified names. #endif generator.Emit(OpCodes.Dup); // obj, obj generator.Emit(OpCodes.Ldstr, fieldName); // obj, obj, fieldname // Get the converter var converter = GetConverter(sourceType, type, dataRequest.DatabaseContext); // Setup stack for call to converter AddConverterToStack(generator, converter); // reader[i] generator.Emit(OpCodes.Ldarg_0); // obj, obj, fieldname, converter?, rdr generator.Emit(OpCodes.Ldc_I4, i); // obj, obj, fieldname, converter?, rdr,i generator.Emit(OpCodes.Callvirt, GetValueMethod); // obj, obj, fieldname, converter?, value // Convert DBNull to null generator.Emit(OpCodes.Dup); // obj, obj, fieldname, converter?, value, value generator.Emit(OpCodes.Isinst, typeof(DBNull)); // obj, obj, fieldname, converter?, value, (value or null) var lblNotNull = generator.DefineLabel(); generator.Emit(OpCodes.Brfalse_S, lblNotNull); // obj, obj, fieldname, converter?, value generator.Emit(OpCodes.Pop); // obj, obj, fieldname, converter? if (converter != null) { generator.Emit(OpCodes.Pop); // obj, obj, fieldname, } generator.Emit(OpCodes.Ldnull); // obj, obj, fieldname, null if (converter != null) { var lblReady = generator.DefineLabel(); generator.Emit(OpCodes.Br_S, lblReady); generator.MarkLabel(lblNotNull); generator.Emit(OpCodes.Callvirt, InvokeMethod); generator.MarkLabel(lblReady); } else { generator.MarkLabel(lblNotNull); } generator.Emit(OpCodes.Callvirt, addMethod); } generator.Emit(OpCodes.Ret); } else if (type.IsValueType || type == typeof(string) || type == typeof(byte[])) { // Do we need to install a converter? var sourceType = reader.GetFieldType(0); var converter = GetConverter(sourceType, type, dataRequest.DatabaseContext); // "if (!rdr.IsDBNull(i))" generator.Emit(OpCodes.Ldarg_0); // rdr generator.Emit(OpCodes.Ldc_I4_0); // rdr,0 generator.Emit(OpCodes.Callvirt, IsDbNullMethod); // bool var continueLabel = generator.DefineLabel(); generator.Emit(OpCodes.Brfalse_S, continueLabel); generator.Emit(OpCodes.Ldnull); // null var finishLabel = generator.DefineLabel(); generator.Emit(OpCodes.Br_S, finishLabel); generator.MarkLabel(continueLabel); // Setup stack for call to converter AddConverterToStack(generator, converter); generator.Emit(OpCodes.Ldarg_0); // rdr generator.Emit(OpCodes.Ldc_I4_0); // rdr,0 generator.Emit(OpCodes.Callvirt, GetValueMethod); // value // Call the converter if (converter != null) { generator.Emit(OpCodes.Callvirt, InvokeMethod); } generator.MarkLabel(finishLabel); generator.Emit(OpCodes.Unbox_Any, type); // value converted generator.Emit(OpCodes.Ret); } else { // Set this when a column has a value. var hasValue = generator.DeclareLocal(typeof(bool)); generator.Emit(OpCodes.Ldc_I4, 0); generator.Emit(OpCodes.Stloc, hasValue); // var poco=new T() const BindingFlags InstanceConstructors = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; generator.Emit(OpCodes.Newobj, type.GetConstructor(InstanceConstructors, null, Array.Empty <Type>(), null)); // Enumerate all fields generating a set assignment for the column for (var i = dataRequest.FirstColumn; i < dataRequest.FirstColumn + dataRequest.FieldCount; i++) { // Get the PocoColumn for this db column, ignore if not known var key = reader.GetName(i); // We may need to set multiple attributes based on a single reader column when there are multiple paths in a POCO to // the same object reference. var attribute = definitions.FirstOrDefault(x => x.ReferenceName == key); if (attribute == default) { continue; } // Get the source type for this column var sourceType = reader.GetFieldType(i); var destinationType = attribute.PropertyInfo.PropertyType; // "if (!rdr.IsDBNull(i))" generator.Emit(OpCodes.Ldarg_0); // poco,rdr generator.Emit(OpCodes.Ldc_I4, i); // poco,rdr,i generator.Emit(OpCodes.Callvirt, IsDbNullMethod); // poco,bool var nextLabel = generator.DefineLabel(); generator.Emit(OpCodes.Brtrue_S, nextLabel); // poco // There's a value here so we set our local variable as such. var setLabel = generator.DefineLabel(); generator.Emit(OpCodes.Ldloc, hasValue); generator.Emit(OpCodes.Ldc_I4, 1); generator.Emit(OpCodes.Beq, setLabel); generator.Emit(OpCodes.Ldc_I4, 1); generator.Emit(OpCodes.Stloc, hasValue); generator.MarkLabel(setLabel); // Back to our poco. generator.Emit(OpCodes.Dup); // poco,poco // Do we need to install a converter? var converter = GetConverter(sourceType, destinationType, dataRequest.DatabaseContext); // Fast var handled = false; if (converter == null) { var valueGetter = typeof(IDataRecord).GetMethod(string.Concat("Get", sourceType.Name), new[] { typeof(int) }); var nullableType = Nullable.GetUnderlyingType(destinationType); var valueGetterMatchesType = valueGetter != null && valueGetter.ReturnType == sourceType && (valueGetter.ReturnType == destinationType || valueGetter.ReturnType == nullableType); if (valueGetterMatchesType) { generator.Emit(OpCodes.Ldarg_0); // *,rdr generator.Emit(OpCodes.Ldc_I4, i); // *,rdr,i generator.Emit(OpCodes.Callvirt, valueGetter); // *,value // Convert to Nullable if (nullableType != null) { var constructorInfo = destinationType.GetConstructor(new[] { nullableType }); if (constructorInfo == null) { throw new InvalidOperationException( string.Format(CultureInfo.CurrentCulture, ErrorMessages.ConstructorInfoDoesNotExist, nullableType)); } generator.Emit(OpCodes.Newobj, constructorInfo); } generator.Emit(OpCodes.Callvirt, attribute.PropertyInfo.GetSetMethod(true)); // poco handled = true; } } // Not so fast if (handled == false) { // Setup stack for call to converter AddConverterToStack(generator, converter); // "value = rdr.GetValue(i)" generator.Emit(OpCodes.Ldarg_0); // *,rdr generator.Emit(OpCodes.Ldc_I4, i); // *,rdr,i generator.Emit(OpCodes.Callvirt, GetValueMethod); // *,value // Call the converter if (converter != null) { generator.Emit(OpCodes.Callvirt, InvokeMethod); } // Assign it generator.Emit(OpCodes.Unbox_Any, attribute.PropertyInfo.PropertyType); // poco,poco,value generator.Emit(OpCodes.Callvirt, attribute.SetValueMethod); // poco } ////columnsMapped--; generator.MarkLabel(nextLabel); } var onLoadedMethod = RecurseInheritedTypes(type, x => x.GetMethod("OnLoaded", InstanceConstructors, null, Array.Empty <Type>(), null)); if (onLoadedMethod != null) { generator.Emit(OpCodes.Dup); generator.Emit(OpCodes.Callvirt, onLoadedMethod); } // Check whether anything was set. var isSetLabel = generator.DefineLabel(); generator.Emit(OpCodes.Ldloc, hasValue); generator.Emit(OpCodes.Ldc_I4, 1); generator.Emit(OpCodes.Beq, isSetLabel); // Remove the poco and load a null onto the stack. generator.Emit(OpCodes.Pop); generator.Emit(OpCodes.Ldnull); generator.Emit(OpCodes.Ret); // Jump here if something was set. generator.MarkLabel(isSetLabel); generator.Emit(OpCodes.Ret); } // Cache it, return it var mappingDelegate = method.CreateDelegate(Expression.GetFuncType(typeof(IDataReader), type)); return(new PocoDelegateInfo(mappingDelegate)); }
/// <inheritdoc /> public async IAsyncEnumerable <T> QueryAsync <T>(string sql, [EnumeratorCancellation] CancellationToken cancellationToken, params object[] args) { if (sql == null) { throw new ArgumentNullException(nameof(sql)); } if (string.IsNullOrWhiteSpace(sql)) { throw new ArgumentException(ErrorMessages.ValueCannotBeNullOrWhiteSpace, nameof(sql)); } if (args == null) { throw new ArgumentNullException(nameof(args)); } await this.OpenSharedConnectionAsync(cancellationToken).ConfigureAwait(false); var entityDefinition = this.RepositoryAdapter.DefinitionProvider.Resolve <T>(); if (this.Connection is DbConnection asyncConnection) { #if NET472 using (var command = this.CreateAsyncCommand(asyncConnection, sql, args)) #else await using (var command = this.CreateAsyncCommand(asyncConnection, sql, args)) #endif { ////var results = new List<T>(); #if NET472 using (var dataReader = await command.ExecuteReaderAsync().ConfigureAwait(false)) #else await using (var dataReader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false)) #endif { while (true) { if (await dataReader.ReadAsync(cancellationToken).ConfigureAwait(false) == false) { yield break; } var pocoDataRequest = new PocoDataRequest(dataReader, entityDefinition, this) { FirstColumn = 0 }; var poco = this.pocoFactory.CreatePoco <T>(pocoDataRequest); yield return(poco); } } ////return results; } } else { using (var command = this.CreateCommand(this.Connection, sql, args)) { ////var results = new List<T>(); using (var dataReader = command.ExecuteReader()) { while (true) { if (dataReader.Read() == false) { yield break; } var pocoDataRequest = new PocoDataRequest(dataReader, entityDefinition, this) { FirstColumn = 0 }; var poco = this.pocoFactory.CreatePoco <T>(pocoDataRequest); yield return(poco); } } ////return results; } } }