< Summary

Information
Class: MyNet.UI.Navigation.NavigationService
Assembly: MyNet.UI
File(s): https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.UI/Navigation/NavigationService.cs
Tag: 323_28699572109
Line coverage
95%
Covered lines: 81
Uncovered lines: 4
Coverable lines: 85
Total lines: 259
Line coverage: 95.2%
Branch coverage
83%
Covered branches: 25
Total branches: 30
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_CanGoBack()100%11100%
get_CanGoForward()100%11100%
NavigateToAsync()100%11100%
GoBackAsync()50%22100%
GoForwardAsync()50%22100%
ResetAsync()100%11100%
NavigateInternalAsync()100%8883.33%
ExecuteGuardsAsync()100%44100%
BuildMiddlewarePipeline(...)100%22100%
UpdateJournal(...)70%1010100%
RaiseStateChanged()100%22100%
Dispose()100%11100%

File(s)

https://raw.githubusercontent.com/sandre58/MyNet/85372080fe102cd9ee155ceab49ae000e7f66103/src/MyNet.UI/Navigation/NavigationService.cs

#LineLine coverage
 1// -----------------------------------------------------------------------
 2// <copyright file="NavigationService.cs" company="Stéphane ANDRE">
 3// Copyright (c) Stéphane ANDRE. All rights reserved.
 4// </copyright>
 5// -----------------------------------------------------------------------
 6
 7using System;
 8using System.Collections.Generic;
 9using System.Linq;
 10using System.Threading;
 11using System.Threading.Tasks;
 12using MyNet.UI.Navigation.Models;
 13
 14namespace MyNet.UI.Navigation;
 15
 16public sealed class NavigationService(
 17    INavigationJournal journal,
 18    INavigationLifecycle lifecycle,
 19    IEnumerable<INavigationGuard> guards,
 20    IEnumerable<INavigationMiddleware> middlewares)
 21    : INavigationService, IDisposable
 22{
 4223    private readonly IReadOnlyList<INavigationMiddleware> _middlewares = [.. middlewares];
 4224    private readonly SemaphoreSlim _navigationLock = new(1, 1);
 25
 26    /// <inheritdoc />
 27    public event EventHandler<NavigationStateChangedEventArgs>? StateChanged;
 28
 29    /// <inheritdoc />
 30    public NavigationContext? CurrentContext { get; private set; }
 31
 32    /// <inheritdoc />
 2133    public bool CanGoBack => journal.BackStack.Any();
 34
 35    /// <inheritdoc />
 1836    public bool CanGoForward => journal.ForwardStack.Any();
 37
 38    #region Public API
 39
 40    /// <inheritdoc />
 41    public async Task<NavigationResult> NavigateToAsync(INavigationPage page, INavigationParameters? parameters = null, 
 42    {
 5143        ArgumentNullException.ThrowIfNull(page);
 44
 5145        await _navigationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 46
 47        try
 48        {
 5149            return await NavigateInternalAsync(page, NavigationMode.Normal, parameters, cancellationToken).ConfigureAwai
 50        }
 51        finally
 52        {
 5153            _navigationLock.Release();
 54        }
 5155    }
 56
 57    /// <inheritdoc />
 58    public async Task<NavigationResult> GoBackAsync(CancellationToken cancellationToken = default)
 59    {
 1260        await _navigationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 61
 62        try
 63        {
 1264            var context = journal.PeekBack();
 65
 1266            return context is null
 1267                ? new(NavigationStatus.NotFound)
 1268                : await NavigateInternalAsync(context.To, NavigationMode.Back, context.Parameters, cancellationToken).Co
 69        }
 70        finally
 71        {
 1272            _navigationLock.Release();
 73        }
 1274    }
 75
 76    /// <inheritdoc />
 77    public async Task<NavigationResult> GoForwardAsync(CancellationToken cancellationToken = default)
 78    {
 979        await _navigationLock.WaitAsync(cancellationToken).ConfigureAwait(false);
 80
 81        try
 82        {
 983            var context = journal.PeekForward();
 84
 985            return context is null
 986                ? new(NavigationStatus.NotFound)
 987                : await NavigateInternalAsync(context.To, NavigationMode.Forward, context.Parameters, cancellationToken)
 88        }
 89        finally
 90        {
 991            _navigationLock.Release();
 92        }
 993    }
 94
 95    /// <inheritdoc />
 96    public async Task ResetAsync()
 97    {
 698        await _navigationLock.WaitAsync().ConfigureAwait(false);
 99
 100        try
 101        {
 6102            CurrentContext = null;
 6103            journal.Clear();
 6104            RaiseStateChanged();
 6105        }
 106        finally
 107        {
 6108            _navigationLock.Release();
 109        }
 6110    }
 111
 112    #endregion
 113
 114    #region Core Navigation Pipeline
 115
 116    private async Task<NavigationResult> NavigateInternalAsync(
 117        INavigationPage to,
 118        NavigationMode mode,
 119        INavigationParameters? parameters,
 120        CancellationToken cancellationToken)
 121    {
 72122        ArgumentNullException.ThrowIfNull(to);
 123
 72124        var previousContext = CurrentContext;
 72125        var context = NavigationContext.Create(previousContext?.To, to, mode, parameters);
 126
 127        try
 128        {
 129            // 1. Guards (decision layer)
 72130            var guardResult = await ExecuteGuardsAsync(previousContext, context, cancellationToken).ConfigureAwait(false
 72131            if (!guardResult)
 132            {
 6133                return new(NavigationStatus.Cancelled, "Navigation blocked by guard.");
 134            }
 135
 136            // 2. Lifecycle: leaving the current page (source only)
 66137            if (previousContext is not null)
 138            {
 33139                await lifecycle.OnNavigatingFromAsync(context, cancellationToken).ConfigureAwait(false);
 140            }
 141
 66142            cancellationToken.ThrowIfCancellationRequested();
 143
 144            // 3. Middleware pipeline (includes core execution; no target lifecycle yet)
 66145            var pipeline = BuildMiddlewarePipeline(previousContext, context, core, cancellationToken);
 146
 66147            var result = await pipeline().ConfigureAwait(false);
 148
 66149            if (result.Status != NavigationStatus.Succeeded)
 3150                return result;
 151
 152            // 4. Lifecycle: entering the target page (after a successful pipeline)
 63153            await lifecycle.OnNavigatingToAsync(context, cancellationToken).ConfigureAwait(false);
 154
 155            // 5. Commit navigation atomically
 63156            UpdateJournal(mode, previousContext);
 63157            CurrentContext = context;
 158
 159            // 6. Lifecycle: navigation committed
 63160            await lifecycle.OnNavigatedAsync(context, cancellationToken).ConfigureAwait(false);
 161
 63162            RaiseStateChanged();
 163
 63164            return result;
 165        }
 0166        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 167        {
 0168            return new(NavigationStatus.Cancelled);
 169        }
 0170        catch (Exception exception)
 171        {
 0172            return new(NavigationStatus.Failed, exception.Message, exception);
 173        }
 174
 175        static Task<NavigationResult> core() => Task.FromResult(new NavigationResult(NavigationStatus.Succeeded));
 72176    }
 177
 178    #endregion
 179
 180    #region Guards
 181
 182    private async Task<bool> ExecuteGuardsAsync(NavigationContext? previousContext, NavigationContext context, Cancellat
 183    {
 162184        foreach (var guard in guards)
 185        {
 12186            var canNavigate = await guard.CanNavigateAsync(previousContext, context, cancellationToken).ConfigureAwait(f
 187
 12188            if (!canNavigate)
 6189                return false;
 190        }
 191
 66192        return true;
 72193    }
 194
 195    #endregion
 196
 197    #region Middleware Pipeline
 198
 199    private Func<Task<NavigationResult>> BuildMiddlewarePipeline(
 200        NavigationContext? from,
 201        NavigationContext to,
 202        Func<Task<NavigationResult>> core,
 203        CancellationToken cancellationToken)
 204    {
 66205        var pipeline = core;
 206
 150207        foreach (var middleware in _middlewares.Reverse())
 208        {
 9209            var next = pipeline;
 210
 9211            pipeline = () => middleware.InvokeAsync(
 9212                from,
 9213                to,
 9214                next,
 9215                cancellationToken);
 216        }
 217
 66218        return pipeline;
 219    }
 220
 221    #endregion
 222
 223    #region Journal
 224
 225    private void UpdateJournal(NavigationMode mode, NavigationContext? previousContext)
 226    {
 227        switch (mode)
 228        {
 229            case NavigationMode.Normal:
 45230                if (previousContext is not null)
 15231                    journal.PushBack(previousContext);
 45232                journal.ClearForward();
 45233                break;
 234
 235            case NavigationMode.Back:
 9236                _ = journal.PopBack();
 9237                if (previousContext is not null)
 9238                    journal.PushForward(previousContext);
 9239                break;
 240
 241            case NavigationMode.Forward:
 9242                _ = journal.PopForward();
 9243                if (previousContext is not null)
 9244                    journal.PushBack(previousContext);
 245                break;
 246        }
 9247    }
 248
 249    private void RaiseStateChanged()
 69250        => StateChanged?.Invoke(
 69251            this,
 69252            new(CurrentContext, CanGoBack, CanGoForward));
 253
 254    /// <inheritdoc />
 21255    public void Dispose() => _navigationLock.Dispose();
 256
 257    #endregion
 258}
 259