private async Task UpdateExistingItemsAsync()

in src/Epam.GraphQL/Loaders/MutableLoader.cs [223:438]


        private async Task UpdateExistingItemsAsync(IResolveFieldContext context, IProfiler profiler, IEnumerable<SaveResultItem<TEntity, TId>> itemsToUpdate)
#pragma warning restore CA1506
#pragma warning restore CA1502
        {
            var graphqlContext = (GraphQLContext<TExecutionContext>?)context.UserContext["ctx"];
            Guards.AssertIfNull(graphqlContext);

            if (itemsToUpdate.Any())
            {
                using (profiler.Step($"Update"))
                {
                    List<IGrouping<TId, TEntity>> itemsFound;
                    var errors = new List<string>();
                    IEnumerable<TEntity> items;

                    using (profiler.Step($"Checks"))
                    {
                        var batcher = context.GetBatcher();
                        var queryExecuter = context.GetQueryExecuter();
                        var hooksExecuter = ObjectGraphTypeConfigurator.ProxyAccessor.CreateHooksExecuter(context);

                        var itemsFoundTasks = itemsToUpdate
                            .Select(item => BatchHelpers
                                .GetLoaderQueryFactory<MutableLoader<TEntity, TId, TExecutionContext>, TEntity, TId, TExecutionContext>(
                                    () => "Checks:All",
                                    this,
                                    IdExpression)(profiler, queryExecuter, null, context.GetUserContext<TExecutionContext>()) // TBD hooksExecuter is null here
                                .LoadAsync(GetId(item.Payload)));

                        itemsFound = new List<IGrouping<TId, TEntity>>();
                        foreach (var itemFoundTask in itemsFoundTasks)
                        {
                            var itemFoundTaskResult = await itemFoundTask.GetResultAsync().ConfigureAwait(false);
                            if (itemFoundTaskResult != null && itemFoundTaskResult.Any())
                            {
                                itemsFound.Add(itemFoundTaskResult);
                            }
                        }

                        var itemsNotFound = itemsToUpdate.Where(item => !itemsFound.Any(i => i.Key.Equals(item.Id)));
                        var itemsHavingMoreThanOneItem = itemsToUpdate.Where(item => itemsFound.Any(i => i.Key.Equals(item.Id) && i.Count() > 1));

                        errors.AddRange(itemsNotFound.Select(item => $"Cannot update entity: Entity was not found (type: {typeof(TEntity).HumanizedName()}, id: {GetId(item.Payload)})."));
                        errors.AddRange(itemsHavingMoreThanOneItem.Select(item => $"Cannot update entity: More than one entity was found (type: {typeof(TEntity).HumanizedName()}: id = {GetId(item.Payload)})."));

                        if (errors.Any())
                        {
                            throw new ExecutionError(string.Join("\n\r", errors));
                        }
                    }

                    items = itemsFound.Select(group => group.Single());
                    using (profiler.Step($"CustomChecks"))
                    {
                        var resolvedEntities = itemsToUpdate.Select(nextItemToUpdate =>
                        {
                            // TODO Optimization of proxy creation
                            var prevEntity = items.Single(i => EqualityComparer<TId>.Default.Equals(GetId(i), nextItemToUpdate.Id));
                            var prevEntityProxy = prevEntity; // TODO transform by calling InputObjectGraphTypeConfigurator.ProxyAccessor.CreateSelectorExpression(context.UserContext, nextItemToUpdate.Properties.Keys).Compile()
                            var fieldsAndNextValues = nextItemToUpdate.Properties
                                .Select(kv => (InputObjectGraphTypeConfigurator.FindFieldByName(kv.Key), kv.Value));

                            List<(IField<TEntity, TExecutionContext> Field, ValueTask<object?> Result, object? Value)>
                                resolveFieldTasks = new();

                            foreach (var prop in nextItemToUpdate.Properties)
                            {
                                var field = InputObjectGraphTypeConfigurator.FindFieldByName(prop.Key);
                                var result = field.Resolver.ResolveAsync(new ResolveFieldContext
                                {
                                    // TODO Don't create another IResolveFieldContext; reuse existing one
                                    UserContext = context.UserContext,
                                    Source = prevEntityProxy,
                                });

                                resolveFieldTasks.Add((field, result, prop.Value));
                            }

                            return (nextItemToUpdate, prevEntity, resolveFieldTasks);
                        }).ToArray();

                        var tasksForWait = resolvedEntities
                            .Select(resolvedEntity => resolvedEntity.resolveFieldTasks.Select(t => t.Result).ToArray());

                        var resolvedTasks = new List<object?[]>();
                        foreach (var taskForWait in tasksForWait)
                        {
                            var resolved = new object?[taskForWait.Length];

                            for (int i = 0; i < taskForWait.Length; i++)
                            {
                                var result = await taskForWait[i].ConfigureAwait(false);
                                resolved[i] = result is IDataLoaderResult dataLoaderResult
                                    ? await dataLoaderResult.GetResultAsync().ConfigureAwait(false)
                                    : result;
                            }

                            resolvedTasks.Add(resolved);
                        }

                        var canEditTasks = new List<(string, TEntity, IDataLoaderResult<(bool, string)>)>();

                        for (int i = 0; i < resolvedEntities.Length; i++)
                        {
                            var nextEntity = resolvedEntities[i].nextItemToUpdate;
                            var prevEntity = resolvedEntities[i].prevEntity;

                            for (int j = 0; j < resolvedEntities[i].resolveFieldTasks.Count; j++)
                            {
                                var (field, _, next) = resolvedEntities[i].resolveFieldTasks[j];
                                var prevValue = resolvedTasks[i][j];
                                var nextValue = next;
                                if (nextValue != null)
                                {
                                    nextValue = nextValue.GetPropertyValue(field.FieldType);
                                }

                                var shouldCheck = (nextValue == null && prevValue != null)
                                    || (nextValue != null && prevValue == null)
                                    || (prevValue != null && !prevValue.Equals(nextValue));

                                if (shouldCheck)
                                {
                                    var fieldChange = FieldChange.Create(field.FieldType, context.GetUserContext<TExecutionContext>(), prevEntity, prevValue, nextValue);
                                    var canEditTask = field.CanEdit(context).LoadAsync(fieldChange);
                                    canEditTasks.Add((field.Name, nextEntity.Payload, canEditTask));
                                }
                            }

                            prevEntity.CopyProperties(
                                nextEntity.Payload,
                                resolvedEntities[i].resolveFieldTasks.Select(fv => fv.Field)
                                    .OfType<IExpressionFieldConfiguration<TEntity, TExecutionContext>>()
                                    .Where(field => field.PropertyInfo != null && field.EditSettings.OnWrite == null && field.EditSettings.OnWriteAsync == null)
                                    .Select(field => field.PropertyInfo!));
                        }

                        var resolvedCanEdiTasks = new List<(bool, string)>();
                        foreach (var canEdiTask in canEditTasks)
                        {
                            var canEditTaskResult = await canEdiTask.Item3.GetResultAsync().ConfigureAwait(false);
                            resolvedCanEdiTasks.Add(canEditTaskResult);
                        }

                        for (var i = 0; i < canEditTasks.Count; i++)
                        {
                            var (fieldName, entity, _) = canEditTasks[i];
                            var (canEdit, disableReason) = resolvedCanEdiTasks[i];
                            if (!canEdit)
                            {
                                errors.Add($"Cannot update entity: Cannot change field `{fieldName}` of entity (type: {typeof(TEntity).HumanizedName()}, id: {GetId(entity)}): {disableReason}");
                            }
                        }

                        if (errors.Any())
                        {
                            throw new ExecutionError(string.Join("\n\r", errors));
                        }
                    }

                    using (profiler.Step($"CanSave"))
                    {
                        var itemsToCheck = new List<TEntity>();
                        foreach (var item in itemsToUpdate)
                        {
                            var payload = items.Single(i => EqualityComparer<TId>.Default.Equals(GetId(i), item.Id));

                            payload.CopyProperties(
                                item.Payload,
                                item.Properties.Keys
                                    .Select(fieldName => InputObjectGraphTypeConfigurator.FindFieldByName(fieldName))
                                    .OfType<IExpressionFieldConfiguration<TEntity, TExecutionContext>>()
                                    .Where(field => field.PropertyInfo != null && field.EditSettings.OnWrite == null && field.EditSettings.OnWriteAsync == null)
                                    .Select(field => field.PropertyInfo!));
                            itemsToCheck.Add(payload);
                        }

                        var canUpdateItemTasks = itemsToCheck.Select(item => CanSaveAsync(graphqlContext, item, false));
                        var canUpdate = true;
                        foreach (var canUpdateItemTask in canUpdateItemTasks)
                        {
                            if (!await canUpdateItemTask.ConfigureAwait(false))
                            {
                                canUpdate = false;
                            }
                        }

                        if (!canUpdate)
                        {
                            throw new ExecutionError($"Cannot update entity (type: {typeof(TEntity).HumanizedName()}): Unauthorized.");
                        }
                    }

                    using (profiler.Step($"Save"))
                    {
                        foreach (var item in itemsToUpdate)
                        {
                            var itemToUpdate = items.Single(i => EqualityComparer<TId>.Default.Equals(GetId(i), item.Id));

                            try
                            {
                                await BeforeUpdateAsync(context.GetUserContext<TExecutionContext>(), itemToUpdate).ConfigureAwait(false);
                            }
                            catch (Exception e)
                            {
                                throw new ExecutionError($"{GetType().HumanizedName()}.{nameof(BeforeUpdate)} has thrown exception:\r\n{e.GetType()}\r\n{e.Message}", e);
                            }

                            await CustomSave(context, item, itemToUpdate).ConfigureAwait(false);

                            item.Payload = itemToUpdate;
                        }
                    }
                }
            }
        }