Exemple #1
0
        /// <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;
        }
Exemple #2
0
        /// <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);
        }
Exemple #3
0
        /// <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);
        }
Exemple #4
0
        /// <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);
        }