Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 27 additions & 1 deletion .well-known/agent-skills/csharpessentials-maybe/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: csharpessentials-maybe
description: Use when representing optional values explicitly — Maybe<T> as a null-safe container, Maybe.From() for creation, HasValue/HasNoValue, Map/Bind chaining, Match for consumption, and ToMaybeResult() to bridge into the Result pattern.
description: Use when representing optional values explicitly — Maybe<T> as a null-safe container, Maybe.From()/FromTry() for creation, HasValue/HasNoValue, Map/Bind chaining, TapNone for None-side effects, Match for consumption, and ToMaybeResult() to bridge into the Result pattern.
---

# CSharpEssentials.Maybe
Expand Down Expand Up @@ -39,6 +39,14 @@ string val = maybe.GetValueOrDefault("fallback");
string val = maybe.GetValueOrThrow(); // throws if None
```

## Exception-safe Creation

```csharp
// Returns None if the factory throws — never propagates the exception
Maybe<int> n = Maybe<int>.FromTry(() => int.Parse(input));
Maybe<User> u = Maybe<User>.FromTry(() => JsonSerializer.Deserialize<User>(json));
```

## Pattern Match

```csharp
Expand All @@ -60,6 +68,24 @@ Maybe<Address> address = Maybe.From(user)
.Bind(u => Maybe.From(u?.Address));
```

## None-side Effects

```csharp
// TapNone — runs only when None; returns the same Maybe unchanged
maybe.TapNone(() => logger.LogWarning("Value was absent"));

// Async — instance and extension variants
await maybe.TapNoneAsync(async () => await NotifyAsync());
await GetMaybeAsync().TapNoneAsync(() => fallback());

// GetValueOrElse — lazy factory, only called when None
int v = maybe.GetValueOrElse(() => ComputeExpensiveDefault());

// OrElse — lazy Maybe chain, alias for Or(Func<Maybe<T>>)
Maybe<Config> cfg = GetCached().OrElse(() => LoadFromDisk());
await GetCached().OrElseAsync(() => Task.FromResult(LoadFromDisk()));
```

## Bridge to Result

```csharp
Expand Down
18 changes: 18 additions & 0 deletions .well-known/agent-skills/csharpessentials-results/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,24 @@ Result<int> safe = Result.Try(() => int.Parse(input), ex => Error.Exception(ex))
Result<Data> data = await Result.TryAsync(() => _db.GetAsync(id), ex => Error.Exception(ex));
```

## Conditional Side Effects

```csharp
// TapIf — predicate-gated tap, only fires when Success AND predicate is true
result.TapIf(v => v > 0, v => Audit(v));

// Bool condition shorthand
result.TapIf(featureEnabled, v => Track(v));

// Async — instance methods
await result.TapIfAsync(v => v > 0, async v => await AuditAsync(v));
await result.TapIfAsync(isEnabled, async v => await TrackAsync(v));

// Extension variants — Task<Result<T>> and ValueTask<Result<T>>
await GetResultAsync().TapIfAsync(v => v > 0, v => Enqueue(v));
await GetValueTaskResultAsync().TapIfAsync(true, async v => await LogAsync(v));
```

## Best Practices

- Never access `.Value` without checking `.IsSuccess` first
Expand Down
28 changes: 28 additions & 0 deletions .well-known/agent-skills/csharpessentials-rules/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,34 @@ result.Match(

---

## Predicate Factories

Create `IRule<T>` or `IAsyncRule<T>` directly from a predicate — no class needed.

```csharp
// Static error
IRule<int> rule = RuleEngine.FromPredicate<int>(
x => x > 0,
Error.Validation("Value.Negative", "Must be positive"));

// Error factory — error message references the failing context
IRule<string> rule = RuleEngine.FromPredicate<string>(
s => s.Length >= 3,
s => Error.Validation("String.TooShort", $"'{s}' must be at least 3 characters"));

// Async predicate (e.g., DB uniqueness check)
IAsyncRule<string> rule = RuleEngine.FromPredicateAsync<string>(
async s => await _db.IsUniqueAsync(s),
s => Error.Conflict("Name.Taken", $"'{s}' is already taken"));

// Evaluate and compose normally
Result r = RuleEngine.Evaluate(rule, context);
Result composed = RuleEngine.Evaluate(
new IRuleBase<int>[] { ruleA, ruleB }.And(), value);
```

---

## Best Practices

- `array.And()` collects **all** failures; `array.Linear()` stops at the **first** failure
Expand Down
112 changes: 112 additions & 0 deletions .well-known/agent-skills/csharpessentials-these/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
name: csharpessentials-these
description: Use when modeling partial success — These<TError, TValue> holds Left (error only), Right (value only), or Both (error + value), enabling scenarios where a result can partially succeed while carrying warnings. Use FromResult to bridge from Result<T>, ToResult/ToResultLenient to bridge back, and Partition to split collections.
---

# CSharpEssentials.These

`These<TError, TValue>` is a three-state discriminated union: Left (failure), Right (success), or Both (partial success with warning). Unlike `Result<T>`, the Both state lets you carry a value AND an error simultaneously.

## Installation

```bash
dotnet add package CSharpEssentials.These
```

## Namespace

```csharp
using CSharpEssentials.These;
using CSharpEssentials.Errors; // for Error type in bridge methods
```

## Creating These

```csharp
These<string, int> failure = These<string, int>.Left("error");
These<string, int> success = These<string, int>.Right(42);
These<string, int> partial = These<string, int>.Both("warning", 42);
```

## Checking State

```csharp
bool isErr = these.IsLeft;
bool isOk = these.IsRight;
bool isBoth = these.IsBoth;

// Safe access via Maybe<T>
Maybe<int> value = these.GetRight(); // None when IsLeft
Maybe<string> error = these.GetLeft(); // None when IsRight
```

## Pattern Match

```csharp
string msg = these.Match(
onLeft: e => $"Error: {e}",
onRight: v => $"Value: {v}",
onBoth: (e, v) => $"Partial: {v} (warning: {e})");
```

## Transforming

```csharp
// Map transforms the right value; Left passes through unchanged
These<string, int> doubled = these.Map(x => x * 2);

// MapLeft transforms the error; Right passes through unchanged
These<string, int> upper = these.MapLeft(e => e.ToUpper());

// FlatMap chains — Both state is unwrapped (loses the error side)
These<string, string> chained = these.FlatMap(x => These<string, string>.Right(x.ToString()));
```

## Side Effects

```csharp
// Tap fires on Right or Both
these.Tap(v => logger.Log($"Got {v}"));

// TapLeft fires on Left or Both
these.TapLeft(e => logger.LogWarning(e));
```

## Bridge to/from Result

```csharp
// Result<T> → These<Error, T>
These<Error, int> these = TheseExtensions.FromResult(result);

// These<Error, T> → Result<T> (Both = failure)
Result<int> strict = these.ToResult();

// These<Error, T> → Result<T> (Both = success, discards error side)
Result<int> lenient = these.ToResultLenient();
```

## Partition Collections

```csharp
var (lefts, rights, boths) = items.Partition();
// lefts → IReadOnlyList<TError>
// rights → IReadOnlyList<TValue>
// boths → IReadOnlyList<(TError, TValue)>
```

## When to Use

| Scenario | Use |
|----------|-----|
| Operation must fully succeed or fail | `Result<T>` |
| Value may or may not exist | `Maybe<T>` |
| Partial success with a warning to propagate | `These<TError, TValue>` |
| Collecting errors while continuing | `These<TError, TValue>` with Both |

## Best Practices

- Prefer `Match()` over checking `IsLeft`/`IsRight`/`IsBoth` separately — exhaustive and compiler-safe
- `FlatMap` loses the Both state — use it only when the warning from the prior step can be discarded
- `ToResultLenient()` is the lenient bridge: Both → success (value wins, error side discarded)
- `ToResult()` is the strict bridge: Both → failure
- Avoid using `These` as a general-purpose error type — use `Result<T>` for that; `These` is for partial-success semantics
29 changes: 16 additions & 13 deletions .well-known/agent-skills/csharpessentials-time/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: csharpessentials-time
description: Use when you need testable time — IDateTimeProvider wraps .NET's TimeProvider so production code uses TimeProvider.System while tests use FakeTimeProvider to freeze/advance the clock; also provides .ToDateOnly() and .ToTimeOnly() DateTime extension methods.
description: Use when you need testable time — IDateTimeProvider wraps clock access so production code uses DateTimeProvider while tests use the built-in FakeDateTimeProvider to freeze/advance/set the clock; also provides .ToDateOnly() and .ToTimeOnly() DateTime extension methods.
---

# CSharpEssentials.Time
Expand Down Expand Up @@ -69,24 +69,27 @@ public class OrderService

---

## Test with FakeTimeProvider
## Test with FakeDateTimeProvider

`CSharpEssentials.Time` ships a built-in `FakeDateTimeProvider` — no extra NuGet package needed.

```csharp
// Install: dotnet add package Microsoft.Extensions.TimeProvider.Testing
using Microsoft.Extensions.Time.Testing;
var fixed = new DateTimeOffset(2025, 1, 15, 10, 0, 0, TimeSpan.Zero);
var fake = new FakeDateTimeProvider(fixed);

var fake = new FakeTimeProvider();
fake.SetUtcNow(new DateTimeOffset(2025, 1, 15, 10, 0, 0, TimeSpan.Zero));
// Inject as IDateTimeProvider
var svc = new OrderService(fake);

var provider = new DateTimeProvider(fake);
var svc = new OrderService(provider);
// Advance the clock without Thread.Sleep
fake.Advance(TimeSpan.FromHours(2));
fake.UtcNow // → 2025-01-15 12:00:00

var order = svc.Create(cart);
Assert.Equal(new DateOnly(2025, 1, 15), order.DueDate.AddDays(-7));
// Jump to a specific instant
fake.SetTime(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));

// Advance the clock
fake.Advance(TimeSpan.FromHours(2));
Assert.Equal(new TimeOnly(12, 0, 0), provider.UtcNowTime);
// NET6+ only
DateOnly date = fake.UtcNowDate;
TimeOnly time = fake.UtcNowTime;
```

---
Expand Down
39 changes: 39 additions & 0 deletions CSharpEssentials.Maybe/Readme.MD
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,42 @@ Maybe<Config[]> configs = configMaybes.Sequence();
| `Sequence()` | `Maybe<T[]>` | `None` if any element is `None` |
| `Traverse(selector)` | `Maybe<TOut[]>` | Applies selector then sequences |
| `Partition()` | `(T[] Values, int NoneCount)` | Never returns `None` — always splits |

### Exception-safe Creation

```csharp
// Returns None if the factory throws
Maybe<int> result = Maybe<int>.FromTry(() => int.Parse(userInput));
Maybe<User> user = Maybe<User>.FromTry(() => JsonSerializer.Deserialize<User>(json));
```

### TapNone — Side Effect on Absence

```csharp
Maybe<int> maybe = Maybe<int>.None;

maybe.TapNone(() => logger.LogWarning("Value missing")); // called when None

// Async
await maybe.TapNoneAsync(async () => await NotifyAsync("Missing"));

// Via Task<Maybe<T>> / ValueTask<Maybe<T>>
await GetMaybeAsync().TapNoneAsync(() => fallback());
```

### GetValueOrElse — Lazy Fallback

```csharp
// Factory only called when None (unlike GetValueOrDefault which always evaluates)
int value = maybe.GetValueOrElse(() => ComputeExpensiveDefault());
```

### OrElse — Lazy Maybe Chain

```csharp
// Alias for Or(Func<Maybe<T>>) — returns self if Some, calls factory if None
Maybe<Config> config = GetFromCache().OrElse(() => GetFromDatabase());

// Async
Maybe<Config> config = await GetFromCache().OrElseAsync(() => Task.FromResult(GetFromDatabase()));
```
19 changes: 19 additions & 0 deletions CSharpEssentials.Results/Readme.MD
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,22 @@ Result pipeline = new[] { CheckStock(), ReserveItem(), CreateOrder() }
| `Traverse(selector)` | Map then sequence | `Result<TOut[]>` |
| `Partition()` | Split | `(T[] Successes, Error[] Errors)` |
| `FirstFailureOrSuccesses()` | Short-circuit | `Result` or `Result<T[]>` |

### TapIf — Conditional Side Effects

```csharp
Result<int> result = Result<int>.Success(42);

// Predicate-based tap — action fires only when Success AND predicate is true
result.TapIf(v => v > 10, v => logger.Log($"Large value: {v}"));

// Bool condition
result.TapIf(featureEnabled, v => Audit(v));

// Async variants
await result.TapIfAsync(v => v > 10, async v => await AuditAsync(v));
await result.TapIfAsync(isEnabled, async v => await TrackAsync(v));

// Works on Task<Result<T>> and ValueTask<Result<T>>
await GetResultAsync().TapIfAsync(v => v > 0, v => Enqueue(v));
```
25 changes: 25 additions & 0 deletions CSharpEssentials.Rules/Readme.MD
Original file line number Diff line number Diff line change
Expand Up @@ -250,3 +250,28 @@ result.Match(
```

Rules return `Result` / `Result<T>` — the same type used throughout CSharpEssentials — so rule outcomes compose directly with `Then`, `Match`, and other chaining operations.

### FromPredicate / FromPredicateAsync — Inline Rules Without Classes

```csharp
// Static error — reuse the same error for any failing context
IRule<int> positiveRule = RuleEngine.FromPredicate<int>(
x => x > 0,
Error.Validation("Value.Negative", "Must be positive"));

// Error factory — error message can reference the failing value
IRule<int> rangeRule = RuleEngine.FromPredicate<int>(
x => x is >= 1 and <= 100,
x => Error.Validation("Value.OutOfRange", $"Value {x} must be between 1 and 100"));

// Async predicate
IAsyncRule<string> uniqueRule = RuleEngine.FromPredicateAsync<string>(
async s => await _db.IsUniqueAsync(s),
s => Error.Conflict("Name.Taken", $"'{s}' is already taken"));

// Evaluate normally — works with all composition methods
Result r = RuleEngine.Evaluate(positiveRule, 42);
Result composed = RuleEngine.Evaluate(
new IRuleBase<int>[] { positiveRule, rangeRule }.And(),
value);
```
Loading
Loading