IVSoftware.Portable.Threading 1.0.0

dotnet add package IVSoftware.Portable.Threading --version 1.0.0                
NuGet\Install-Package IVSoftware.Portable.Threading -Version 1.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="IVSoftware.Portable.Threading" Version="1.0.0" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add IVSoftware.Portable.Threading --version 1.0.0                
#r "nuget: IVSoftware.Portable.Threading, 1.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 IVSoftware.Portable.Threading as a Cake Addin
#addin nuget:?package=IVSoftware.Portable.Threading&version=1.0.0

// Install IVSoftware.Portable.Threading as a Cake Tool
#tool nuget:?package=IVSoftware.Portable.Threading&version=1.0.0                

This package contains a helper class that I made up to "test the untestable" or "await the unawaitable", whether or not it occurs in a UI environment. For me, this is simple and it works real good.

An example of a real life scenario might be an app where the user makes a new record and adds it to the local database. As a result of this change, the app posts (but does not await) an update the Google Drive API. Meanwhile, there is a free-running background process running that polls for cloud changes at a time interval (which for testing is set small, like 15 seconds or so). At some point, Google Drive is going to respond with a loopback of this change but we don't know when, and since the polling loop runs forever we obviously can't await that task.


Helper class with static `Awaited`` event
namespace IVSoftware.Portable.Core.Threading
{
    public partial class Extensions
    {
        public static void OnAwaited(this object sender, AwaitedEventArgs e)
        {
            Awaited?.Invoke(sender, e);
        }
        public static event AwaitedEventHandler Awaited;
    }
    // Because of the [CallerMemberName] attribute, the `Caller`
    // property will hold the name of the calling method.
    public class AwaitedEventArgs : EventArgs
    {
        public AwaitedEventArgs([CallerMemberName] string caller = null)
        {
            Caller = caller;
        }
        public AwaitedEventArgs(object args, [CallerMemberName] string caller = null)
        {
            Caller = caller;
            Args = args;
        }
        public string Caller { get; }
        public object Args { get; }
    }
    public delegate void AwaitedEventHandler(object sender, AwaitedEventArgs e);
}

In the app, add an OnAwaited hook in the free-running loop.

In general, to make our code testable, we generously sprinkle it with OnAwaited statements like the one shown here in the polling routine. When we temporarily subscribe to this static event in a TestMethod, it's automatically identified with the sender this and the name of the calling method (because of the [CallerMemberName]) attribute. You can also put anything you want in the args property, in anticipation of what you might need in the TestMethod to know whether it worked or not.

using IVSoftware.Portable.Core.Threading;
class GoogleDriveSync
{
    DriveService DriveService { get; }    
    string CurrentPageToken { get; set; }
    SemaphoreSlim _isBusyPolling = new SemaphoreSlim(0, 1);
    internal async Task PollForChanges()
    {
        if (_isBusyPolling.Wait(0))
        {
            try
            {
                do
                {
                    await localExecuteChangePollingCycle();
                } while (!localIsCancelPolling());

                #region L o c a l F x
                async Task localExecuteChangePollingCycle()
                {
                    string pageToken = CurrentPageToken;
                    ChangesResource.ListRequest request = DriveService.Changes.List(pageToken);
                    request.Fields = "nextPageToken, newStartPageToken, changes(fileId, removed, file(id, name, trashed, createdTime, modifiedTime, mimeType, appProperties))";
                    if (await request.ExecuteAsync() is ChangeList response && response.Changes.Any())
                    {
                        // Group by extension, from the least to the most recent change.
                        var byExt =
                            response
                            .Changes
                            .Where(_ => _.Removed != true)
                            .GroupBy(_ => Path.GetExtension(_.File.Name))
                            .ToDictionary(
                                _ => _.Key,
                                _ => _.OrderBy(change => change.File.ModifiedTimeDateTimeOffset).ToArray());
                        // Process changes
                        .
                        .
                        .                        
                        
                        // Fire static event when ready. In this case, we'll  populate
                        // the args with a dictionary specific to this method
                        if (RuntimeContext > RuntimeContext.Production)
                        {
                            this.OnAwaited(new AwaitedEventArgs(args: new Dictionary<string, object>
                            {
                                {"Changes", response.Changes }
                            }));
                        }
                    }
                }
                bool localIsCancelPolling() => // Check for external cancel requests
                #endregion L o c a l F x
            }
            catch (Exception ex)
            {
                CancelPolling();
                Debug.Fail(ex.Message);
            }
        }
    }
    .
    .
    .
}

In the async TestMethod, make a temporary listener for the static event

Now, in our asynchronous TestMethod, we can await the OnAwaited hook even though there's nothing inherently awaitable about the polling loop itself.

[TestMethod]
public async Task TestGoogleDriveChangeLoopback()
{
    SemaphoreSlim awaiter = new SemaphoreSlim(0,1);

    try
    {
        // Start listening for the event
        Extensions.Awaited += localOnAwaited;
        // The database write itself is awaitable.
        var success = await Program.InsertAsync(new Record("TestItem"));
        Assert.AreEqual(1, success, "Expecting successful local database write.");

        // Meanwhile, program logic has begun a non-awaitable process
        // that will 'eventually' result in the polling loop detecting
        // the change in the cloud. We want to know exactly when that happens.
        try
        {
            await awaiter.WaitAsync(timeout: TimeSpan.FromMinutes(1));
        }
        catch (TaskCanceledException)
        {
            Assert.Fail("Google Drive is expected to respond in well under a minute.");
        }
    }
    finally
    {
        // Ensure there's something to release, 
        // even if timed out or exception.
        awaiter.Wait(0);    
        // This makes it safe to release without violating `maxCount` of awaiter.
        awaiter.Release();
        // CRITICAL to unconditionally unsubscribe
        // from the static method when done.
        Extensions.Awaited -= localOnAwaited;
    }

    #region L o c a l M e t h o d s
    void localOnAwaited(object sender, AwaitedEventArgs e)
    {
        switch (e.Caller)
        {
            case nameof(GoogleDriveSync.PollForChanges):
                if(
                    e.Args is Dictionary<string, object> args &&
                    args is IList<Change> changes)
                {
                    Assert.IsTrue(changes.Any(), "Expecting a non-empty list of Change objects");
                    if (changes.Any())
                    {
                        // Inspect the changes to see whether they
                        // contain the item we're looking for and
                        // release the awaiter when found.
                        awaiter.Release();
                    }
                }
                break;
        }
    }
    #endregion L o c a l M e t h o d s
}
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.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last updated
1.0.0 3 9/9/2024