public async Task <List <HistoryValueElement> > GetComponentStatusValues(ComponentStatusFilter filter, CancellationToken cancellationToken)
        {
            var result = new List <HistoryValueElement>();

            await _lock.EnterAsync(cancellationToken).ConfigureAwait(false);

            try
            {
                var rootPath = BuildComponentStatusPath(filter.ComponentUid, filter.StatusUid);
                var dayPaths = BuildDayPaths(filter.RangeStart, filter.RangeEnd);

                foreach (var dayPath in dayPaths)
                {
                    var path = Path.Combine(rootPath, dayPath.Path, ValuesFilename);
                    if (!File.Exists(path))
                    {
                        continue;
                    }

                    using (var fileStream = File.OpenRead(path))
                    {
                        var valueStream = new HistoryValuesStream(fileStream);

                        HistoryValueElement currentElement = null;

                        while (await valueStream.MoveNextAsync())
                        {
                            if (valueStream.CurrentToken is BeginToken beginToken)
                            {
                                currentElement = new HistoryValueElement
                                {
                                    Begin = new DateTime(
                                        dayPath.Year, dayPath.Month,
                                        dayPath.Day,
                                        beginToken.Value.Hours,
                                        beginToken.Value.Minutes,
                                        beginToken.Value.Seconds,
                                        beginToken.Value.Milliseconds,
                                        DateTimeKind.Utc)
                                };
                            }
                            else if (valueStream.CurrentToken is ValueToken valueToken)
                            {
                                currentElement.Value = valueToken.Value;
                            }
                            else if (valueStream.CurrentToken is EndToken endToken)
                            {
                                currentElement.End = new DateTime(
                                    dayPath.Year, dayPath.Month,
                                    dayPath.Day,
                                    endToken.Value.Hours,
                                    endToken.Value.Minutes,
                                    endToken.Value.Seconds,
                                    endToken.Value.Milliseconds,
                                    DateTimeKind.Utc);

                                result.Add(currentElement);
                            }
                        }
                    }
                }
            }
            finally
            {
                _lock.Exit();
            }

            return(result);
        }
        public async Task Update(HistoryUpdate update, CancellationToken cancellationToken)
        {
            if (update is null)
            {
                throw new ArgumentNullException(nameof(update));
            }

            await _lock.EnterAsync(cancellationToken).ConfigureAwait(false);

            try
            {
                var path = Path.Combine(
                    _storageService.DataPath,
                    "History",
                    update.Path,
                    BuildDayPath(update.Timestamp));

                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }

                path = Path.Combine(path, ValuesFilename);

                using (var valuesStream = new HistoryValuesStream(new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite)))
                {
                    valuesStream.SeekEnd();

                    var createNewValue = true;

                    if (await valuesStream.MovePreviousAsync(cancellationToken).ConfigureAwait(false))
                    {
                        var endToken       = (EndToken)valuesStream.CurrentToken;
                        var valueIsExpired = update.Timestamp.TimeOfDay - endToken.Value > ComponentStatusOutdatedTimeout;

                        if (!valueIsExpired)
                        {
                            await valuesStream.MovePreviousAsync(cancellationToken).ConfigureAwait(false);

                            var valueToken = (ValueToken)valuesStream.CurrentToken;
                            await valuesStream.MoveNextAsync().ConfigureAwait(false); // Move back to end token.

                            if (string.Equals(valueToken.Value, update.Value, StringComparison.Ordinal))
                            {
                                createNewValue = false;
                            }

                            // The end date is moved to the same value (-1 ms) as the new beginning value to fill small gaps.
                            // The 1 ms is removed to avoid wrong comparisons when begin is exactly the same as end (which is the correct value?).
                            var newEndTimestamp = update.Timestamp.TimeOfDay.Subtract(TimeSpan.FromMilliseconds(1));
                            await valuesStream.WriteTokenAsync(new EndToken(newEndTimestamp), cancellationToken).ConfigureAwait(false);

                            await valuesStream.MoveNextAsync().ConfigureAwait(false);
                        }
                    }

                    if (createNewValue)
                    {
                        await valuesStream.WriteTokenAsync(new BeginToken(update.Timestamp.TimeOfDay), cancellationToken).ConfigureAwait(false);

                        await valuesStream.WriteTokenAsync(new ValueToken(update.Value), cancellationToken).ConfigureAwait(false);

                        await valuesStream.WriteTokenAsync(new EndToken(update.Timestamp.TimeOfDay), cancellationToken).ConfigureAwait(false);
                    }
                }
            }
            finally
            {
                _lock.Exit();
            }
        }
        public async Task <List <HistoryValueElement> > Read(HistoryReadOperation readOperation, CancellationToken cancellationToken)
        {
            if (readOperation is null)
            {
                throw new ArgumentNullException(nameof(readOperation));
            }

            var result = new List <HistoryValueElement>();

            var dayPaths = BuildDayPaths(readOperation.RangeStart, readOperation.RangeEnd);

            await _lock.EnterAsync(cancellationToken).ConfigureAwait(false);

            try
            {
                foreach (var dayPath in dayPaths)
                {
                    var path = Path.Combine(readOperation.Path, dayPath.Path, ValuesFilename);
                    if (!File.Exists(path))
                    {
                        continue;
                    }

                    using (var fileStream = File.OpenRead(path))
                    {
                        using (var valueStream = new HistoryValuesStream(fileStream))
                        {
                            HistoryValueElement currentElement = null;

                            while (await valueStream.MoveNextAsync().ConfigureAwait(false))
                            {
                                if (valueStream.CurrentToken is BeginToken beginToken)
                                {
                                    currentElement = new HistoryValueElement
                                    {
                                        Begin = new DateTime(
                                            dayPath.Year,
                                            dayPath.Month,
                                            dayPath.Day,
                                            beginToken.Value.Hours,
                                            beginToken.Value.Minutes,
                                            beginToken.Value.Seconds,
                                            beginToken.Value.Milliseconds,
                                            DateTimeKind.Utc)
                                    };
                                }
                                else if (valueStream.CurrentToken is ValueToken valueToken)
                                {
                                    currentElement.Value = valueToken.Value;
                                }
                                else if (valueStream.CurrentToken is EndToken endToken)
                                {
                                    currentElement.End = new DateTime(
                                        dayPath.Year,
                                        dayPath.Month,
                                        dayPath.Day,
                                        endToken.Value.Hours,
                                        endToken.Value.Minutes,
                                        endToken.Value.Seconds,
                                        endToken.Value.Milliseconds,
                                        DateTimeKind.Utc);

                                    result.Add(currentElement);
                                }
                            }
                        }
                    }
                }
            }
            finally
            {
                _lock.Exit();
            }

            return(result);
        }