Skip to content

Commit

Permalink
Initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
doomchild committed Apr 18, 2022
1 parent 7ab5539 commit ef26f74
Show file tree
Hide file tree
Showing 21 changed files with 1,504 additions and 2 deletions.
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
root = true

[*.{cs,md}]
indent_style=spaces
indent_size=2
encoding=utf-8
25 changes: 25 additions & 0 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
name: Publish Release

on:
push:
branches:
- main
- master

jobs:
publish-release:
# ubuntu-latest provides many dependencies.
# See: https://github.com/actions/virtual-environments/blob/master/images/linux/Ubuntu1804-README.md
runs-on: ubuntu-latest

steps:
- name: Checkout latest commit
uses: actions/checkout@v2
- name: Install CICEE
run: dotnet tool install -g cicee || dotnet tool update -g cicee
- name: Execute publish script - Publish project artifacts
run: cicee exec -c ci/bin/publish.sh
env:
NUGET_API_KEY: ${{secrets.NUGET_API_KEY}}
NUGET_SOURCE: ${{secrets.NUGET_SOURCE}}
RELEASE_ENVIRONMENT: true
27 changes: 27 additions & 0 deletions .github/workflows/validate-project.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Validate Project

on: [pull_request]

jobs:
validate-project:
# ubuntu-latest provides many dependencies.
# See: https://github.com/actions/virtual-environments/blob/main/images/linux/Ubuntu2004-README.md
runs-on: ubuntu-latest

steps:
- name: Checkout latest commit
uses: actions/checkout@v2
- name: Install CICEE
run: dotnet tool install -g cicee || dotnet tool update -g cicee
- name: Execute verification script - Validate source
run: cicee exec -c ci/bin/validate.sh
env:
NUGET_API_KEY: ${{secrets.NUGET_API_KEY}}
NUGET_SOURCE: ${{secrets.NUGET_SOURCE}}
RELEASE_ENVIRONMENT: false
- name: Execute compose script - Perform dry-run composition
run: cicee exec -c ci/bin/compose.sh
env:
NUGET_API_KEY: ${{secrets.NUGET_API_KEY}}
NUGET_SOURCE: ${{secrets.NUGET_SOURCE}}
RELEASE_ENVIRONMENT: false
136 changes: 136 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.

# User-specific files
*.suo
*.user
*.sln.docstates
.vs/

# Build results

[Dd]ebug/
[Rr]elease/
x64/
[Bb]in/
[Oo]bj/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.log
*.svclog
*.scc

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
*.cachefile

# Visual Studio profiler
*.psess
*.vsp
*.vspx

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# Click-Once directory
publish/

# Publish Web Output
*.Publish.xml
*.pubxml
*.azurePubxml

# NuGet Packages Directory
## TODO: If you have NuGet Package Restore enabled, uncomment the next line
packages/
## TODO: If the tool you use requires repositories.config, also uncomment the next line
!packages/repositories.config

# Windows Azure Build Output
csx/
*.build.csdef

# Windows Store app package directory
AppPackages/

# Others
sql/
*.Cache
ClientBin/
[Ss]tyle[Cc]op.*
![Ss]tyle[Cc]op.targets
~$*
*~
*.dbmdl
*.[Pp]ublish.xml

*.publishsettings

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm

# SQL Server files
App_Data/*.mdf
App_Data/*.ldf

# =========================
# Windows detritus
# =========================

# Windows image file caches
Thumbs.db
ehthumbs.db

# Folder config file
Desktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Mac desktop service store files
.DS_Store

_NCrunch*

!ci/bin/
121 changes: 119 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,119 @@
# task-chaining
Monadic-style chaining for C# Tasks
# RLC.TaskChaining

Monadic-style chaining for C# Tasks.

## Rationale

Asynchronous code (particularly in C#) typically relies on using the `async`/`await` feature introduced in C# 5.0. This has a lot of benefits, but it unfortunately tends to push code into an imperative style. This library aims to make writing asychronous functional code easier, cleaner, and less error-prone using extensions to `System.Threading.Tasks`.

## Installation

Install RLC.TaskChaining as a NuGet package via an IDE package manager or using the command-line instructions at [nuget.org][].

## API

### Chaining

#### Then

Once a `Task<T>` has been created, successive operations can be chained using the `Then` method.

```c#
HttpClient client; // Assuming this is coming from an HttpClientFactory or injected or whatever
Task.FromResult("https://www.google.com") // Task<string>
.Then(client.GetAsync) // Task<HttpResponseMessage>
.Then(response => response.StatusCode); // Task<System.Net.HttpStatusCode>
```

#### Catch

When a `Task<T>` enters a faulted state, the `Catch` method can be used to return the `Task<T>` to a non-faulted state.

```c#
HttpClient client; // Assuming this is coming from an HttpClientFactory or injected or whatever
Task.FromResult("not-a-url") // Task<string>
.Then(client.GetAsync) // Task<HttpResponseMessage> but FAULTED
.Catch(exception => exception.Message) // Task<string> and NOT FAULTED
.Then(message => message.Length) // Task<int>
```

Note that it's entirely possible for a `Catch` method to cause the `Task<T>` to remain in a faulted state, e.g. if you only wanted to recover into a non-faulted state if a particular exception type occurred.

```c#
HttpClient client; // Assuming this is coming from an HttpClientFactory or injected or whatever
Task.FromResult("not-a-url") // Task<string>
.Then(client.GetAsync) // Task<HttpResponseMessage> but FAULTED
.Catch(exception => exception is NullReferenceException
? exception.Message
: Task.FromException(exception)) // Task<string> but STILL FAULTED if anything other than NullReferenceException occurred
.Then(message => message.Length) // Task<int>
```

#### IfFulfilled/IfRejected/Tap

The `IfFulfilled` and `IfRejected` methods can be used to perform side effects such as logging when the `Promise<T>` is in the fulfilled or faulted state, respectively.

```c#
HttpClient client; // Assuming this is coming from an HttpClientFactory or injected or whatever
Promise<string>.Resolve("https://www.google.com/")
.Then(client.GetAsync)
.IfFulfilled(response => _logger.LogDebug("Got response {Response}", response)
.Then(response => response.StatusCode);
```

```c#
HttpClient client; // Assuming this is coming from an HttpClientFactory or injected or whatever
Promise<string>.Resolve("not-a-url")
.Then(client.GetAsync)
.IfRejected(exception => _logger.LogException(exception, "Failed to get URL")
.Catch(exception => exception.Message)
.Then(message => message.Length);
```

The `Tap` method takes both an `onFulfilled` and `onRejected` `Action` in the event that you want to perform some side effect on both sides of the `Promise` at a single time.

```c#
HttpClient client; // Assuming this is coming from an HttpClientFactory or injected or whatever
Promise<string>.Resolve(someExternalUrl)
.Then(client.GetAsync)
.Tap(
response => _logger.LogDebug("Got response {Response}", response),
exception => _logger.LogException(exception, "Failed to get URL")
)
```

### Static Methods

There are some convenience methods on `TaskExtras` that are useful when transitioning between the fulfilled and faulted states.

#### RejectIf

`RejectIf` can be used to transition into a faulted state based on some `Predicate<T>`.

```c#
Task.FromResult(1)
.Then(TaskExtras.RejectIf(
value => value % 2 == 0,
value => new Exception($"{nameof(value)} was not even")
));
```

#### ResolveIf

`ResolveIf` can be used to transition from a faulted state to a fulfilled state based on some `Predicate<Exception>`.

```c#
Task.FromException<int>(new ArgumentException())
.Catch(TaskExtras.ResolveIf(
exception => exception is ArgumentException,
exception => exception.Message.Length
))
```

[nuget.org]: https://www.nuget.org/packages/RLC.TaskChaining/
31 changes: 31 additions & 0 deletions TaskChaining.sln
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30114.105
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskChaining", "src\TaskChaining.csproj", "{68ECCC63-041F-4224-9FF7-CDA13C547DCB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TaskChainingTests", "tests\unit\TaskChainingTests.csproj", "{0F9F7E48-C6A5-4558-A5F5-8F9E5B4690B1}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{68ECCC63-041F-4224-9FF7-CDA13C547DCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{68ECCC63-041F-4224-9FF7-CDA13C547DCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{68ECCC63-041F-4224-9FF7-CDA13C547DCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{68ECCC63-041F-4224-9FF7-CDA13C547DCB}.Release|Any CPU.Build.0 = Release|Any CPU
{0F9F7E48-C6A5-4558-A5F5-8F9E5B4690B1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0F9F7E48-C6A5-4558-A5F5-8F9E5B4690B1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0F9F7E48-C6A5-4558-A5F5-8F9E5B4690B1}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0F9F7E48-C6A5-4558-A5F5-8F9E5B4690B1}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{0F9F7E48-C6A5-4558-A5F5-8F9E5B4690B1} = {26E4FD11-2DB7-4E0D-841C-5ADC5760A46C}
EndGlobalSection
EndGlobal
10 changes: 10 additions & 0 deletions ci/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Universal, base image
# See: https://registry.hub.docker.com/_/microsoft-vscode-devcontainers?tab=description
# See: https://github.com/microsoft/vscode-dev-containers/tree/master/containers/codespaces-linux
FROM mcr.microsoft.com/vscode/devcontainers/universal:linux AS build-environment

USER root

# Install CICEE and make sure .NET global tools are added to path
RUN dotnet tool install -g cicee
ENV PATH="${PATH}:/root/.dotnet/tools"
Loading

0 comments on commit ef26f74

Please sign in to comment.