protected virtual SendResult SendIteration(IList <ITransportPacketInfo> packetInfos, IList <ConfigurationRequestDataItem> configurationsToDownloadInfos, SendIterationContext context)
        {
            var url = this.GetUri(this.GetSendMessageHeaders(context));

            string formDataBoundary = string.Format("----------{0:N}", Guid.NewGuid());
            string contentType      = "multipart/form-data; boundary=" + formDataBoundary;

            // для net40
#if !NETSTANDARD2_0
            try
            {
                var req = WebRequest.Create(url) as HttpWebRequest;
                //req.KeepAlive = true;
                //req.KeepAlive = false;
                req.AllowWriteStreamBuffering = false;
                req.SendChunked = true;
                req.ServicePoint.Expect100Continue = true;
                req.Method      = "POST";
                req.ContentType = contentType;
                //req.ContinueTimeout = xxx;

                TransportSendStats sendededStats = null;
                using (Stream requestStream = req.GetRequestStream())
                {
                    sendededStats = this.WriteMessageToBody(formDataBoundary, requestStream, packetInfos, configurationsToDownloadInfos, context, () => req.HaveResponse);

                    // для тестирования...
                    //var length = 1 * 1024 * 1024 * 1024;
                    //var batch = 1 * 1024 * 1024;
                    //for (int i = 0; i < length; i += batch)
                    //{
                    //    var batchBuffer = new List<byte>();
                    //    for (int j = 0; j < batch; j++)
                    //    {
                    //        batchBuffer.Add(0);
                    //    }
                    //    if (req.HaveResponse)
                    //    {
                    //        break;
                    //    }
                    //    requestStream.Write(batchBuffer.ToArray(), 0, batchBuffer.Count);
                    //}
                }

                try
                {
                    using (var response = req.GetResponse())
                        using (var resStream = response.GetResponseStream())
                            using (var streamReader = new StreamReader(resStream))
                            {
                                // обрабатывает ответ
                                return(this.SaveRequestResults(resStream, sendededStats, context));
                            }
                }
                catch (WebException ex)
                {
                    var wRespStatusCode = ((HttpWebResponse)ex.Response).StatusCode;
                    // кастомная обработка конфигурации (для проблемы когда тело запроса с агента отправляется, хотя в этом нет смысла - лишние данные ходят по сети, это можно исправить)
                    if ((int)wRespStatusCode == 399)
                    {
                        using (var resStream = ((HttpWebResponse)ex.Response).GetResponseStream())
                            using (var streamReader = new StreamReader(resStream))
                            {
                                // обрабатывает ответ
                                return(this.SaveRequestResults(resStream, sendededStats, context));
                            }
                    }
                    if ((int)wRespStatusCode == 429)
                    {
                        var timeout = int.TryParse(ex.Response.Headers.GetValues("Retry-After").FirstOrDefault(), out var retryTm)
                            ? retryTm
                            : Convert.ToInt64(context.TransportSettings.ErrorRetryTimeout.TotalMilliseconds);

                        return(SendResult.Retry(timeout));
                    }

                    // при общих ошибках
                    return(SendResult.Retry(context.TransportSettings.ServerErrorRetryTimeout));
                }
            }
            catch (Exception)
            {
                // при общих ошибках
                return(SendResult.Retry(context.TransportSettings.ErrorRetryTimeout));
            }
#else
            try
            {
                TransportSendStats sendedStats = null;
                var reqContent = new HttpRequestMessage(HttpMethod.Post, url)
                {
                    Content = new WriteToStreamContent((requestStream, ctx) =>
                    {
                        sendedStats = this.WriteMessageToBody(formDataBoundary, requestStream, packetInfos, configurationsToDownloadInfos, context, () => false);

                        // для тестирования...
                        //var length = 1 * 1024 * 1024 * 1024;
                        //var batch = 1 * 1024 * 1024;
                        //for (int i = 0; i < length; i += batch)
                        //{
                        //    var batchBuffer = new List<byte>();
                        //    for (int j = 0; j < batch; j++)
                        //    {
                        //        batchBuffer.Add(0);
                        //    }
                        //    //if (req.HaveResponse)
                        //    //{
                        //    //    break;
                        //    //}
                        //    requestStream.Write(batchBuffer.ToArray(), 0, batchBuffer.Count);
                        //}
                    }),
                };

                // workaround net core 2.2 и тех кто использует библиотеку System.Net.Http под netstandard (с версии 4.2.1.0)
                // https://github.com/dotnet/corefx/blob/v2.1.5/src/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/HttpConnection.cs#L537  (версия 2.1.5 указана для примера)
                // нужно для того чтобы в случаях "отказов" не происходило чтение всего тела запроса, а читались только основные параметры
                reqContent.Headers.ExpectContinue      = true;
                reqContent.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
                //reqContent.Version = new Version(1, 1);
                var result = this.httpClient.SendAsync(reqContent, HttpCompletionOption.ResponseHeadersRead).Result;

                if (result.IsSuccessStatusCode)
                {
                    using (var stream = result.Content.ReadAsStreamAsync().Result)
                    {
                        // обрабатывает ответ
                        return(this.SaveRequestResults(stream, sendedStats, context));
                    };
                }
                else
                {
                    // кастомная обработка конфигурации (для проблемы когда тело запроса с агента отправляется, хотя в этом нет смысла - лишние данные ходят по сети, это можно исправить)
                    if ((int)result.StatusCode == 399)
                    {
                        using (var stream = result.Content.ReadAsStreamAsync().Result)
                        {
                            // обрабатывает ответ
                            return(this.SaveRequestResults(stream, sendedStats, context));
                        };
                    }
                    if ((int)result.StatusCode == 429)
                    {
                        var timeout = int.TryParse(result.Headers.GetValues("Retry-After").FirstOrDefault(), out var retryTm)
                                        ? retryTm
                                        : Convert.ToInt64(context.TransportSettings.ErrorRetryTimeout.TotalMilliseconds);

                        return(SendResult.Retry(timeout));
                    }

                    return(SendResult.Retry(context.TransportSettings.ServerErrorRetryTimeout));
                }
            }
            catch (Exception)
            {
                // при общих ошибках
                return(SendResult.Retry(context.TransportSettings.ErrorRetryTimeout));
            }
#endif
        }
        protected virtual SendResult SaveRequestResults(Stream responseStream, TransportSendStats sendStats, SendIterationContext context)
        {
            bool needRetry       = false;
            bool serverDbChanged = false;
            TransportResponseStats responseStats;

            using (var streamReader = new StreamReader(responseStream))
                using (var jsonReader = new JsonTextReader(streamReader))
                {
                    var jsonSerializer = new JsonSerializer();
                    // сохраним потоки данных конфигураций при десериализации
                    // TODO Доработать configurationsConverter
                    var configurationsConverter = new TransportResponseConfigurationsConverter(this.configurationStore);
                    jsonSerializer
                    .Converters
                    .Add(configurationsConverter);

                    var response = jsonSerializer.Deserialize <TransportResponse>(jsonReader);

                    if (response.ErrorMessage != null)
                    {
                        SendResult.Retry(context.TransportSettings.ErrorRetryTimeout);
                    }

                    // изменеилась статическая конфигурация
                    if (response.StaticConfigData != null)
                    {
                        this.agentInfoService.SetStaticConfig(new TransportStaticConfig
                        {
                            ConfigToken   = response.StaticConfigData.ConfigToken,
                            ConfigVersion = response.StaticConfigData.ConfigVersion,
                        });

                        return(SendResult.Retry(TransportConstants.DefaultUpdateStaticConfigTimeout));
                    }
                    else
                    {
                        // ConfigurationsByteArraysToFilesConverter используется при сохранении конфигураций
                        this.sendStateStore.ConfigurationsUpdated();

                        // изменилась бд системы (например сменили сервера)
                        if (response.DbTokenData != null)
                        {
                            serverDbChanged = true;

                            this.agentInfoService.SetDbToken(new TransportDbTokenData
                            {
                                DbToken = response.DbTokenData.DbToken
                            });

                            //return SendResult.Retry(TransportConstants.DefaultUpdateDbTokenTimeout);
                        }

                        responseStats = new TransportResponseStats()
                        {
                            SendedPacketsStats = sendStats.SendedPacketsStats,
                            // изменилась бд системы
                            TransferedPacketsProcessingResults = response.DbTokenData != null ? new List <TransferedPacketStats>() : sendStats.SendedPacketsStats.Select(x =>
                            {
                                var transferedPacketResponse = response.TransferedPackets.First(p => p.PacketId == x.Key.Identity.PacketId && p.ProviderKey == x.Key.ProviderKey);
                                //
                                x.Value.PreviousPartIdentity = new PacketPartIdentity(transferedPacketResponse.StorageToken, transferedPacketResponse.Id);
                                return(new TransferedPacketStats
                                {
                                    PacketInfo = x.Key,
                                    SendStats = x.Value,
                                    Result = transferedPacketResponse.Result,
                                });
                            }).ToList(),
                        };
                    }
                }

            // сохраняем данные
            var packets = responseStats.TransferedPacketsProcessingResults;

            if (serverDbChanged)
            {
                this.packetManager.RemoveAll();
            }
            else
            {
                foreach (var item in packets)
                {
                    var packet = item.PacketInfo;
                    if (item.Result == PacketProcessingResult.Saved)
                    {
                        this.packetManager.SaveSendStats(packet.ProviderKey, packet.Identity, item.SendStats);
                    }
                    else if (item.Result == PacketProcessingResult.Error)
                    {
                        needRetry = true;
                    }
                    else if (item.Result == PacketProcessingResult.Resend)
                    {
                        // сбрасываем статистику
                        this.packetManager.SaveSendStats(packet.ProviderKey, packet.Identity, new SendStats());
                    }
                    // игнорим эти результаты (если потом появится обработчик на сервере или обновиться агент до нормальной версии и в нём будет другая обработка, предполагаю что следующий набор данных будет доставлен успешно)
                    //else if (item.Result == PacketProcessingResult.Unknown)
                    //{
                    //}
                    //else if (item.Result == PacketProcessingResult.NoProcessor)
                    //{
                    //}
                    else
                    {
                        this.packetManager.SaveSendStats(packet.ProviderKey, packet.Identity, item.SendStats);
                    }
                }
            }

            return(needRetry
                ? SendResult.Retry(context.TransportSettings.ErrorRetryTimeout)
                : SendResult.Success(serverDbChanged, packets, sendStats.IgnoredPackets));
        }
        public void Process()
        {
            if (Interlocked.CompareExchange(ref this.processing, 1, 0) == 1)
            {
                return;
            }

            try
            {
                // TODO так отсылка сообщений идёт неравномерно для разных провайдеров
                var packetInfos = packetManager
                                  .GetTransportProviderInfos()
                                  .SelectMany(x => x.GetPackets())
                                  .OrderBy(x => x.Identity.OrderValue)
                                  .ToList();

                var configurationsToDownloadInfos = this.configurationStore.GetStateItems().Select(ConfigurationRequestExtensions.ToRequestItem).ToList();

                // TODO проверять мин. размер и таймаут (можно ещё проверить что полный размер можно разбить на куски к которым применяется PacketSizeLimits)
                var size = packetInfos.Sum(x => x.Length);
                SendIterationContext context = this.CreateSendIterationConxtext(packetInfos);

                var needSendPackets = size > context.TransportSettings.PacketSizeLimits.Min || this.NeedSendPacketsByTimeout(context);
                // если нужно cкачать конфигурации
                var needUpdateConfigs = this.NeedUpdateConfigs(context);
                if (size > 0 && !needSendPackets)
                {
                    // отмечаем что рассчитан размер пакетов но им не требуется отправка
                    this.sendStateStore.CheckedPacketsSize();
                }

                if (!needSendPackets && !needUpdateConfigs)
                {
                    Interlocked.Exchange(ref this.processing, 0);
                    return;
                }

                SendResult sendResult = null;
                do
                {
                    // отправляем и сохраняем данные по ответу
                    sendResult = this.SendIteration(packetInfos, configurationsToDownloadInfos, context);

                    if (sendResult.TimeoutToNextTry.HasValue)
                    {
                        context.Attempt++;
                        context.FirstFailTimeUtc = context.FirstFailTimeUtc ?? DateTime.UtcNow;
                        Delay(sendResult.TimeoutToNextTry.Value).Wait();
                    }

                    // отправлять старые данные не нужно, дождёмся следующей отправки
                    if (sendResult.ServerDbChanged)
                    {
                        // TODO следует сократить интервал до следующей отправки?
                        return;
                    }

                    // TODO переписать лучше
                    packetInfos = packetInfos
                                  .Where(x => !sendResult.IgnoredPackets.Contains(x) && !sendResult.TransferedPackets.Any(tp => tp.Result == PacketProcessingResult.Saved && tp.SendStats.TransferCompleted && tp.PacketInfo.Identity == x.Identity))
                                  .OrderBy(x => x.Identity.OrderValue)
                                  .ToList();

                    configurationsToDownloadInfos = this.configurationStore.GetStateItems().Select(ConfigurationRequestExtensions.ToRequestItem).ToList();

                    context = this.CreateSendIterationConxtext(packetInfos);

                    // нужно ли обновить конфиг
                    needUpdateConfigs = this.NeedUpdateConfigs(context);
                } while (packetInfos.Any() && (configurationsToDownloadInfos.Any(x => !x.IsCompleted) || needUpdateConfigs));

                // после отправки пробуем обнулить счётчик для упорядочивания пакетов
                this.packetManager.ReInitOrderCounter();
            }
            finally
            {
                Interlocked.Exchange(ref this.processing, 0);
            }
        }