ContainerExpressions 12.0.1
dotnet add package ContainerExpressions --version 12.0.1
NuGet\Install-Package ContainerExpressions -Version 12.0.1
<PackageReference Include="ContainerExpressions" Version="12.0.1" />
paket add ContainerExpressions --version 12.0.1
#r "nuget: ContainerExpressions, 12.0.1"
// Install ContainerExpressions as a Cake Addin #addin nuget:?package=ContainerExpressions&version=12.0.1 // Install ContainerExpressions as a Cake Tool #tool nuget:?package=ContainerExpressions&version=12.0.1
ContainerExpressions
ContainerExpressions provides generic abstractions to remove boilerplate code needed by programs.
This package provides various wrappers for any type T
, that give it some sort of "superpower".
The containers provide a base, for the expressions to target.
PM> Install-Package ContainerExpressions
Response<T>
An enclosing type around a method's normal return type.
Used when methods may return an error beyond your control, the container signals if the method ran successfully or not.
If the method completed successfully, you can access the real value, otherwise the response is in an error state.
In the example below we show the pattern for using the response container. The response type will be valid only when a value is set.
class CustomerService
{
public Response<Customer> LoadCustomer(int id)
{
var response = new Response<Customer>(); // The response starts off in an invalid state.
try
{
string json = File.ReadAllText($"./User/{id}.json");
Customer customer = JsonConvert.DeserializeObject<Customer>(json);
response = response.WithValue(customer); // The response is in a valid state.
}
catch (Exception ex)
{
Log.Error(ex);
}
return response;
}
}
The consuming code looks like:
CustomerService service = new CustomerService();
Response<Customer> customer service.LoadCustomer(1337);
if (customer.IsValid)
{
// Do something with the customer...
}
Response Extension Methods
Useful utilities for the Response<T>
type:
T GetValueOrDefault<T>
Returns the default value when the Response is in an invalid state.Response<TResult> Bind<T, TResult>
Invokes a second function with the output of the first one.Response<TResult> Transform<T, TResult>
Changes typeT
, to typeTResult
.Func<T, Response<TResult>> Lift<T, TResult>
Elevate the function’s type fromT
, toResponse<T>
Response<TResult> Pivot<T, TResult>
Execute the first function if the condition is true, otherwise execute the second function.bool IsTrue<T>
When the Response is in a valid state the func's bool result is returned, otherwise false is returned.Response<T> Create<T>
Create a response container in a valid state.Response<T> With<T>
Create a new response container in a valid state, with the same typeT
as the original response.Task<Response<T>> ToResponseTaskAsync<T>
Convert someTask<T>
toTask<Response<T>>
, in a valid state when the task did not fault; and finished executing.Response<T> Unpack<T>
ConvertsResponse<Response<T>>
toResponse<T>
, which works much likeTask
'sUnwrap
extension to flattenTask<Task<T>>
toTask<T>
.Response<T> Validate<T>
Determines ifT
is in a valid state or not, and returns a valid response container if it is.Response<TResult> BindIf<T, TResult>
BindIf will execute a function only if the Response is valid, and the boolean predicate is true.
In general you will find various overloads for these extension methods.
They target T
, Response
, and Response<T>
; with options for both sync, and async types.
Response
Similar to Response<T>
, but used for methods that return void, instead of a real type.
class CustomerService
{
public Response SaveCustomer(int id, Customer customer)
{
var response = new Response(); // The response starts off in an invalid state.
try
{
string json = JsonConvert.SerializeObject(customer);
File.WriteAllText($"./Users/{id}.json", json);
response = response.AsValid(); // The response is in a valid state.
}
catch (Exception ex)
{
Log.Error(ex);
}
return response;
}
}
Later<T>
Used in situations where you desire the value to be calulated the first time it's accessed.
In the example below IUserService
is injected into other services using dependency injection, the code runs in the context of a web server.
So depending on what time the DI framework creates the UserService
, and what time the user is authenticated (and therefore sets the thread's CurrentPrincipal
), reading the name may cause an error.
Using the later container we no longer care about the execution order of the authentication, and the dependency injector.
class UserService : IUserService
{
public string Name { get { return _username; } }
private readonly Later<string> _username;
public UserService()
{
_username = Later.Create(() => Thread.CurrentPrincipal.Identity.Name);
}
}
Note: there is also Later.CreateAsync()
for asynchronous values.
ValueLater<T>
Struct option of the Later<T>
type.
Use this for lazy loads, where copy semantics make sense.
There is no async
version for this value type.
var later = new ValueLater<int>(() => 42); // Helper method included: ValueLater.Create(() => 42);.
int number1 = later.Lazy.Value; // Long syntax (function is calculated at this point).
int number2 = later; // Short syntax (same behaviour as above).
Try<T>
Wrap an Action
(a void function), or a Func<T>
(a function returning a "real" type) in a Try Container to safely execute otherwise problematic code.
If the code in a function can throw errors, and those aren't handled internally the Try Container can help out.
This would be used in cases where the function doesn't return a Response
, or Response<T>
, and can throw exceptions.
It can make code clearer as the logic isn't clouded by error handling, however when to use this instead or handling the errors in the function itself is left to the implementer (you).
By default errors aren't logged, but you can add your own logger that'll be ran each time the Try Container encounters an error.
If you'd like to log any errors it's suggested you set up a logger at the start of the program, however you're able to change, or remove the error logger at any point in the program.
Whatever logger is set at the time a Try container is created, is the logger that Container will use. It's suggested your logger is stateless to avoid runtime complications.
The custom logger is a simple 'Action' that takes an 'Exception' as an argument.
For example a logger in a console app might look like: Try.SetExceptionLogger(ex => Console.WriteLine(ex))
- or simply: Try.SetExceptionLogger(Console.WriteLine)
.
In the example below a Widget
is persisted to disk in a fire, and forget fashion.
Since the result of the save isn't used, the return type is void
. The function lacks error handling, so it's lifted to a Try Container.
var result = Try.Run(() => Persist(widget));
private static void Persist(Widget widget)
{
var contents = JsonConvert.SerializeObject(widget);
var path = $"{RELATIVE_PATH}/{Path.GetRandomFileName()}.json";
File.WriteAllText(path, contents);
}
Note: there is also Try.RunAsync() for asynchronous functions.
When logging exceptions from Container Expressions, you can find additional Caller Attribte information in the exception's data dictionary.
The key can be found in the constant: Try.DataKey
.
Preferably you can use the provided extension method: string GetCallerAttributes(Exception)
.
For example logging out the data:
// The logger is setup once at startup.
Try.SetExceptionLogger(ex => Console.WriteLine($"{ex}\n\n{ex.GetCallerAttributes()}"));
Action error = () => { throw new Exception("I am an error!"); };
Response result = Try.Run(error); // Response is not valid, as an exception was thrown, and caught.
// Console Output:
"""
System.Exception: I am an error!
at Tests.ContainerExpressions.Containers.TryTests.<>c.<Base_MethodThrowsError_ErrorIsCaught>b__1_1() in C:\GitHub\ContainerExpressions\src\Tests.ContainerExpressions\Containers\TryTests.cs:line 36
at ContainerExpressions.Containers.Try.PaddedCage(Action action, ExceptionLogger error) in C:\GitHub\ContainerExpressions\src\ContainerExpressions\Containers\Try.cs:line 69
Message: I am an error!
CallerArgumentExpression: error
CallerMemberName: Base_MethodThrowsError_ErrorIsCaught
CallerFilePath: C:\GitHub\ContainerExpressions\src\Tests.ContainerExpressions\Containers\TryTests.cs
CallerLineNumber: 38
"""
Either<T>
If you have a function that can benefit from returning one type, from a selection of types, then Either is what you're looking for.
Either can change it's internal type during program execution similar to object, but in a type safe way.
A case where you might use Either is when you have a function, and you find yourself about to drastically change it's return type for an edge case.
Let's say you have a SaveCustomer function that takes a Customer, and returns a boolean indicating if the save was successful or not.
A new requirement comes in, you must display a error message to the client if the customer's email is already in use by another client.
You can't just rely on the boolean's false, because you don't know if it was false from of a database write error, or a duplicate email.
So you must change the return type from boolean to CustomerResponse, and CustomerResponse contains two fields, one for the boolean, and another string for the error message.
Alternatively you could modify the return type to be Either<bool, string>
, and return a string in the case of a duplicate email.
In the example below we display different messages based off Either's internal type.
Here we see types used to indicate state, something that would normally be done with values of a type, not the type itself.
// Given the two types:
struct Ok { }
struct Error { }
// We can set Either to contain one of these two types.
Either<Ok, Error> either = new Ok();
if (new Random().Next() % 2 == 0)
{
either = new Error();
}
string message = either.Match(
ok => "Operation was successful.", // When Either's type is Ok, this string is returned.
error => "Internal error - try again later." // When Either's type is Error, this string is returned.
);
Either
is also useful in as an input argument, where you can accept different types; and would normally have to overload the function.
Below we see Either
allows you to create only one function that accept either a string
, or a Uri
.
private User GetUserById(int id); // Some encapsulated function.
// Without Either - we need two functions to accept the type overloads:
public User GetUser(string id) => Get(int.parse(id));
public User GetUser(int id) => GetUserById(id);
// With Either - one function can combine the two overloads:
public User GetUser(Either<string, int> id) => GetUserById(id.Match(int.Parse, x => x));
// Invoking the functions remain the same, as Either has implicit operators.
User user = GetUser("1337"); // Auto cast to Either<string, int>.
NotNull<T>
A containter for a reference type T
, that ensures the provided value of T
is not null.
The constructor is private, therefore callers must cast from T
to NotNull<T>
(an implicit cast is fine, and most appropriate).
In essence this is the ArgumentNullException
parameter check defined in a type.
A short demo below shows its use:
// userId T (in this case string) is wrapped in NotNull, it will not allow a cast from T to NotNull<T> when T is null.
string GetUserName(NotNull<string> userId)
{
var service = new UserService();
var username = service.GetUsername(userId); // We can be confident that userId has a value, as passing null to this method results in a runtime exception.
return username;
}
class UserService
{
public string GetUsername(string userId) => USER_NAME; // Observe userId is a normal string here, there is no need to explicitly extract the underlying value from NotNull<T>.
}
The method GetUserName
would be called as if userId was a normal string:
var username = GetUserName("87654321");
There is a struct version of NotNull
described as NN<T>
.
This was included as creating a reference for such a short lived type seemed wasteful.
You may select to use NN<T>
when considering garbage collection, or if you need copy semantics.
A struct is harder to "lock down", as you cannot stop the caller from invoking the default constructor.
If you trust the caller enough to not break you on purpose, and pass in T
letting the implicit cast occur, then using NN<T>
is preferable in most cases.
Maybe<TValue, TError>
Maybe contains an optional TError
type when the caller needs to make different decisions on error, and requires some (or all) of the low level details.
If you do not need the custom TError
type overload, then the base Exception
can be used from the type Maybe<TValue>
(as opposed to Maybe<TValue, TError>
).
Maybe will contain one of either the TValue
, or the TError
/ Exception
types (never both value and error).
Note that Maybe
is very similar to that of Response<T>
, and Either<T1, T2>
.
However it deserves it's own container, as it hits a niche that neither those containers would be good substitutes for.
Response<T>
expects the low level code to handle errors, and only surfaces with a false result when an opperation fails (i.e. containing no error details).
This is by desgin, we do not want to encourage "leaky abstractions". The caller may know "creating a new user failed", but not "it failed because of a table lock".
Table locks can be fixed at the database, or user creation service level, not by the high level caller (this is not their concern).
The caller still has context to generate useful messages for the end client.
Either<T1, T2>
can model the value / error types with it's T2
implementation, but it is not specialized to account for aggregate errors; making it awkward to use at best.
For example Either<T1, T2>
would be fine for a single use of some value / error, where the caller unwraps it right away, and handles the value, or error conditions then and there.
However this is only the very simplest of use cases, more likey the caller wants to invoke many of these in a row thoughout the course of fulfilling some incoming request.
When Maybe
has a value, it is propagated to the next Maybe
for further execution.
When Maybe
has an error, and is combining its result with that of another Maybe
; which also has an error, we need to store both errors, so they may be presented to the top level caller together.
Either
is not capable of storing the aggregated errors, so the user would be forced to do it manually (or lose all errors besides the final one, which would not have been the initial cause).
We cannot provide a general solution for Either
, as values do not need to be stored (they are used, then thrown away); in addition any Either
with 3 or more types would not benefit from such a design.
Lastly Either
does not have the concept of one of the <T>
's being an error. They can be anything at all, so would T1
, or T2
be the error type? This question does not make sense to ask Either
.
In summary use...
Response
when you do not need to know the details of how some operation failed (just that it did).
Either
when you have a range of values that can be produced from some action (one of the values could still be an error, but doesn't have to be).
Maybe
when you want low level error details propagated up to the caller, so they can make better decisions with the provided data.
Below we find a contrived example of how one would use Maybe
:
We attempt to parse a integer from a string input.
Maybe we will get a integer from the operation, or maybe we will get some domain specific error type instead.
class ParseService
{
public enum ParseError { InputNull, InputNotInteger }
private static readonly Maybe<int, ParseError> _maybe = default;
public Maybe<int, ParseError> Integer(string input)
{
if (input == null) return _maybe.With(ParseError.InputNull);
if (!int.TryParse(input, out int number)) return _maybe.With(ParseError.InputNotInteger);
return _maybe.With(number);
}
}
The calling code is as follows:
var parse = new ParseService();
var number = parse.Integer("1234");
var nan = parse.Integer("hello world");
var result1 = number.Match(value => $"{value} is a integer!", error => $"Error: {error}.");
var result2 = nan.Match(value => $"{value} is a integer!", error => $"Error: {error}.");
// [Output]
// result1: "1234 is a integer!"
// result2: "Error: InputNotInteger."
If a custom error type is not required, then ParseService would be implemented as follows:
class ParseService
{
public Maybe<int> Integer(string input)
{
var maybe = new Maybe<int>();
try
{
var result = int.Parse(input); // Will throw Exception when format is not valid.
maybe = maybe.With(result);
}
catch (Exception ex)
{
maybe = maybe.With(ex);
}
return maybe;
}
}
When you don't define a type for TError
, then Exception
is the default.
Alias<T>
Alias allows you to "name" existing types, without changing their underlying behaviour.
For example, you may find yourself returning a string
from a method, but it is not clear to the caller what this string
value is used for.
Alias
provides some clarity as to the type's purpose, in the same vein as a property or field name would (i.e. "string email" is more descriptive than "string" alone).
Take the method Task<Response<string>> GetBearerToken(Authentication model)
.
This method's name, argument, and return type tells us a lot of information.
It's clear from the method name that the string
returned will be a bearer token, the argument will house the username / password + grant type etc.
Task
tells us that the method will run over the network asynchronously, and Response
let's us know the method can fail while retrieving the bearer token.
In this scenario "naming" the return type from string
to something else provides little to no clarification, than what the method already signals to the caller.
Often when integrating with third party APIs, you need only one value from endpoint A, to then invoke endpoint B.
A common pattern is to search by some client reference, then gather more detail using the search Id.
Let's say we're getting customer details using a third party API from "Acme Corporation".
Such methods might look like the following (Task / Response removed for brevity):
string SearchFor(string email)
Customer DetailFor(string acmeRequestId)
In this example, the string
returned from SearchFor
is the acmeRequestId, but that is somewhat ambiguous.
It might be obvious when these methods are next to each other on some service / interface, but what if these methods are surrounded by 10 others?
What if you came to the project a year later, found you need to call the detail API, and you must to determine what the acmeRequestId is?
Since it's a string it really could be anything, it doesn't give you much help in finding what you should be passing in.
There are other solutions here of course, you could:
- Examine unit tests for examples of the detail API usage.
- Check the method's comments.
- Consult the project's documentation.
- Ask a colleague.
These solutions work, but the answers are found too far away from the code you're trying to write.
Instead have the types tell the story, be the documentation.
So we introduce a custom domain model, and change the method signatures to clear up all uncertainty.
class SearchModel
{
public string AcmeRequestId { get; set; }
}
SearchModel SearchFor(string email)
Customer DetailFor(SearchModel acmeRequest)
This is pretty good, it's clear if we want to invoke the detail API, we must first invoke the search API to retrieve a SearchModel
.
But... look at the verbosity we have introduced for something so simple.
If SearchModel
had additional required business values it would be great, as we could justify the POCO.
There is now more for the developer to understand (an extra model), and they may be surpised if they look at the definition, that it only contains a single string.
Of course you could create an abstraction over the two methods that you expose to the rest of the codebase so you only deal with the "complexity" once:
i.e. Customer GetCustomerDetails(string email) => DetailFor(SearchFor(email))
.
That's just an inconvenient truth of the example I've chosen, so let's ignore that for now; and stretch your imagination such that the "low level" search method is commonly used.
You could also argue adding another layer of abstraction here (i.e. layers of indirection) needlessly complicates the overall architecture, so you opt to not "combine" these methods.
I like the SearchModel
solution, but I didn't want to have a new class, in a different file, that I had to create for one single value.
I like the "abstract two methods into one method" solution, now "acmeRequest" is hidden from me; but it's harder to see what is happening, as the "real" work is deep down in abstractions.
While the so called "complexity" of the abstraction is not yet evident, as the codebase grows over time it will be introduced.
The space between these two methods will widen, as they are split into different services, and the mental leap required to join these two methods adds to your 99 other problems.
Leaving the return type for the search API as a string
is an option, but then developers have to read the method's implementations, or rely on the comments / documentation staying up to date with the code.
Enter Alias<T>
.
When you want to give a name to a type.
class AcmeRequestId : Alias<string> { public AcmeRequestId(string value) : base(value) { } }
To define our own Alias
we start with a class, inhert from Alias<T>
, and provide a constructor.
Let's rewrite our initial methods that used raw strings:
AcmeRequestId SearchFor(string email)
Customer DetailFor(AcmeRequestId acmeRequestId)
AcmeRequestId
is more or less a normal string - with a name!
Since this is a one liner (won't be adding extra properties, or methods to this type), I'm happy to define this inline with my service code (instead of creating a new file for it).
Alias
has helped me document the argument for Acme's detail API (with little effort).
AcmeRequestId
will implicitly cast to a string
(or any T), making it easy to use with existing code that expects a string (or T) type; no need to manually convert it back to a string
/ T
.
Another use case for Alias<T>
is overloaded methods:
string SearchFor(string email)
string SearchFor(string name)
string SearchFor(string mobile)
This is not possible, as the runtime can't determine which method you're trying to invoke; you may end up creating different names such as SearchByMobile(string value)
x 3.
Let's create an Alias
for them instead:
class Email : Alias<string> { public Email(string value) : base(value) { } }
class Name : Alias<string> { public Name(string value) : base(value) { } }
class Mobile : Alias<string> { public Mobile(string value) : base(value) { } }
Now we can have the same method name for all search terms (even though they are all essentially strings):
string SearchFor(Email email)
string SearchFor(Name name)
string SearchFor(Mobile mobile)
Lastly Alias<T>
can be used for simple value transformations.
For example, let's say you wanted a string to always be uppercase. Instead of adding validation to ensure it is (or doing it yourself), you could create an Alias<string>
:
class UpperCase : Alias<string> { public UpperCase(string value) : base(value?.ToUpper()) { } }
As you see we transfom the value in the constructor before passing it down to the base class.
The type would then be used in place of a string:
void Save(UpperCase username)
{
// username is guaranteed to be upper case (assuming it's not null).
}
The base class Alias<T>
, cannot implement autocasting from T
, to your custom type.
This is because the abstract class has no knowledge of the your type, and implicit operators
do not accept generic parameters in C#.
If you would like to implement this behaviour (such that the user does not need to call your constructor), simply add this one liner to your Alias
definition.
/// <summary>The current element's index.</summary>
public sealed class Index : Alias<int> {
public Index(int value) : base(value) { }
public static implicit operator Index(int value) => new (value);
}
The above custom type Index
works great, but it has one big problem.
It forces you to create a reference class (Index
) for a value struct (int
).
This might be ok if T
was a reference type too, but quite wasteful when T
is a value type.
To get around this, we also have a struct alias type: ValueAlias<T>
.
Structs are not as versatile as classes, for example they cannot implement inheritance - which is how Alias<T>
works today (it is abstract, so there is no runtime penality for doing this_).
There are two work arounds I have for you:
- Use a file, or global using directive (a complier feature).
global using Index = ContainerExpressions.Containers.ValueAlias<int>; // Put this in GlobalUsings.cs file.
// OR
using Index = ContainerExpressions.Containers.ValueAlias<int>; // Put this in the local file - not as useful as the Index name won't work everywhere.
That solution is pretty good, you get a descriptive name; and a type overload.
But you do miss out on a few things:
> You cannot add XML comments to the type, to further explain your alias.
> You cannot customise the implicit operator
behaviour, or override any of the Equals
, GetHashCode
, or ToString
methods.
> You cannot modify the value in the constructor, before it's sent down to the alias type (so you could not implement UpperCase
for example).
- If you need something more than a
using directive
, you'll need to create this wrapper around yourT
.
/// <summary>The current element's index.</summary>
public readonly struct Index
{
public ValueAlias<int> Value { get; }
public Index(int value) { Value = new(value); }
public static implicit operator int(Index alias) => alias.Value.Value;
public override string ToString() => Value.ToString();
}
This gets you everything in Alias<int>
, missing from ValueAlias<int>
- but in struct form!
I cannot make this struct for you unfortunately, you will have to copy + paste it; and replace the int
with your desired type.
That said, this should be possible if using Source Generators, but I haven't played with them yet.
Be aware that Index
is not safe to do Equals
comparison on, or to use in collections such as a Dictionary
.
For such cases you would need to expose the underlying ValueAlias<T>
, so cast it first, or access it directly though the Value
property.
Note: the reference type Alias<T>
does not have this problem, and is safe to use anywhere you would use T
.
You could do away with the the helper methods for a more concise version, you'd just need to manually access the underlying alias type (ValueAlias<int>
) though the Value
property:
public readonly struct Index
{
public ValueAlias<int> Value { get; }
public Index(int value) { Value = new(value); }
}
You can use it "as is" without wrapping it too:
var index = new ValueAlias<int>(1);
You might ask - why am I using Index
in so many examples? When would you wrap a plain int
?
Well 1) to show the differences without introducing new concepts; and 2) because I have a perfect example that would benefit from an alias.
Take the following extension method from the System.Linq
namespace:
// Summary:
// Projects each element of a sequence into a new form by incorporating the element's index.
//
// Selector:
// A transform function to apply to each source element; the second parameter of the function represents the index of the source element.
//
public static IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, int, TResult> selector);
// Code example of using the above extension method:
new string[] { "Jane", "John" }.Select((x, i) => $"Index: {i}, Name: {x}.");
The problem here is the selector: Func<TSource, int, TResult>
- you can infer what TSource
, and TResult
are, but what is int
doing here?
When creating the lambda function in your IDE, there is no hint on the int
type, as C# does not provide a way to add XML comments on Func
type parameters.
So you either already know it, or you stop what you're doing and go read the full comments, or Google / StackOverflow / ChatGPT it.
If int
was named Index
instead, it would be much more obvious what this parameter is doing here.
We could put XML comments on the type too, which would show up in the IDE as you are creating the lambda - awesome!
I had a similar method to write in this project, I started off with an int
, but changed it to Index
(with comments explaining it's usage):
// Before:
public static TError[] LogError<TError>(this TError[] ex, Func<TError, int, string> format) where TError : Exception
{
for (int i = 0; i < ex.Length; i++) { Trace.Log(format(ex[i], i)); LogException(ex[i]); }
return ex;
}
// After:
public static TError[] LogError<TError>(this TError[] ex, Func<TError, Index, string> format) where TError : Exception
{
for (int i = 0; i < ex.Length; i++) { Trace.Log(format(ex[i], i)); LogException(ex[i]); }
return ex;
}
// Notice how nothing else in the function needed to change after replacing int with Index?
// This is thanks to the implicit casting provided by Alias, the loop's i is converted into an Index automatically.
You could make the argument that new C# language features such as a properties' init accessor, or record types make this Alias
container obsolete.
They dramatically cut down on the red tape required when creating new types, which is the same goal as Alias
.
In the long term I imagine that will be the ultimate fate of this library, every addition here will slowly be eaten away as C# adopts similar concepts into it's specification.
That is already the fate of the Match
expression, as C# now has some pretty powerful pattern matching built into the standard kit.
If you think the language has a better implementation than the types here, you should definitely use them.
Honesty, if C# ever got some level of Monad support (not counting Task or LINQ), that would be the end for this library.
Combining Alias
& Either
You can create custom reusable friendly named types by combining Alias
, and Either
.
This can save you from needing to write out the entire Either
type each time you use it; and you can give the type a descriptive name too.
// Accepts either a: string, or an int. Does not do any extra processing on the input.
class StringOrInt : Alias<Either<int, string>> {
public StringOrInt(Either<int, string> value) : base(value) { }
}
StringOrInt stringOrInt = new StringOrInt("1"); // String value
int number = stringOrInt.Value.Match(x => x, int.Parse); // Output: 1
bool isString = stringOrInt == "1"; // Output: true
bool isInt = stringOrInt == 1; // Output: false
stringOrInt = new StringOrInt(1); // Int value
number = stringOrInt.Value.Match(x => x, int.Parse); // Output: 1
isString = stringOrInt == "1"; // Output: false
isInt = stringOrInt == 1; // Output: true
// Accepts either a: string, short, int, or long. Converts the input to a long.
class ConvertToLong : Alias<long> {
public ConvertToLong(Either<string, short, int, long> value) : base(value.Match(long.Parse, Convert.ToInt64, Convert.ToInt64, x => x)) { }
}
ConvertToLong convertToLong = new ConvertToLong("1"); // String value
long number = convertToLong; // Output: 1
convertToLong = new ConvertToLong((short)1); // Short value
number = convertToLong; // Output: 1
convertToLong = new ConvertToLong((int)1); // Int value
number = convertToLong; // Output: 1
convertToLong = new ConvertToLong((long)1); // Long value
number = convertToLong; // Output: 1
Compose<T>
Used to run dependant functions one after each other, such that the first function's output feeds into the second function's input.
This continues until the last function, when that type is returned in the container Response<T>
.
If any of the functions fail, the whole chain will fail and the final container's response will be invalid.
In the example below DownloadHtml
, and PersistHtml
are functions with the return type of Response<T>
, in this case T is string for both functions.
The function DownloadHtml
retrieves a webpage from a server, and the function PersistHtml
saves that html to a file, returning the path to the new file.
In this case the final function PersistHtml
, has a return type of Response<string>
, so the return type of Expression.Compose
, will also be Response<string>
.
If either of these functions fail, the end result of the expression will be an invalid response.
var filepath = Expression.Compose(DownloadHtml, PersistHtml);
Note: there is also Expression.ComposeAsync() for composing asynchronous functions.
Trace
Logs function inputs, and outputs so you can save them to a trace file.
Tracing is necessary in any non-trivial program to determine production runtime bugs.
However these traces typically get in the way of the core code, and force you to break up code into pieces so you can log it's current state.
The trace container slots in with existing code by taking a type, and returning that same type.
Before you use Trace you must set a logger, an Action that takes a string (returns void), see below for an example.
Trace.SetLogger(Console.WriteLine);
See below for an example of logging the output of each function:
// Initialize the Trace with a logger.
var logs = new List<string>();
Trace.SetLogger(log => logs.Add(log));
// Create a function to trace the incrementing.
Func<int, string> trace = x => string.Format("The value of the int is {0}.", x);
// Some functions that keep incrementing their input.
Func<Response<int>> identity = () => Response.Create(0);
Func<int, Response<int>> increment = x => Response.Create(x + 1);
var count = Expression.Compose(identity.Log(trace), increment.Log(trace), increment.Log(trace));
// The follow is logged to Trace:
// The value of the int is 0.
// The value of the int is 1.
// The value of the int is 2.
Retry<T>
Execute the same function until it's Response is valid, or you run out of retries as defined by the options.
By default the options are set to 1 retry, and a delay of 100 milliseconds before trying again.
There is a method overload to pass in your own options for a more customized Retry.
In the example below, we create a user in a database, and get their Id in return.
// Using default options, this will try a second time if the first time fails.
var userId = Retry.Execute(() => CreateUser(new UserModel { Name = "John Smith" }));
public Response<T> CreateUser(UserModel user)
{
var response = new Response<int>(); // Invalid state, indicates a Retry.
try
{
using (var connection = new SqlConnection(connectionString))
using (var command = new SqlCommand("usp_insert_createUser", connection))
{
command.CommandType = CommandType.StoredProcedure;
command.Parameters.Add("@Name" user.Name);
connection.Open();
var userId = (int)command.ExecuteScalar();
response = response.WithValue(userId); // State is now valid, no need to run the function again.
}
}
catch (exception ex)
{
// Log error here...
}
return response;
}
Note: we also have options for jitter, and Retry.ExecuteExponential()
to grow the delays exponentially (instead of linearly) between failed attempts.
Reduce<T>
Combine many values of T
to a single value of T
.
Useful for any type that is associative.
Only valid states of Response<T>
are combined in the reduce expression.
In the example below we combine many words into a sentence.
Func<string, string, string> combine = (x, y) => string.Concat(x, " ", y);
var words = new Response<string>[] { Response.Create("world") };
var arg1 = "hello";
string sentence = Expression.Reduce(combine, arg1, words);
Funnel<T>
Takes many Response<T>
's and invokes a function passing each T
as an argument; if and only if each Response<T>
is in a valid state.
If at least one input is in an invalid state, then an invalid Response<T>
is returned instead of calling the function.
Useful when you'd like an operation to happen only when several previous operation completed successfully.
In the example below we only take the power of two numbers if they were calculated correctly.
// Returns an Invalid Response<double> when the divisor argument is 0, otherwise the result is stored in a valid Response<double>.
Func<double, double, Response<double>> divide = (dividend, divisor) => divisor == 0 ? new Response<double>() : Response.Create(dividend / divisor);
var e = divide(150D, 55D);
var pi = divide(22D, 7D);
var answer = Expression.Funnel(e, pi, Math.Pow);
Shared Types
Common reusable types for use with containers such as: Either
, Maybe
, and Response
.
Prebaked types for a "batteries included" feeling, these should be common types you'd make yourself; feel free to use these.
In general the shared types will be structs, with a generic, and non-generic implementation:
Unit:
A marker type to use when you have no return type, or result. Provides access to a cachedResponse<Unit>
for reuse.Ok:
200 / Created: 201 / Accepted: 202 / NoContent: 204 (Your request was successfully processed).BadRequest:
400 (Validation error on your request).Unauthorized:
401 (We do not know who you are).Forbidden:
403 (We know who you are, but you are not allowed to be here).NotFound:
404 (Requested server resource was not found).TooManyRequests:
429 (Rate Limit).Error:
500 (Internal Server Error).BadGateway:
502 (Upstream Error).GatewayTimeout:
504 (Upstream Timeout).
You might have custom types, such as: InvalidEmail
, UserNotActive
, AdminUser
, etc.
Find a contrived example below:
public class UserController : ControllerBase
{
public IActionResult Put(User user)
{
return Update(user).Match<IActionResult>(
ok => NoContent(),
notFound => StatusCode(404),
badRequest => BadRequest(badRequest.Value), // Using the generic version to provide error messages to the client.
error => StatusCode(500)
);
}
private static Either<Ok, NotFound, BadRequest<string[]>, Error> Update(User _)
{
return new BadRequest<string[]>(new string[] { "Your request is bad." });
}
}
Shared Functions
Generic, common, little helper functions for lambdas.
There are only a few implementations (for now) - so I'm just going to paste the class here in lieu of documentation:
/// <summary>A place to put shared common functions.</summary>
public static class Lambda
{
/// <summary>Maps the input directly to the output.</summary>
public static T Identity<T>(T x) => x;
/// <summary>Pretends to return a T (i.e. to compile), but will really throw the passed exception.</summary>
public static T Identity<T>(Exception ex) => throw ex;
/// <summary>Discards the function input, and returns the specified result.</summary>
public static Func<T, T> Default<T>(T result = default) => _ => result;
/// <summary>Discards the function input, and returns the specified result (of a different type to the input type).</summary>
public static Func<T, TResult> Default<T, TResult>(TResult result = default) => _ => result;
// Wrap method delegates, so the complier can recognise them as Func{TResult} types.
public static Func<TResult> ToFunc<TResult>(this Func<TResult> func) => func;
// Wrap method delegates, so the complier can recognise them as Action types.
public static Action ToAction(this Action action) => action;
}
Why use these? They are static functions, with descriptive names.
Of course you can make your anonymous delegates static inline now too i.e. (static x => x)
.
When ordering ints
using System.Linq
, you must provide a key to OrderBy
.
This forces you to write: new int[] { 3, 2, 5, 7, 6 }.OrderBy(x => x);
.
You can replace (x => x)
, with the Identity
function: new int[] { 3, 2, 5, 7, 6 }.OrderBy(Identity);
.
This makes the code's intent clear.
Note: you would normally need to write OrderBy(Lambda.Identity)
instead.
I suggest adding global using static ContainerExpressions.Containers.Lambda;
to your GlobalUsings.cs file.
The ToFunc<TResult>
, and ToAction
helpers are used to turn any method into a base that can be targeted with extensions.
private static int GetResult(int i, string s) => 1;
private static void GetVoid(int i, string s) => throw new Exception(); // Error is caught (and logged) by the await below.
// Func with 2 args is converted into a func with 0 args.
Response<int> funcResult = await Lambda.ToFunc(() => GetResult(0, "")); // await safely wraps the function in a Response<int>.
// Action with 2 args is converted into a action with 0 args.
Response<Unit> actionResult = await Lambda.ToAction(() => GetVoid(0, "")); // await safely wraps the function in a Response<Unit> (Unit represents void).
// There is an argument builder with compiler implicit type support. The end result is the same as above, so it's more of a preference.
Response<int> argsFuncResult = await Lambda.Args(0, "").ToFunc(GetResult);
Response<Unit> argsActionResult = await Lambda.Args(0, "").ToAction(GetVoid);
Instance
Holds instances for any reference type TValue
, you can use this instead of null
when falling back to some default value.
By not having to create new instances, you are saving on object allocation.
The instances given to, and read from Instance
should be considered readonly, and immutable.
Instances should be created at program start up.
Instance
is implemented in classes created at compile time (i.e. using static + generics), so there is no backing collection, or explicit locking.
Along the lines of:
> string.Empty
, and Array.Empty<T>()
.
> We have: var str = Cache.Get<string>()
, and var strArray = Cache.Get<string[]>()
.
You can only have one value per type. However this restriction can be worked around using Alias<T>
.
// Example of adding different string references to Instance.
class Jane : Alias<string> { public Jane() : base(nameof(Jane)) { } }
class John : Alias<string> { public John() : base(nameof(John)) { } }
// This works because we are not adding a string type, we are adding two new types: Jane, and John.
Instance.Create(new Jane());
Instance.Create(new John());
John john = Instance.Of<John();
string name = Instance.Of<John>(); // Auto casting provided by Alias<string> works here.
Jane jane = Instance.Of<Jane>();
InstanceAsync
Just like Instance
, but targets Task
types.
InstanceAsync.Of<int>():
gets a cached, and completedTask<int>
with a defaultint
value.InstanceAsync.ValueOf<int>():
gets a completedValueTask<int>
, with a defaultint
value; newly created on each invoke.InstanceAsync.ResponseOf<int>():
gets a cached, and completedResponseAsync<int>
, with a defaultint
value.
Combining Alias
& Instance
Below is an example of creating a CVS type, that reuses a char[]
instance.
As demonstrated, custom types can be quick to build, and efficient to run.
public sealed class Csv : Alias<string[]> {
private sealed class Comma : Alias<char[]> { public Comma() : base(new char[] { ',' }) { } } // Private type for Instance to avoid collisions on char[].
static Csv() => Instance.Create(new Comma()); // Static constructor registers the Instance type of Comma.
public Csv(string csv) : base(csv.Split(Instance.Of<Comma>())) { } // Splits the string into a string[], the char[] argument is reused.
}
ResponseAsync<T>
A task-like type that can be used on async functions to catch, and log exceptions thrown in a method.
You can use ResponseAsync<T>
anywhere you would use Task<T>
, and it will return a Response<T>
- instead of a T
.
private static async ResponseAsync<int> GetValue()
{
return await Task.FromResult(42);
}
private static async ResponseAsync<int> GetError()
{
throw new Exception();
return await Task.FromResult(42);
}
var value = await GetValue(); // Result: 42.
var error = await GetError(); // Result: No value, as an error occurred (but no exception is thrown from this await).
Note: Exceptions are logged to the Try.SetExceptionLogger()
listener, so make sure to set this at startup to catch them.
ResponseAsync<T>
functionality also includes:
AsTask():
ConvertResponseAsync<T>
into aTask<Response<T>>
(that won't throw exceptions).AsValueTask():
ConvertResponseAsync<T>
into aValueTask<Response<T>>
(that won't throw exceptions).AsResponse():
Converts aTask<T>
, orValueTask<T>
into aTask<Response<T>>
, orValueTask<Response<T>>
respectively (with no exceptions).FromResult<T>(T):
Creates aResponseAsync<T>
task in a completed state, with a validResponse<T>
result.FromException<T>(Exception):
Creates aResponseAsync<T>
task in a completed state, with an invalidResponse<T>
result (errors are logged).
Custom Async Method Builders
Override the default async method builder for Task
, and Task<T>
/ Task<Response<T>>
.
This keeps their task-like types, and awaiters intact; only replacing the method builder implementation (specifically around exception handling).
If the method returns a T
, instead of a Response<T>
, errors will still be logged, but they will also be thrown if you attempt to get the result.
Example usage:
// Returns an invalid response when an exception is thrown in this method (or the task is cancelled), otherwise returns a valid response.
[AsyncMethodBuilder(typeof(ResponseAsyncTaskCompletionSource<>))]
public async Task<Response<string>> Foo() // TaskAwaiter with a custom async method builder.
{
await Task.Delay(0);
return Response.Create("Hello, World!");
}
Custom Awaiters
A Task
can be canceled, or faulted; in these cases you won't get a result from await
, instead you'll get a runtime exception.
If you wrap a Task
in a Response
, a custom awaiter will kick in:
- Normal:
Task<Response<T>>
orTask<T>
- Target:
Response<Task<Response<T>>>
orResponse<Task<T>>
int number = 100;
Response<int> input = Response.Create(number);
Task<Response<int>> task = Task.FromResult(input); // Normal: Task<Response<T>>
Response<Task<Response<int>>> response = Response.Create(task); // Target: Response<Task<Response<T>>>
Response<int> result = await response; // This is a "safe" Response, it will be invalid when the Task is canceled, or faulted - instead of throwing exceptions.
Assert.IsTrue(result);
Assert.AreEqual(number, result);
There are other custom awaiters available for ValueTask
, and Task
; along with their various Packed implementations.
All these types can be safely converted to Response
/ Response<T>
using the await
keyword:
Action
Func<T>
Func<Task>
Func<Task<T>>
Func<Task<Response>>
Func<Task<Response<T>>>
Func<ValueTask>
Func<ValueTask<T>>
Func<ValueTask<Response>>
Func<ValueTask<Response<T>>>
Func<ResponseAsync<T>>
Response<ValueTask>
Response<ValueTask<T>>
Response<Task>
Response<Task<T>>
Response<Task<Response<Response>>>
Response<Task<Response<Task<Response>>>>
Response<Task<Response<Response<T>>>>
Response<Task<Response<Task<Response<T>>>>>
Tag
You can Tag
any value, or reference type with a message, the message can be of any type.
Tag<TReference>
has two methods, Get, and Set: Response<TMessage> Get<TMessage>(bool)
, and TReference Set<TMessage>(TMessage)
.
Get: Retrieves a message from a tagged object. By default the message is removed once read, but there is an option to keep it.
Set: Stores a message on a tagged object. If the object already has the same typed message on it, it will be overwritten.
Tag
is useful for when you want to add extra data to an object, but you don't want to break the method signatures, or you don't control the object.
Example:
Here we are adding a Datetime
message to a Guid
object.
Note the method's signature does not change, neither does the Guid
model itself; and we're still able to pull the extra data out.
private static Guid GenerateGuid()
{
// Tag a Guid, with the datetime it was created.
return Guid.NewGuid().Tag().Set(DateTime.UtcNow);
}
Guid guid = GenerateGuid(); // 7db88344-5309-46dc-a535-1c3f0e029f69
DateTime created = guid.Tag().Get<DateTime>(); // 21/07/2024 1:43:04 PM
Tag
is thread safe, and works in one of two ways:
Value Types: Using the Equals
, and GetHashCode
methods to find a message given an object.
Reference Types: Using the pointer's address in memory to uniquely identify the object, and match it to a message.
Fire And Forget
A place to push
tasks to, that can finish in the background while the program is doing other things.
The background tasks can be waited later on before completing the current request, or they can be left alone to clean themselves up.
Pick a strategy based on your execution model.
Short lived apps like containers, lambdas, CLIs would want to wait on the background jobs to finish, before returning.
Long lived apps like web servers, consoles, desktop apps might prefer to let the background jobs run without waiting for them.
Keep in mind FireAndForget
does not store state, and resume it later on; it is ephemeral.
If the main process ends, all executing background jobs will be lost with it (if you don't call WhenAll() on shutdown to let them wrap up).
Example: Add some tasks, and wait for them to finish.
// Push several tasks to "FireAndForget".
FireAndForget.Push(Task.FromResult(42));
FireAndForget.Push(Task.Delay(0));
FireAndForget.Push(Task.CompletedTask);
// If the tasks are very quick, they will finish; and remove themselves before you can wait for them.
// Longer running tasks have a higher chance of still existing, and then needing to be waited on.
var completedTasks = await FireAndForget.WhenAll(); // Returns the number of tasks that were awaited (between 0, and 3 in this case).
FireAndForget
is thread safe, you can Push
from any thread / context.
If any errors are thrown by the tasks, they will be sent to the Try
container's error logger.
You can continue pushing new tasks to FireAndForget
, even if you have called WhenAll()
; and are waiting on it to finished.
The new tasks won't be included in the current WhenAll()
's awaiter.
SmartEnum<T>
Like a normal enum
, a SmartEnum<T>
has string, and integer values; and can be parsed with either low level types.
What makes a smart enum different then is the fact you're able to add properties, and method to them (since they are classes).
Smart enums are a little more forgiving when it comes to parsing string values, and a little more explicit when it comes to dealing with flags (opt-in behaviour).
Smart enums don't look too different from standard enums.
// A smart enum without flags (i.e. only one value can be held at a time).
class Weekend : SmartEnum
{
// The string value is the same as the property name, the int value starts from 0, and increments 1 at a time from there.
public static readonly Weekend None = new();
public static readonly Weekend Saturday = new();
public static readonly Weekend Sunday = new();
private Weekend() { }
}
// A smart enum with flags (i.e. can hold many values at a time).
class Colour : SmartEnum
{
// The int value here is explicitly set, instead of relying on the defaults.
// If the int values are powers of 2, the smart enum will behave as a [flags] enum normally would.
public static readonly Colour None = new(0);
public static readonly Colour Purple = new(1);
public static readonly Colour Red = new(2);
public static readonly Colour Green = new(4);
public static readonly Colour Teal = new(8);
private Colour(int value) : base(value) { }
}
You can set custom values for string, and int by overriding the appropriate base constructor.
You may also set aliases for the string name values, allowing more than one string value to map to a smart enum field.
Usage of smart enums is as follows:
var smartEnum1 = SmartEnum<Colour>.FromObjects(Colour.Purple, Colour.Red); // Fields from the Colour smart enum.
var smartEnum2 = SmartEnum<Colour>.FromNames("purple,red"); // Case-insensitive csv matching the smart enum names.
var smartEnum3 = SmartEnum<Colour>.FromValues(3); // Integer value of Colour.Purple, and Colour.Red.
var smartEnum4 = SmartEnum<Colour>.FromValues(Colour.Purple | Colour.Red); // Same as the "3" value above, but using the bitwise OR.
var hasFlag = smartEnum1.HasFlag(Colour.Red); // True.
var addGreen = smartEnum1 | Colour.Green; // Bitwise OR adds a target if it doesn't exist.
var toggleGreen = addGreen ^ Colour.Green; // Bitwise XOR removes the target if it exists, otherwise adds it.
Credits
- Icon made by Vitaly Gorbachev from [Flaticon]
Changelog
6.0.0
- Started a changelog (i.e. pre version 6 you need to check the raw git commits).
Bind
andTransform
now take anyT
off someResponse
instead of only aResponse<T>
.- You can now
Log
off anyT
, previously it was only forResponse<T>
. - Async
Try
container now handlesAggregateException
separately from the standardException
type. - The
Retry
container now accepts an exponential backoff algorithm, with optional jitter. - Any
T
value can now initiate aResponse
chain using thePush
extension. - Introduced
NotNull<T>
as a way to counter reference types being null. - Lots of small method signature changes to flesh out
Bind
,Transform
, andLog
; allowing more combinations ofT
,Response<T>
, andTask<Response<T>>
in chains. - New container
Maybe<TResult, TError>
with operations forMatch
,Bind
, andTransform
.
There is nothing "major" in this release, most of he changes are quality of life around logging: "anything anywhere"; and composing varied functions via Bind
and Transform
that were missing.
The major version was bumped (MAJOR.MINOR.PATCH), as we've introduced backwards incompatible changes to some signatures that weren't quite right before.
6.0.1
- Nuget package metadata added to the project file, allowing easy releases via the Visual Studio Pack command.
7.0.0
- Breaking change: renamed extension method
Response.WithValue(Any<T>)
toResponse.With(Any<T>)
to follow the conventions of other containers. - New container
Alias<T>
allows you to give names to types, while retaining the behavior of the underlying type.
8.0.0
- Breaking change: some methods have been renamed, as the complier had some issues working out the overloaded types (
T
/Response
/Response<T>
/Task
/Task<T>
/Task<Response>
/Task<Response<T>>
). - When a raw
Task
, orTask<T>
are passed into extension methods; they will now be safely unwrapped, and any errors will be logged out.
8.0.1
- Added readme file to nuget.
9.0.0
- Updated additional Pack details in the solution file (debug symbols, xml comments, readme file, etc).
- New extension method for
Response
, andResponse<T>
types (both sync, and async) calledUnpack()
- which flattens response containers. - When
Try.SetExceptionLogger()
is configured, exceptions logged thoughLogError()
- or found inResponse
, andMaybe
containers, are sent though. - Added a
LogError
extension method, so you can which forwards exceptions to the logger setup for:Try.SetExceptionLogger
. - Expanded the function targets for
Funnel
. Was onlyT
, now includes:Response<T>
,Task<T>
, andTask<Response<T>>
. - Added equals overloads to
Either
, so you can easily compare someT
value to theEither
container. - Added
TryGetT*
toEither
, allowing access to the types without going thoughMatch<TResult>
. - Added a struct version of
Alias<T>
calledA<T>
. - Added shared types to resue in containers, such as
OK
,BadRequest
,Error
, etc. - Created shared common lambda functions, such as
Lambda.Identity
to reflect the input as the output. i.e. instead of having to write(x => x)
. - Added
TryGetValue()
, andTryGetError()
toMaybe
. - Added
Unpack
toMaybe
. - Created a new container
Cache:
store readonly default values for reference types (i.e. instead of using null).
10.0.0
- Renamed
Cache
toInstance
, as the former name lead to confusion on what the container did. - Added custom awaiters for
Response
types wrappingTask
/ValueTask
. - Created
ResponseTask
to make casting betweenTask
, andResponse
easier (ResponseValueTask
is also available forValueTask
sources). ResponseAsync<T>
is a task-like type that can be used onasync
functions to catch, and log exceptions thrown in a method.- Added async method builders for
Task
, andValueTask
to safely createResponse
types. - Added a value type verion of
Later<T>
-ValueLater<T>
. Maybe<TValue>
, andMaybe<TValue, TError>
now have a methodTryGetAggregateErrors
, which provides easy access to the aggregate errors (should they exist).Maybe<TValue>
, andMaybe<TValue, TError>
now have a methodTryGetAllErrors
, which provides easy access all errors - top level and aggregate (should they exist).- Added
TryGetT*
to theEither<T*>
containers. To retrieve the value if it's present. - Added
WhenT*
to theEither<T*>
containers. Execute aFunc
, orAction
on the value if it's present. - Added an
Action
overload to theMatch
method for theEither<T*>
containers. Async
versions for theTryGetT*
,WhenT*
, andMatch
methods on theEither<T*>
containers.- Renamed
A<T>
toValueAlias<T>
, which follows the .net naming conventions of value types. - Added Caller Attributes to the
Try
container, and error logging functions, to provide more context.
11.0.0
- This version's breaking change is around removing types that haven't provided value, or have been out classed by C# / .NET improvements.
- Created a repo for the dead types: Containers.Experimental.
- Removed the
Loop<T>
type (speed improvements are coming to linq in .net 8). - Removed the
Match<T>
type (pattern matching is now a C# language feature). - Removed the type
ResponseValueTask<T>
type (made redundant by ResponseAsync). - Removed the type
ResponseTask<T>
type (made redundant by ResponseAsync).
12.0.0
- Changed exception throwing function from
void ThrowError<T>
toT ThrowError<T>
, allowing it to be used in more places that need a result (such as switch pattern matching). - Removed
WhenT1Async
, due to the use of generics there was no difference between that, andWhenT1
(i.e. the non-async version). - Removed the string alias
Message
used in logging, and replaced it with aFormat
type; which has the message template, as well as the args. - Added a new formatted logger for trace messages (
Trace.SetFormattedLogger
), and error logs (Try.SetFormattedExceptionLogger
). - Created a
Tag
extension method to set, and get messages on objects. - Added a function overload to the extension method
Pivot
, to include the previous result as part of theboolean
condition:Func<T, bool> condition
. - Created a new extension method for
Response<T>
: Validate<T>. Validate determines if the state forT
is valid or not, returning a valid response container if it is. - Renamed
AsTask
,AsValueTask
, andAsResponse
to:ToTask
,ToValueTask
, andToResponse
respectively. The As convention is used for casting objects, and these methods are creating new ones. - Created a new extension method for
Response<T>
: BindIf<T, TResult>. BindIf will execute a function only if the Response is valid, and the boolean predicate is true. - Created a new container:
FireAndForget
- enables a place to push tasks to for background completion. - Created
SmartEnum<T>
- an alterative for enums, that can have methods, and properties.
12.0.1
- Added protections to the
SmartEnum<T>
container, when interacting on a defaultEnumRange<T>
collection; making it safe to use even when not initialized.
Product | Versions 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. |
-
.NETStandard 2.0
- System.Threading.Tasks.Extensions (>= 4.5.4)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on ContainerExpressions:
Package | Downloads |
---|---|
FrameworkContainers
Framework Containers provides useful parts of frameworks wrapped into single types. The general idea is to replace common services, with simple types instead, reducing overheads when creating new projects. Common services include http, sql, and serialization. |
GitHub repositories
This package is not used by any popular GitHub repositories.
Version | Downloads | Last updated |
---|---|---|
12.0.1 | 287 | 9/17/2024 |
12.0.0 | 98 | 9/9/2024 |
10.0.0 | 3,241 | 6/25/2023 |
9.0.0 | 848 | 4/25/2023 |
8.0.1 | 4,876 | 11/11/2021 |
8.0.0 | 1,791 | 11/1/2021 |
7.0.0 | 1,116 | 1/27/2021 |
6.0.1 | 1,039 | 1/1/2021 |
5.0.0 | 2,765 | 11/4/2018 |
4.0.0 | 1,800 | 10/16/2016 |
3.0.0 | 2,218 | 4/21/2016 |
2.0.0 | 1,794 | 4/20/2016 |
1.1.0 | 2,013 | 4/19/2016 |
1.0.0 | 1,824 | 4/14/2016 |
* Added protections to the `SmartEnum<T>` container, when interacting on a default `EnumRange<T>` collection; making it safe to use even when not initialized.