/// <summary> /// Clones everything in <paramref name="original"/>. /// </summary> public MappingInfo(MappingInfo original, IEnumerable <PropertyMappingInfo> simpleProps, IEnumerable <MappingInfo> collectionProps) { if (original is null) { throw new ArgumentNullException(nameof(original)); } MetadataForSave = original.MetadataForSave; SimpleProperties = simpleProps ?? throw new ArgumentNullException(nameof(simpleProps)); CollectionProperties = collectionProps ?? throw new ArgumentNullException(nameof(collectionProps)); CreateEntity = original.CreateEntity; GetEntitiesForRead = original.GetEntitiesForRead; GetOrCreateListForSave = original.GetOrCreateListForSave; Display = original.Display; Select = original.Select; }
/// <summary> /// Inspects the data and mapping and loads all related entities from the API, that are referenced by custom use keys like Code and Name. /// The purpose is to use the Ids of these entities in the constructed Entity objects that are being imported /// </summary> private async Task <RelatedEntities> LoadRelatedEntities(IEnumerable <string[]> dataWithoutHeader, MappingInfo mapping, ImportErrors errors) { if (errors is null) { throw new ArgumentNullException(nameof(errors)); } // Each set of foreign keys will result in an API query to retrieve the corresponding Ids for FK hydration // So we group foreign keys by the target type, the target definition Id and the key property (e.g. Code or Name) var queryInfos = new List <(Type navType, int?navDefId, PropertyMetadata keyPropMeta, HashSet <object> keysSet)>(); foreach (var g in mapping.GetForeignKeys().Where(p => p.NotUsingIdAsKey) .GroupBy(fk => (fk.TargetType, fk.TargetDefId, fk.KeyPropertyMetadata))) { var(navType, navDefId, keyPropMetadata) = g.Key; HashSet <object> keysSet = new HashSet <object>(); foreach (var fkMapping in g) { int rowNumber = 2; foreach (var row in dataWithoutHeader) { string stringKey = row[fkMapping.Index]; if (string.IsNullOrEmpty(stringKey)) { continue; } switch (fkMapping.KeyType) { case KeyType.String: keysSet.Add(stringKey); break; case KeyType.Int: if (int.TryParse(stringKey, out int intKey)) { keysSet.Add(intKey); } else if (!errors.AddImportError(rowNumber, fkMapping.ColumnNumber, _localizer[$"Error_TheValue0IsNotAValidInteger", stringKey])) { // This means the validation errors are at maximum capacity, pointless to keep going. return(null); } break; default: throw new InvalidOperationException("Bug: Only int and string IDs are supported"); } rowNumber++; } } if (keysSet.Any()) { // Actual API calls are delayed till the end in case there are any errors queryInfos.Add((navType, navDefId, keyPropMetadata, keysSet)); } } if (!errors.IsValid) { return(null); } using var _1 = _instrumentation.Block("Loading Related Entities"); using var _2 = _instrumentation.Disable(); var result = new RelatedEntities(); if (queryInfos.Any()) { // Load all related entities in parallel // If they're the same type we load them in sequence because it will be the same controller instance and it might cause issues await Task.WhenAll(queryInfos.GroupBy(e => e.navType).Select(async g => { var navType = g.Key; foreach (var queryInfo in g) { var(_, navDefId, keyPropMeta, keysSet) = queryInfo; // Deconstruct the queryInfo var service = _sp.FactWithIdServiceByEntityType(navType.Name, navDefId); var keyPropDesc = keyPropMeta.Descriptor; var keyPropName = keyPropDesc.Name; var args = new SelectExpandArguments { Select = keyPropName }; var(data, _) = await service.GetByPropertyValues(keyPropName, keysSet, args, cancellation: default); var grouped = data.GroupBy(e => keyPropDesc.GetValue(e)).ToDictionary(g => g.Key, g => (IEnumerable <EntityWithKey>)g); result.TryAdd((navType, navDefId, keyPropName), grouped); } // The code above might override the definition Id of the service calling it, here we fix that _sp.FactWithIdServiceByEntityType(navType.Name, mapping.MetadataForSave.DefinitionId); })); } return(result); }
/// <summary> /// Takes a <see cref="List{T}"/> of <see cref="Entity"/> objects and composes them into raw data /// using the specifications in the <see cref="MappingInfo"/>. This raw data can then be exported as a CSV file. /// This function is the opposite of <see cref="DataParser.ParseAsync{TEntityForSave}(IEnumerable{string[]}, MappingInfo, ImportErrors)"/>. /// </summary> public IEnumerable <string[]> Compose <TEntityForSave>(List <TEntityForSave> entities, MappingInfo mapping) where TEntityForSave : EntityWithKey { var result = new List <string[]>(entities.Count); // it will be at least that long, so might as well int columnCount = mapping.ColumnCount(); // Recursive function int numberOfRows = 0; foreach (var entity in entities) { numberOfRows += ComposeDataRowsFromEntity(entity, mapping, result, numberOfRows, columnCount); } return(result); }
/// <summary> /// Takes an <see cref="IEnumerable{T}"/> of string arrays representing the raw data of the imported file. /// Then using the specifications in the <see cref="MappingInfo"/> it translates this raw data into an /// <see cref="IEnumerable{T}"/> of entities. Any problems will be added in the <see cref="ImportErrors"/> dictionary. /// This function is the opposite of <see cref="DataComposer.Compose{TEntityForSave}(List{TEntityForSave}, MappingInfo)"/>. /// </summary> public async Task <IEnumerable <TEntityForSave> > ParseAsync <TEntityForSave>(IEnumerable <string[]> dataWithoutHeader, MappingInfo mapping, ImportErrors errors) where TEntityForSave : Entity { // Load related entities from API var relatedEntities = await LoadRelatedEntities(dataWithoutHeader, mapping, errors); if (!errors.IsValid) { return(null); } // Clear all cached entities and lists, and create the root list mapping.ClearEntitiesAndLists(); mapping.List = new List <TEntityForSave>(); // This root list will contain the final result // Set some values on the root mapping infor for handling self referencing FKs mapping.IsRoot = true; int matchesIndex = 0; foreach (var prop in mapping.SimpleProperties.OfType <ForeignKeyMappingInfo>().Where(p => p.IsSelfReferencing)) { prop.EntityMetadataMatchesIndex = matchesIndex++; } int rowNumber = 2; foreach (var dataRow in dataWithoutHeader) // Foreach row { bool keepGoing = ParseRow(dataRow, rowNumber, mapping, relatedEntities, errors, matchesIndex); // Recursive function if (!keepGoing) { // This means the errors collection is full, no need to keep going break; } rowNumber++; } // Grab the result from the root mapping var result = mapping.List.Cast <TEntityForSave>(); // Hydrate self referencing FKs if any, these properties are not hydrated in ParseRow() // Since they requires all the imported entities to be created and all their other // properties already hydrated HydrateSelfReferencingForeignKeys(result, mapping, errors); // Return the result return(result); }