Gress 2.0.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Gress --version 2.0.0
NuGet\Install-Package Gress -Version 2.0.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Gress" Version="2.0.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Gress --version 2.0.0
#r "nuget: Gress, 2.0.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// Install Gress as a Cake Addin
#addin nuget:?package=Gress&version=2.0.0

// Install Gress as a Cake Tool
#tool nuget:?package=Gress&version=2.0.0

Gress

Build Coverage Version Downloads Discord Donate

Project status: active. What does it mean?

Gress is a library that extends the standard IProgress<T> interface with a set of utilities for collecting, transforming, filtering, and muxing progress updates in your code.

💬 If you want to chat, join my Discord server.

Download

📦 NuGet: dotnet add package Gress

Screenshots

demo

Usage

Percentage type

To make progress updates more explicit, Gress provides a universal progress unit -- the Percentage type. Unlike raw numeric values commonly used with IProgress<T>, this type unambiguously represents reported progress as a portion of work that has been completed so far.

An instance of Percentage can be created from either a value or a fraction:

using Gress;

// Mapped from value
var fiftyPercent = Percentage.FromValue(50); // 50%

// Mapped from fractional representation
var twentyPercent = Percentage.FromFraction(0.2); // 20%

Similarly, both value and fraction can be extracted from an initialized Percentage by accessing the corresponding properties:

using Gress;

var fiftyPercent = Percentage.FromValue(50);

var asValue = fiftyPercent.Value; // 50.0 (double)
var asFraction = fiftyPercent.Fraction; // 0.5 (double)

Using IProgress<Percentage> allows an operation to communicate its progress to the caller without relying on any semantic assumptions:

using Gress;

async Task PerformWorkAsync(IProgress<Percentage> progrss)
{
    await Task.Delay(100);
    
    // Half-way done
    progress.Report(Percentage.FromFraction(0.5));
    
    await Task.Delay(100);
    
    // Finished
    progress.Report(Percentage.FromFraction(1));
}

// ...

var progress = new Progress<Percentage>(p => Console.WriteLine(p));
await PerformWorkAsync(progress);

// Console output:
// 50,0%
// 100,0%

However, you may need to interface with external methods that already specify their own progress handler signatures. In such cases, you can use some of the provided extensions to convert a percentage-based handler into a handler of an appropriate type:

using Gress;

async Task FooAsync(IProgress<double> progress) { /* ... */ }
async Task BarAsync(IProgress<int> progress) { /* ... */ }

var progress = new Progress<Percentage>(p => /* ... */);

await FooAsync(progress.ToDoubleBased());
await BarAsync(progress.ToInt32Based());

Likewise, there are also extensions that facilitate conversion in the other direction, which can be useful for preserving backwards-compatibility in existing methods:

using Gress;

async Task FooAsync(IProgress<double> progress)
{
    var actualProgress = progress.ToPercentageBased();
    
    // Reports 0.5 on the original progress handler
    actualProgress.Report(Percentage.FromFraction(0.5));
}

async Task BarAsync(IProgress<int> progress)
{
    var actualProgress = progress.ToPercentageBased();
    
    // Reports 50 on the original progress handler
    actualProgress.Report(Percentage.FromFraction(0.5));
}

💡 When converting between percentage-based and double-based handlers, percentages are mapped using their fractional form by default. To override this behavior and map by value instead, use ToDoubleBased(false) and ToPercentageBased(false).

💡 For more complex conversion scenarios, consider using the WithTransform(...) method.

Terminal handlers

Every progress reporting chain ultimately ends with a terminal handler, which usually relays the information to the user or stores it somewhere else. To simplify some of the most common scenarios, Gress comes with two terminal handlers built in.

Progress container

This handler simply represents an object with a single property, whose value is updated every time a new progress update is reported. It also implements INotifyPropertyChanged, which allows the property to be bound with XAML-based UI frameworks.

Here's a very basic example of how you would use it in a typical WPF application:

public class MainViewModel
{
    public ProgressContainer<Percentage> Progress { get; } = new();
    
    public IRelayCommand PerformWorkCommand { get; }
    
    public MainViewModel() =>
        PerformWorkCommand = new RelayCommand(PerformWork);
        
    public async void PerformWork()
    {
        for (var i = 1; i <= 100; i++)
        {
            await Task.Delay(200);
            Progress.Report(Percentage.FromValue(i));
        }
    }
}
<Window
    x:Class="MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    d:DataContext="{d:DesignInstance Type=MainViewModel}">
    <StackPanel>
        <Button
            Margin="32" 
            Content="Execute"
            Command="{Binding PerformWorkCommand}" />

        <ProgressBar
            Margin="32"
            Height="10"
            Minimum="0"
            Maximum="100"
            Value="{Binding Progress.Current.Value, Mode=OneWay}" />
    </StackPanel>
</Window>
Progress collector

This handler works by storing every reported progress update in an internal collection that can be accessed later. It's primarily designed for testing scenarios, where it can be useful to verify whether a specific routine reports its progress correctly.

You can use it like so:

[Fact]
public async Task My_method_reports_progress_correctly()
{
    // Arrange
    var progress = new ProgressCollector<Percentage>();
    var worker = new Worker();
    
    // Act
    await worker.PerformWorkAsync(progress);
    
    // Assert
    progress.GetValues().Should().OnlyHaveUniqueItems(); // e.g.: no redundant progress updates
}

Composing handlers

Existing progress handlers can be composed into more complex handlers using some of the extension methods that Gress offers. These can be used to easily apply transformations, inject filtering logic, or merge multiple handlers together.

Transformation

You can use WithTransform(...) to create a handler that transforms all reported progress values into a different form:

using Gress;

enum Status { Started, HalfWay, Completed }

var progress = new Progress<Percentage>(p => /* ... */);

// Transform into a progress handler that accepts an enum value and maps
// it into a value of the original type
var progressTransformed = progress.WithTransform((Status s) => s switch
{
    Status.Completed => Percentage.FromValue(100), // 100%
    Status.HalfWay => Percentage.FromValue(50), // 50%
    _ => Percentage.FromValue(0) // 0%
});

// This effectively reports 50% on the original handler
progressTransformed.Report(Status.HalfWay);

A simpler overload of the above method can also be used when transforming between values of the same type:

using Gress;

var progress = new Progress<int>(p => /* ... */);

var progressTransformed = progress.WithTransform(p => 5 * p);

// This effectively reports 50 on the original handler
progressTransformed.Report(10);

💡 Method WithTransform(...) bears some resemblance to LINQ's Select(...), however they are not completely equivalent. The main difference is that the flow of data in IProgress<T> is inverse to that of IEnumerable<T>, which means that the transformations in WithTransform(...) are applied in the opposite direction.

Filtering

You can use WithFilter(...) to create a handler that drops progress updates that don't satisfy a predicate:

using Gress;

var progress = new Progress<Percentage>(p => /* ... */);

// Filter out values below 10%
var progressFiltered = progress.WithFilter(p => p.Fraction >= 0.1);

// ✖
progressFiltered.Report(Percentage.FromFraction(0.05));

// ✓
progressFiltered.Report(Percentage.FromFraction(0.25));
Deduplication

You can use WithDeduplication(...) to create a handler that filters out consecutive progress reports with the same value:

using Gress;

var progress = new Progress<Percentage>(p => /* ... */);

var progressDeduplicated = progress.WithDeduplication();

progressDeduplicated.Report(Percentage.FromFraction(0.1)); // ✓
progressDeduplicated.Report(Percentage.FromFraction(0.3)); // ✓
progressDeduplicated.Report(Percentage.FromFraction(0.3)); // ✖
progressDeduplicated.Report(Percentage.FromFraction(0.3)); // ✖
progressDeduplicated.Report(Percentage.FromFraction(0.5)); // ✓
Merging

You can use Merge(...) to combine multiple progress handlers into one:

using Gress;

var progress1 = new Progress<Percentage>(p => /* ... */);
var progress2 = new Progress<Percentage>(p => /* ... */);

var progressMerged = progress1.Merge(progress2);

// Reports 50% on both progress handlers
progressMerged.Report(Percentage.FromFraction(0.5));

This method can also be called on collections:

using Gress;

var progresses = new[]
{
    new Progress<Percentage>(p => /* ... */),
    new Progress<Percentage>(p => /* ... */),
    new Progress<Percentage>(p => /* ... */),
    new Progress<Percentage>(p => /* ... */)
};

var progressMerged = progresses.Merge();

// Reports 50% on all progress handlers
progressMerged.Report(Percentage.FromFraction(0.5));

Muxing

Muxing allows a single handler to aggregate progress reports from multiple input sources. This is useful when you want to track progress of an operation that itself encapsulates other operations.

To do this, call CreateMuxer() on a progress handler and then create an input corresponding to each operation:

using Gress;

var progress = new Progress<Percentage>(p => /* ... */);

var muxer = progress.CreateMuxer();
var progressSub1 = muxer.CreateInput();
var progressSub2 = muxer.CreateInput();
var progressSub3 = muxer.CreateInput();

When progress is reported on any of the individual inputs, its value is aggregated with values reported on other inputs, and then routed to the original target handler. The sample below illustrates this process:

// ...

progressSub1.Report(Percentage.FromFraction(0.5));

// Input 1 ->  50%
// Input 2 ->   0%
// Input 3 ->   0%
// Total   -> ~17%

progressSub1.Report(Percentage.FromFraction(1));
progressSub2.Report(Percentage.FromFraction(0.75));

// Input 1 -> 100%
// Input 2 ->  75%
// Input 3 ->   0%
// Total   -> ~58%

progressSub2.Report(Percentage.FromFraction(1));
progressSub3.Report(Percentage.FromFraction(0.9));

// Input 1 -> 100%
// Input 2 -> 100%
// Input 3 ->  90%
// Total   -> ~97%

progressSub3.Report(Percentage.FromFraction(1));

// Input 1 -> 100%
// Input 2 -> 100%
// Input 3 -> 100%
// Total   -> 100%

Muxer inputs, being progress handlers themselves, can be muxed even further to create a progress hierarchy:

using Gress;

async Task PerformWorkAsync(IProgress<Percentage> progress)
{
    for (var i = 1; i <= 100; i++)
    {
        await Task.Delay(200);
        progress.Report(Percentage.FromValue(i));
    }
}

async Task FooAsync(IProgress<Percentage> progress)
{
    var muxer = progress.CreateMuxer();
    var progressSub1 = muxer.CreateInput();
    var progressSub2 = muxer.CreateInput();
    
    await Task.WhenAll(
        PerformWorkAsync(progressSub1),
        PerformWorkAsync(progressSub2)
    );
}

async Task BarAsync(IProgress<Percentage> progress)
{
    var muxer = progress.CreateMuxer();
    var progressSub1 = muxer.CreateInput();
    var progressSub2 = muxer.CreateInput();
    var progressSub3 = muxer.CreateInput();
    
    await Task.WhenAll(
        FooAsync(progressSub1),
        FooAsync(progressSub2),
        FooAsync(progressSub3)
    );
}

⚠️ Muxing is only available on percentage-based handlers because it relies on their ability to represent progress as a fraction of all work. If needed, you can convert certain numeric handlers into percentage-based handlers using the ToPercentageBased() extension method.

With custom weight

A muxer input may be assigned a custom weight, which determines the priority of a particular input in relation to others. Progress reported on a handler with higher weight influences the aggregated progress to a greater degree and vice versa.

To create a weighted muxer input, pass the corresponding value to the CreateInput(...) method:

using Gress;

var progress = new Progress<Percentage>(p => /* ... */);

var muxer = progress.CreateMuxer();
var progressSub1 = muxer.CreateInput(1);
var progressSub2 = muxer.CreateInput(4);

// Weight split:
// Input 1 -> 20% of total
// Input 2 -> 80% of total

progressSub1.Report(Percentage.FromFraction(0.9));
progressSub2.Report(Percentage.FromFraction(0.3));

// Input 1 -> 90%
// Input 2 -> 30%
// Total   -> 42%
With auto-reset

In some cases, you may need to report progress on an infinite workflow where new operations start and complete in a continuous fashion. This can be achieved by using an auto-reset muxer.

Inputs to an auto-reset muxer implement the ICompletableProgress<T> interface and are capable of reporting completion after all of the underlying work is finished. Once all connected handlers report completion, they are disconnected and the muxer resets back to the initial state:

using Gress;
using Gress.Completable;

var progress = new Progress<Percentage>(p => /* ... */);

var muxer = progress.CreateMuxer().WithAutoReset();
var progressSub1 = muxer.CreateInput();
var progressSub2 = muxer.CreateInput();

progressSub1.Report(Percentage.FromFraction(0.3));
progressSub2.Report(Percentage.FromFraction(0.9));

// Input 1 -> 30%
// Input 2 -> 90%
// Total   -> 60%

progressSub1.Report(Percentage.FromFraction(1));
progressSub1.ReportCompletion();

// Input 1 -> 100% (completed)
// Input 2 -> 90%
// Total   -> 95%

progressSub2.Report(Percentage.FromFraction(1));
progressSub2.ReportCompletion();

// All inputs disconnected
// Total   -> 0%

var progressSub3 = muxer.CreateInput();
progressSub3.Report(Percentage.FromFraction(0.5));

// Input 3 -> 50%
// Total   -> 50%

💡 You can wrap an instance of ICompletableProgress<T> in a disposable container by calling ToDisposable(). This allows you to encapsulate the handler in a using (...) block, which ensures that it reports completion regardless of potential exceptions.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • .NETStandard 2.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories (4)

Showing the top 4 popular GitHub repositories that depend on Gress:

Repository Stars
Tyrrrz/DiscordChatExporter
Exports Discord chat logs to a file
Tyrrrz/YoutubeDownloader
Downloads videos and playlists from YouTube
Tyrrrz/YoutubeExplode
Abstraction layer over YouTube's internal API
Tyrrrz/OsuHelper
Beatmap suggester for osu!
Version Downloads Last updated
2.1.1 14,983 4/27/2023
2.0.1 19,992 2/15/2022
2.0.0 462 2/13/2022
1.2.0 9,803 4/19/2020
1.1.1 3,182 6/15/2019
1.1.0 594 5/13/2019
1.0.2 1,655 2/9/2019
1.0.1 659 2/4/2019
1.0.0 656 2/4/2019