1 //------------------------------------------------------------------------------ 2 // <copyright file="ColumnMapFactory.cs" company="Microsoft"> 3 // Copyright (c) Microsoft Corporation. All rights reserved. 4 // </copyright> 5 // @owner Microsoft 6 // @backupOwner Microsoft 7 //------------------------------------------------------------------------------ 8 9 using System.Collections.Generic; 10 using System.Data.Common; 11 using System.Data.Common.Utils; 12 using System.Data.Entity; 13 using System.Data.Mapping; 14 using System.Data.Metadata.Edm; 15 using System.Data.Objects.ELinq; 16 using System.Diagnostics; 17 using System.Linq; 18 using System.Linq.Expressions; 19 using System.Reflection; 20 21 namespace System.Data.Query.InternalTrees 22 { 23 /// <summary> 24 /// Factory methods for prescriptive column map patterns (includes default 25 /// column maps for -- soon to be -- public materializer services and function 26 /// mappings). 27 /// </summary> 28 internal static class ColumnMapFactory 29 { 30 /// <summary> 31 /// Creates a column map for the given reader and function mapping. 32 /// </summary> CreateFunctionImportStructuralTypeColumnMap(DbDataReader storeDataReader, FunctionImportMappingNonComposable mapping, int resultSetIndex, EntitySet entitySet, StructuralType baseStructuralType)33 internal static CollectionColumnMap CreateFunctionImportStructuralTypeColumnMap(DbDataReader storeDataReader, FunctionImportMappingNonComposable mapping, int resultSetIndex, EntitySet entitySet, StructuralType baseStructuralType) 34 { 35 FunctionImportStructuralTypeMappingKB resultMapping = mapping.GetResultMapping(resultSetIndex); 36 Debug.Assert(resultMapping != null); 37 if (resultMapping.NormalizedEntityTypeMappings.Count == 0) // no explicit mapping; use default non-polymorphic reader 38 { 39 // if there is no mapping, create default mapping to root entity type or complex type 40 Debug.Assert(!baseStructuralType.Abstract, "mapping loader must verify abstract types have explicit mapping"); 41 42 return CreateColumnMapFromReaderAndType(storeDataReader, baseStructuralType, entitySet, resultMapping.ReturnTypeColumnsRenameMapping); 43 } 44 45 // the section below deals with the polymorphic entity type mapping for return type 46 EntityType baseEntityType = baseStructuralType as EntityType; 47 Debug.Assert(null != baseEntityType, "We should have entity type here"); 48 49 // Generate column maps for all discriminators 50 ScalarColumnMap[] discriminatorColumns = CreateDiscriminatorColumnMaps(storeDataReader, mapping, resultSetIndex); 51 52 // Generate default maps for all mapped entity types 53 var mappedEntityTypes = new HashSet<EntityType>(resultMapping.MappedEntityTypes); 54 mappedEntityTypes.Add(baseEntityType); // make sure the base type is represented 55 Dictionary<EntityType, TypedColumnMap> typeChoices = new Dictionary<EntityType, TypedColumnMap>(mappedEntityTypes.Count); 56 ColumnMap[] baseTypeColumnMaps = null; 57 foreach (EntityType entityType in mappedEntityTypes) 58 { 59 ColumnMap[] propertyColumnMaps = GetColumnMapsForType(storeDataReader, entityType, resultMapping.ReturnTypeColumnsRenameMapping); 60 EntityColumnMap entityColumnMap = CreateEntityTypeElementColumnMap(storeDataReader, entityType, entitySet, propertyColumnMaps, resultMapping.ReturnTypeColumnsRenameMapping); 61 if (!entityType.Abstract) 62 { 63 typeChoices.Add(entityType, entityColumnMap); 64 } 65 if (entityType == baseStructuralType) 66 { 67 baseTypeColumnMaps = propertyColumnMaps; 68 } 69 } 70 71 // NOTE: We don't have a null sentinel here, because the stored proc won't 72 // return one anyway; we'll just presume the data's always there. 73 MultipleDiscriminatorPolymorphicColumnMap polymorphicMap = new MultipleDiscriminatorPolymorphicColumnMap(TypeUsage.Create(baseStructuralType), baseStructuralType.Name, baseTypeColumnMaps, discriminatorColumns, typeChoices, (object[] discriminatorValues) => mapping.Discriminate(discriminatorValues, resultSetIndex)); 74 CollectionColumnMap collection = new SimpleCollectionColumnMap(baseStructuralType.GetCollectionType().TypeUsage, baseStructuralType.Name, polymorphicMap, null, null); 75 return collection; 76 } 77 78 /// <summary> 79 /// Build the collectionColumnMap from a store datareader, a type and an entitySet. 80 /// </summary> 81 /// <param name="storeDataReader"></param> 82 /// <param name="edmType"></param> 83 /// <param name="entitySet"></param> 84 /// <returns></returns> CreateColumnMapFromReaderAndType(DbDataReader storeDataReader, EdmType edmType, EntitySet entitySet, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList)85 internal static CollectionColumnMap CreateColumnMapFromReaderAndType(DbDataReader storeDataReader, EdmType edmType, EntitySet entitySet, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList) 86 { 87 Debug.Assert(Helper.IsEntityType(edmType) || null == entitySet, "The specified non-null EntitySet is incompatible with the EDM type specified."); 88 89 // Next, build the ColumnMap directly from the edmType and entitySet provided. 90 ColumnMap[] propertyColumnMaps = GetColumnMapsForType(storeDataReader, edmType, renameList); 91 ColumnMap elementColumnMap = null; 92 93 // NOTE: We don't have a null sentinel here, because the stored proc won't 94 // return one anyway; we'll just presume the data's always there. 95 if (Helper.IsRowType(edmType)) 96 { 97 elementColumnMap = new RecordColumnMap(TypeUsage.Create(edmType), edmType.Name, propertyColumnMaps, null); 98 } 99 else if (Helper.IsComplexType(edmType)) 100 { 101 elementColumnMap = new ComplexTypeColumnMap(TypeUsage.Create(edmType), edmType.Name, propertyColumnMaps, null); 102 } 103 else if (Helper.IsScalarType(edmType)) 104 { 105 if (storeDataReader.FieldCount != 1) 106 { 107 throw EntityUtil.CommandExecutionDataReaderFieldCountForScalarType(); 108 } 109 elementColumnMap = new ScalarColumnMap(TypeUsage.Create(edmType), edmType.Name, 0, 0); 110 } 111 else if (Helper.IsEntityType(edmType)) 112 { 113 elementColumnMap = CreateEntityTypeElementColumnMap(storeDataReader, edmType, entitySet, propertyColumnMaps, null/*renameList*/); 114 } 115 else 116 { 117 Debug.Assert(false, "unexpected edmType?"); 118 } 119 CollectionColumnMap collection = new SimpleCollectionColumnMap(edmType.GetCollectionType().TypeUsage, edmType.Name, elementColumnMap, null, null); 120 return collection; 121 } 122 123 /// <summary> 124 /// Requires: a public type with a public, default constructor. Returns a column map initializing the type 125 /// and all properties of the type with a public setter taking a primitive type and having a corresponding 126 /// column in the reader. 127 /// </summary> CreateColumnMapFromReaderAndClrType(DbDataReader reader, Type type, MetadataWorkspace workspace)128 internal static CollectionColumnMap CreateColumnMapFromReaderAndClrType(DbDataReader reader, Type type, MetadataWorkspace workspace) 129 { 130 Debug.Assert(null != reader); 131 Debug.Assert(null != type); 132 Debug.Assert(null != workspace); 133 134 // we require a default constructor 135 ConstructorInfo constructor = type.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, 136 null, Type.EmptyTypes, null); 137 if (type.IsAbstract || (null == constructor && !type.IsValueType)) 138 { 139 throw EntityUtil.InvalidOperation( 140 Strings.ObjectContext_InvalidTypeForStoreQuery(type)); 141 } 142 143 // build a LINQ expression used by result assembly to create results 144 var memberInfo = new List<Tuple<MemberAssignment, int, EdmProperty>>(); 145 foreach (PropertyInfo prop in type.GetProperties(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)) 146 { 147 // for enums unwrap the type if nullable 148 var propertyUnderlyingType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType; 149 Type propType = propertyUnderlyingType.IsEnum ? propertyUnderlyingType.GetEnumUnderlyingType() : prop.PropertyType; 150 151 EdmType modelType; 152 int ordinal; 153 if (TryGetColumnOrdinalFromReader(reader, prop.Name, out ordinal) && 154 MetadataHelper.TryDetermineCSpaceModelType(propType, workspace, out modelType) && 155 (Helper.IsScalarType(modelType)) && 156 prop.CanWrite && prop.GetIndexParameters().Length == 0 && null != prop.GetSetMethod(/* nonPublic */true)) 157 { 158 memberInfo.Add(Tuple.Create( 159 Expression.Bind(prop, Expression.Parameter(prop.PropertyType, "placeholder")), 160 ordinal, 161 new EdmProperty(prop.Name, TypeUsage.Create(modelType)))); 162 } 163 } 164 // initialize members in the order in which they appear in the reader 165 MemberInfo[] members = new MemberInfo[memberInfo.Count]; 166 MemberBinding[] memberBindings = new MemberBinding[memberInfo.Count]; 167 ColumnMap[] propertyMaps = new ColumnMap[memberInfo.Count]; 168 EdmProperty[] modelProperties = new EdmProperty[memberInfo.Count]; 169 int i = 0; 170 foreach (var memberGroup in memberInfo.GroupBy(tuple => tuple.Item2).OrderBy(tuple => tuple.Key)) 171 { 172 // make sure that a single column isn't contributing to multiple properties 173 if (memberGroup.Count() != 1) 174 { 175 throw EntityUtil.InvalidOperation(Strings.ObjectContext_TwoPropertiesMappedToSameColumn( 176 reader.GetName(memberGroup.Key), 177 String.Join(", ", memberGroup.Select(tuple => tuple.Item3.Name).ToArray()))); 178 } 179 180 var member = memberGroup.Single(); 181 MemberAssignment assignment = member.Item1; 182 int ordinal = member.Item2; 183 EdmProperty modelProp = member.Item3; 184 185 members[i] = assignment.Member; 186 memberBindings[i] = assignment; 187 propertyMaps[i] = new ScalarColumnMap(modelProp.TypeUsage, modelProp.Name, 0, ordinal); 188 modelProperties[i] = modelProp; 189 i++; 190 } 191 NewExpression newExpr = null == constructor ? Expression.New(type) : Expression.New(constructor); 192 MemberInitExpression init = Expression.MemberInit(newExpr, memberBindings); 193 InitializerMetadata initMetadata = InitializerMetadata.CreateProjectionInitializer( 194 (EdmItemCollection)workspace.GetItemCollection(DataSpace.CSpace), init, members); 195 196 // column map (a collection of rows with InitializerMetadata markup) 197 RowType rowType = new RowType(modelProperties, initMetadata); 198 RecordColumnMap rowMap = new RecordColumnMap(TypeUsage.Create(rowType), 199 "DefaultTypeProjection", propertyMaps, null); 200 CollectionColumnMap collectionMap = new SimpleCollectionColumnMap(rowType.GetCollectionType().TypeUsage, 201 rowType.Name, rowMap, null, null); 202 return collectionMap; 203 } 204 205 /// <summary> 206 /// Build the entityColumnMap from a store datareader, a type and an entitySet and 207 /// a list ofproperties. 208 /// </summary> 209 /// <param name="storeDataReader"></param> 210 /// <param name="edmType"></param> 211 /// <param name="entitySet"></param> 212 /// <param name="propertyColumnMaps"></param> 213 /// <returns></returns> CreateEntityTypeElementColumnMap( DbDataReader storeDataReader, EdmType edmType, EntitySet entitySet, ColumnMap[] propertyColumnMaps, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList)214 private static EntityColumnMap CreateEntityTypeElementColumnMap( 215 DbDataReader storeDataReader, EdmType edmType, EntitySet entitySet, 216 ColumnMap[] propertyColumnMaps, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList) 217 { 218 EntityType entityType = (EntityType)edmType; 219 220 // The tricky part here is 221 // that the KeyColumns list must point at the same ColumnMap(s) that 222 // the properties list points to, so we build a quick array of 223 // ColumnMap(s) that are indexed by their ordinal; then we can walk 224 // the list of keyMembers, and find the ordinal in the reader, and 225 // pick the same ColumnMap for it. 226 227 // Build the ordinal -> ColumnMap index 228 ColumnMap[] ordinalToColumnMap = new ColumnMap[storeDataReader.FieldCount]; 229 230 foreach (ColumnMap propertyColumnMap in propertyColumnMaps) 231 { 232 int ordinal = ((ScalarColumnMap)propertyColumnMap).ColumnPos; 233 ordinalToColumnMap[ordinal] = propertyColumnMap; 234 } 235 236 // Now build the list of KeyColumns; 237 IList<EdmMember> keyMembers = entityType.KeyMembers; 238 SimpleColumnMap[] keyColumns = new SimpleColumnMap[keyMembers.Count]; 239 240 int keyMemberIndex = 0; 241 foreach (EdmMember keyMember in keyMembers) 242 { 243 int keyOrdinal = GetMemberOrdinalFromReader(storeDataReader, keyMember, edmType, renameList); 244 245 Debug.Assert(keyOrdinal >= 0, "keyMember for entity is not found by name in the data reader?"); 246 247 ColumnMap keyColumnMap = ordinalToColumnMap[keyOrdinal]; 248 249 Debug.Assert(null != keyColumnMap, "keyMember for entity isn't in properties collection for the entity?"); 250 keyColumns[keyMemberIndex] = (SimpleColumnMap)keyColumnMap; 251 keyMemberIndex++; 252 } 253 254 SimpleEntityIdentity entityIdentity = new SimpleEntityIdentity(entitySet, keyColumns); 255 256 EntityColumnMap result = new EntityColumnMap(TypeUsage.Create(edmType), edmType.Name, propertyColumnMaps, entityIdentity); 257 return result; 258 } 259 260 /// <summary> 261 /// For a given edmType, build an array of scalarColumnMaps that map to the columns 262 /// in the store datareader provided. Note that we're hooking things up by name, not 263 /// by ordinal position. 264 /// </summary> 265 /// <param name="storeDataReader"></param> 266 /// <param name="edmType"></param> 267 /// <returns></returns> GetColumnMapsForType(DbDataReader storeDataReader, EdmType edmType, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList)268 private static ColumnMap[] GetColumnMapsForType(DbDataReader storeDataReader, EdmType edmType, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList) 269 { 270 // First get the list of properties; NOTE: we need to hook up the column by name, 271 // not by position. 272 IBaseList<EdmMember> members = TypeHelpers.GetAllStructuralMembers(edmType); 273 ColumnMap[] propertyColumnMaps = new ColumnMap[members.Count]; 274 275 int index = 0; 276 foreach (EdmMember member in members) 277 { 278 if (!Helper.IsScalarType(member.TypeUsage.EdmType)) 279 { 280 throw EntityUtil.InvalidOperation(Strings.ADP_InvalidDataReaderUnableToMaterializeNonScalarType(member.Name, member.TypeUsage.EdmType.FullName)); 281 } 282 283 int ordinal = GetMemberOrdinalFromReader(storeDataReader, member, edmType, renameList); 284 285 propertyColumnMaps[index] = new ScalarColumnMap(member.TypeUsage, member.Name, 0, ordinal); 286 index++; 287 } 288 return propertyColumnMaps; 289 } 290 CreateDiscriminatorColumnMaps(DbDataReader storeDataReader, FunctionImportMappingNonComposable mapping, int resultIndex)291 private static ScalarColumnMap[] CreateDiscriminatorColumnMaps(DbDataReader storeDataReader, FunctionImportMappingNonComposable mapping, int resultIndex) 292 { 293 // choose an arbitrary type for discriminator columns -- the type is not 294 // actually statically known 295 EdmType discriminatorType = 296 MetadataItem.EdmProviderManifest.GetPrimitiveType(PrimitiveTypeKind.String); 297 TypeUsage discriminatorTypeUsage = 298 TypeUsage.Create(discriminatorType); 299 300 IList<string> discriminatorColumnNames = mapping.GetDiscriminatorColumns(resultIndex); 301 ScalarColumnMap[] discriminatorColumns = new ScalarColumnMap[discriminatorColumnNames.Count]; 302 for (int i = 0; i < discriminatorColumns.Length; i++) 303 { 304 string columnName = discriminatorColumnNames[i]; 305 ScalarColumnMap columnMap = new ScalarColumnMap(discriminatorTypeUsage, columnName, 0, 306 GetDiscriminatorOrdinalFromReader(storeDataReader, columnName, mapping.FunctionImport)); 307 discriminatorColumns[i] = columnMap; 308 } 309 return discriminatorColumns; 310 } 311 312 /// <summary> 313 /// Given a store datareader and a member of an edmType, find the column ordinal 314 /// in the datareader with the name of the member. 315 /// </summary> 316 /// <param name="storeDataReader"></param> 317 /// <param name="member"></param> 318 /// <returns></returns> GetMemberOrdinalFromReader(DbDataReader storeDataReader, EdmMember member, EdmType currentType, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList)319 private static int GetMemberOrdinalFromReader(DbDataReader storeDataReader, EdmMember member, EdmType currentType, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList) 320 { 321 int result; 322 string memberName = GetRenameForMember(member, currentType, renameList); 323 324 if (!TryGetColumnOrdinalFromReader(storeDataReader, memberName, out result)) 325 { 326 throw EntityUtil.CommandExecutionDataReaderMissingColumnForType(member, currentType); 327 } 328 return result; 329 } 330 GetRenameForMember(EdmMember member, EdmType currentType, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList)331 private static string GetRenameForMember(EdmMember member, EdmType currentType, Dictionary<string, FunctionImportReturnTypeStructuralTypeColumnRenameMapping> renameList) 332 { 333 // if list is null, 334 // or no rename mapping at all, 335 // or partial rename and the member is not specified by the renaming 336 // then we return the original member.Name 337 // otherwise we return the mapped one 338 return renameList == null || renameList.Count == 0 || !renameList.Any(m => m.Key == member.Name) ? member.Name : renameList[member.Name].GetRename(currentType); 339 } 340 341 /// <summary> 342 /// Given a store datareader, a column name, find the column ordinal 343 /// in the datareader with the name of the column. 344 /// 345 /// We only have the functionImport provided to include it in the exception 346 /// message. 347 /// </summary> 348 /// <param name="storeDataReader"></param> 349 /// <param name="columnName"></param> 350 /// <param name="functionImport"></param> 351 /// <returns></returns> GetDiscriminatorOrdinalFromReader(DbDataReader storeDataReader, string columnName, EdmFunction functionImport)352 private static int GetDiscriminatorOrdinalFromReader(DbDataReader storeDataReader, string columnName, EdmFunction functionImport) 353 { 354 int result; 355 if (!TryGetColumnOrdinalFromReader(storeDataReader, columnName, out result)) 356 { 357 throw EntityUtil.CommandExecutionDataReaderMissinDiscriminatorColumn(columnName, functionImport); 358 } 359 return result; 360 } 361 362 /// <summary> 363 /// Given a store datareader and a column name, try to find the column ordinal 364 /// in the datareader with the name of the column. 365 /// </summary> 366 /// <param name="storeDataReader"></param> 367 /// <param name="columnName"></param> 368 /// <param name="ordinal"></param> 369 /// <returns>true if found, false otherwise.</returns> TryGetColumnOrdinalFromReader(DbDataReader storeDataReader, string columnName, out int ordinal)370 private static bool TryGetColumnOrdinalFromReader(DbDataReader storeDataReader, string columnName, out int ordinal) 371 { 372 if (0 == storeDataReader.FieldCount) 373 { 374 // If there are no fields, there can't be a match (this check avoids 375 // an InvalidOperationException on the call to GetOrdinal) 376 ordinal = default(int); 377 return false; 378 } 379 380 // Wrap ordinal lookup for the member so that we can throw a nice exception. 381 try 382 { 383 ordinal = storeDataReader.GetOrdinal(columnName); 384 return true; 385 } 386 catch (IndexOutOfRangeException) 387 { 388 // No column matching the column name found 389 ordinal = default(int); 390 return false; 391 } 392 } 393 } 394 } 395