Skip to content

Commit 0d53e21

Browse files
committed
Add ValueTask result paths and primitive failures
1 parent 596d108 commit 0d53e21

23 files changed

Lines changed: 1150 additions & 40 deletions

‎AGENTS.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ always check all test are passed.
3333
- For `/plan` requests, perform research and produce the implementation plan without changing feature code, project references, packages, or tests.
3434
- Use `ManagedCode.Orleans.RateLimiting` as the distributed Orleans rate-limiting backend instead of duplicating its grain algorithms; Communication owns the `ICommand`, `Result`/`Problem`, telemetry, and adapter layer.
3535
- Command execution APIs must support handlers returning raw values/tasks by wrapping them into `Result`, and handlers already returning `Result`/`Result<T>` by preserving them unchanged; provide symmetric `Task` and `ValueTask` overloads.
36+
- Async result APIs must preserve the caller's async shape: a `Task` input or handler returns `Task`, while a `ValueTask` input or handler returns `ValueTask`; keep this symmetric across `Result`, collection-result, conversion, and command-execution helpers.
3637
- Do not introduce inline string literals in implementation or test code; declare every string value as a named constant under its canonical owner and reuse an existing owner constant before adding a new one.
3738

3839
# Repository Guidelines

‎Directory.Build.props‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
<RepositoryUrl>https://github.com/managedcode/Communication</RepositoryUrl>
2828
<PackageProjectUrl>https://github.com/managedcode/Communication</PackageProjectUrl>
2929
<Product>Managed Code - Communication</Product>
30-
<Version>10.2.1</Version>
31-
<PackageVersion>10.2.1</PackageVersion>
30+
<Version>10.2.2</Version>
31+
<PackageVersion>10.2.2</PackageVersion>
3232

3333
</PropertyGroup>
3434
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
Lines changed: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
1+
using System;
2+
using System.Threading.Tasks;
3+
4+
namespace ManagedCode.Communication.Extensions;
5+
6+
/// <summary>
7+
/// Allocation-conscious railway overloads for <see cref="ValueTask{TResult}" /> result receivers.
8+
/// </summary>
9+
public static partial class ResultRailwayExtensions
10+
{
11+
/// <summary>Awaits a value-task result, then transforms its value synchronously.</summary>
12+
public static async ValueTask<Result<TOut>> Map<TIn, TOut>(
13+
this ValueTask<Result<TIn>> resultTask,
14+
Func<TIn, TOut> mapper)
15+
{
16+
ArgumentNullException.ThrowIfNull(mapper);
17+
18+
var result = await resultTask.ConfigureAwait(false);
19+
return result.IsSuccess
20+
? Result<TOut>.Succeed(mapper(result.Value))
21+
: result.PropagateFailure<TOut>();
22+
}
23+
24+
/// <summary>Awaits a value-task result, then runs a synchronous result-producing step.</summary>
25+
public static async ValueTask<Result<TOut>> BindAsync<TIn, TOut>(
26+
this ValueTask<Result<TIn>> resultTask,
27+
Func<TIn, Result<TOut>> binder)
28+
{
29+
ArgumentNullException.ThrowIfNull(binder);
30+
31+
var result = await resultTask.ConfigureAwait(false);
32+
return result.IsSuccess ? binder(result.Value) : result.PropagateFailure<TOut>();
33+
}
34+
35+
/// <summary>Alias for the synchronous-step value-task <c>BindAsync</c> overload.</summary>
36+
public static ValueTask<Result<TOut>> ThenAsync<TIn, TOut>(
37+
this ValueTask<Result<TIn>> resultTask,
38+
Func<TIn, Result<TOut>> next)
39+
{
40+
return resultTask.BindAsync(next);
41+
}
42+
43+
/// <summary>Awaits a value-task result, then runs a synchronous side effect on success.</summary>
44+
public static async ValueTask<Result<T>> TapAsync<T>(
45+
this ValueTask<Result<T>> resultTask,
46+
Action<T> action)
47+
{
48+
ArgumentNullException.ThrowIfNull(action);
49+
50+
var result = await resultTask.ConfigureAwait(false);
51+
if (result.IsSuccess)
52+
{
53+
action(result.Value);
54+
}
55+
56+
return result;
57+
}
58+
59+
/// <summary>Alias for the synchronous-action value-task <c>TapAsync</c> overload.</summary>
60+
public static ValueTask<Result<T>> DoAsync<T>(this ValueTask<Result<T>> resultTask, Action<T> action)
61+
{
62+
return resultTask.TapAsync(action);
63+
}
64+
65+
/// <summary>Awaits a value-task result, then recovers from failure synchronously.</summary>
66+
public static async ValueTask<Result<T>> CompensateAsync<T>(
67+
this ValueTask<Result<T>> resultTask,
68+
Func<Problem, Result<T>> recovery)
69+
{
70+
ArgumentNullException.ThrowIfNull(recovery);
71+
72+
var result = await resultTask.ConfigureAwait(false);
73+
return result.IsSuccess ? result : recovery(result.Problem!);
74+
}
75+
76+
/// <summary>Awaits a value-task result, then substitutes a synchronous alternative on failure.</summary>
77+
public static async ValueTask<Result<T>> ElseAsync<T>(
78+
this ValueTask<Result<T>> resultTask,
79+
Func<Result<T>> alternative)
80+
{
81+
ArgumentNullException.ThrowIfNull(alternative);
82+
83+
var result = await resultTask.ConfigureAwait(false);
84+
return result.IsSuccess ? result : alternative();
85+
}
86+
87+
/// <summary>Awaits a value-task result, then runs a synchronous action on either outcome.</summary>
88+
public static async ValueTask<Result<T>> FinallyAsync<T>(
89+
this ValueTask<Result<T>> resultTask,
90+
Action<Result<T>> action)
91+
{
92+
ArgumentNullException.ThrowIfNull(action);
93+
94+
var result = await resultTask.ConfigureAwait(false);
95+
action(result);
96+
return result;
97+
}
98+
99+
/// <summary>Awaits a value-task result, then runs an asynchronous next step.</summary>
100+
public static async ValueTask<Result> ThenAsync(
101+
this ValueTask<Result> resultTask,
102+
Func<ValueTask<Result>> next)
103+
{
104+
ArgumentNullException.ThrowIfNull(next);
105+
106+
var result = await resultTask.ConfigureAwait(false);
107+
return result.IsSuccess ? await next().ConfigureAwait(false) : result;
108+
}
109+
110+
/// <summary>Awaits a value-task result, then introduces a value asynchronously.</summary>
111+
public static async ValueTask<Result<T>> BindAsync<T>(
112+
this ValueTask<Result> resultTask,
113+
Func<ValueTask<Result<T>>> next)
114+
{
115+
ArgumentNullException.ThrowIfNull(next);
116+
117+
var result = await resultTask.ConfigureAwait(false);
118+
return result.IsSuccess
119+
? await next().ConfigureAwait(false)
120+
: Result<T>.Fail(result.Problem!);
121+
}
122+
123+
/// <summary>Awaits a value-task result, then runs an asynchronous side effect on success.</summary>
124+
public static async ValueTask<Result> TapAsync(
125+
this ValueTask<Result> resultTask,
126+
Func<ValueTask> action)
127+
{
128+
ArgumentNullException.ThrowIfNull(action);
129+
130+
var result = await resultTask.ConfigureAwait(false);
131+
if (result.IsSuccess)
132+
{
133+
await action().ConfigureAwait(false);
134+
}
135+
136+
return result;
137+
}
138+
139+
/// <summary>Awaits a value-task result, then substitutes an asynchronous alternative on failure.</summary>
140+
public static async ValueTask<Result> ElseAsync(
141+
this ValueTask<Result> resultTask,
142+
Func<ValueTask<Result>> alternative)
143+
{
144+
ArgumentNullException.ThrowIfNull(alternative);
145+
146+
var result = await resultTask.ConfigureAwait(false);
147+
return result.IsSuccess ? result : await alternative().ConfigureAwait(false);
148+
}
149+
150+
/// <summary>Awaits a value-task result, then runs an asynchronous action on either outcome.</summary>
151+
public static async ValueTask<Result> FinallyAsync(
152+
this ValueTask<Result> resultTask,
153+
Func<Result, ValueTask> action)
154+
{
155+
ArgumentNullException.ThrowIfNull(action);
156+
157+
var result = await resultTask.ConfigureAwait(false);
158+
await action(result).ConfigureAwait(false);
159+
return result;
160+
}
161+
162+
/// <summary>Awaits a value-task result and handles both branches synchronously.</summary>
163+
public static async ValueTask<TOut> MatchAsync<TOut>(
164+
this ValueTask<Result> resultTask,
165+
Func<TOut> onSuccess,
166+
Func<Problem, TOut> onFailure)
167+
{
168+
ArgumentNullException.ThrowIfNull(onSuccess);
169+
ArgumentNullException.ThrowIfNull(onFailure);
170+
171+
var result = await resultTask.ConfigureAwait(false);
172+
return result.IsSuccess ? onSuccess() : onFailure(result.Problem!);
173+
}
174+
175+
/// <summary>Awaits a value-task result, then runs an asynchronous result-producing step on its value.</summary>
176+
public static async ValueTask<Result<TOut>> BindAsync<TIn, TOut>(
177+
this ValueTask<Result<TIn>> resultTask,
178+
Func<TIn, ValueTask<Result<TOut>>> binder)
179+
{
180+
ArgumentNullException.ThrowIfNull(binder);
181+
182+
var result = await resultTask.ConfigureAwait(false);
183+
return result.IsSuccess
184+
? await binder(result.Value).ConfigureAwait(false)
185+
: result.PropagateFailure<TOut>();
186+
}
187+
188+
/// <summary>Alias for the asynchronous value-task <c>BindAsync</c> overload.</summary>
189+
public static ValueTask<Result<TOut>> ThenAsync<TIn, TOut>(
190+
this ValueTask<Result<TIn>> resultTask,
191+
Func<TIn, ValueTask<Result<TOut>>> next)
192+
{
193+
return resultTask.BindAsync(next);
194+
}
195+
196+
/// <summary>Awaits a value-task result, then transforms its value asynchronously.</summary>
197+
public static async ValueTask<Result<TOut>> MapAsync<TIn, TOut>(
198+
this ValueTask<Result<TIn>> resultTask,
199+
Func<TIn, ValueTask<TOut>> mapper)
200+
{
201+
ArgumentNullException.ThrowIfNull(mapper);
202+
203+
var result = await resultTask.ConfigureAwait(false);
204+
return result.IsSuccess
205+
? Result<TOut>.Succeed(await mapper(result.Value).ConfigureAwait(false))
206+
: result.PropagateFailure<TOut>();
207+
}
208+
209+
/// <summary>Awaits a value-task result, then runs an asynchronous step that discards its value.</summary>
210+
public static async ValueTask<Result> BindAsync<T>(
211+
this ValueTask<Result<T>> resultTask,
212+
Func<T, ValueTask<Result>> binder)
213+
{
214+
ArgumentNullException.ThrowIfNull(binder);
215+
216+
var result = await resultTask.ConfigureAwait(false);
217+
return result.IsSuccess
218+
? await binder(result.Value).ConfigureAwait(false)
219+
: Result.Fail(result.Problem!);
220+
}
221+
222+
/// <summary>Awaits a value-task result and applies a synchronous success predicate.</summary>
223+
public static async ValueTask<Result<T>> EnsureAsync<T>(
224+
this ValueTask<Result<T>> resultTask,
225+
Func<T, bool> predicate,
226+
Problem problem)
227+
{
228+
ArgumentNullException.ThrowIfNull(predicate);
229+
ArgumentNullException.ThrowIfNull(problem);
230+
231+
var result = await resultTask.ConfigureAwait(false);
232+
return result.Ensure(predicate, problem);
233+
}
234+
235+
/// <summary>Awaits a value-task result and applies an asynchronous success predicate.</summary>
236+
public static async ValueTask<Result<T>> EnsureAsync<T>(
237+
this ValueTask<Result<T>> resultTask,
238+
Func<T, ValueTask<bool>> predicate,
239+
Problem problem)
240+
{
241+
ArgumentNullException.ThrowIfNull(predicate);
242+
ArgumentNullException.ThrowIfNull(problem);
243+
244+
var result = await resultTask.ConfigureAwait(false);
245+
if (result.IsFailed)
246+
{
247+
return result;
248+
}
249+
250+
return await predicate(result.Value).ConfigureAwait(false) ? result : Result<T>.Fail(problem);
251+
}
252+
253+
/// <summary>Awaits a value-task result, then runs an asynchronous side effect on success.</summary>
254+
public static async ValueTask<Result<T>> DoAsync<T>(
255+
this ValueTask<Result<T>> resultTask,
256+
Func<T, ValueTask> action)
257+
{
258+
ArgumentNullException.ThrowIfNull(action);
259+
260+
var result = await resultTask.ConfigureAwait(false);
261+
if (result.IsSuccess)
262+
{
263+
await action(result.Value).ConfigureAwait(false);
264+
}
265+
266+
return result;
267+
}
268+
269+
/// <summary>Alias for the asynchronous-action value-task <c>DoAsync</c> overload.</summary>
270+
public static ValueTask<Result<T>> TapAsync<T>(
271+
this ValueTask<Result<T>> resultTask,
272+
Func<T, ValueTask> action)
273+
{
274+
return resultTask.DoAsync(action);
275+
}
276+
277+
/// <summary>Awaits a value-task result, then substitutes an asynchronous alternative on failure.</summary>
278+
public static async ValueTask<Result<T>> ElseAsync<T>(
279+
this ValueTask<Result<T>> resultTask,
280+
Func<ValueTask<Result<T>>> alternative)
281+
{
282+
ArgumentNullException.ThrowIfNull(alternative);
283+
284+
var result = await resultTask.ConfigureAwait(false);
285+
return result.IsSuccess ? result : await alternative().ConfigureAwait(false);
286+
}
287+
288+
/// <summary>Awaits a value-task result, then recovers from failure asynchronously.</summary>
289+
public static async ValueTask<Result<T>> CompensateAsync<T>(
290+
this ValueTask<Result<T>> resultTask,
291+
Func<Problem, ValueTask<Result<T>>> recovery)
292+
{
293+
ArgumentNullException.ThrowIfNull(recovery);
294+
295+
var result = await resultTask.ConfigureAwait(false);
296+
return result.IsSuccess ? result : await recovery(result.Problem!).ConfigureAwait(false);
297+
}
298+
299+
/// <summary>Awaits a value-task result, then replaces a failure with a successful default value.</summary>
300+
public static async ValueTask<Result<T>> CompensateWithAsync<T>(
301+
this ValueTask<Result<T>> resultTask,
302+
T defaultValue)
303+
{
304+
var result = await resultTask.ConfigureAwait(false);
305+
return result.IsSuccess ? result : Result<T>.Succeed(defaultValue);
306+
}
307+
308+
/// <summary>Awaits a value-task result, then runs an asynchronous action on either outcome.</summary>
309+
public static async ValueTask<Result<T>> FinallyAsync<T>(
310+
this ValueTask<Result<T>> resultTask,
311+
Func<Result<T>, ValueTask> action)
312+
{
313+
ArgumentNullException.ThrowIfNull(action);
314+
315+
var result = await resultTask.ConfigureAwait(false);
316+
await action(result).ConfigureAwait(false);
317+
return result;
318+
}
319+
320+
/// <summary>Awaits a value-task result and handles both branches synchronously.</summary>
321+
public static async ValueTask<TOut> MatchAsync<TIn, TOut>(
322+
this ValueTask<Result<TIn>> resultTask,
323+
Func<TIn, TOut> onSuccess,
324+
Func<Problem, TOut> onFailure)
325+
{
326+
ArgumentNullException.ThrowIfNull(onSuccess);
327+
ArgumentNullException.ThrowIfNull(onFailure);
328+
329+
var result = await resultTask.ConfigureAwait(false);
330+
return result.IsSuccess ? onSuccess(result.Value) : onFailure(result.Problem!);
331+
}
332+
333+
/// <summary>Awaits a value-task result and handles both branches asynchronously.</summary>
334+
public static async ValueTask<TOut> MatchAsync<TIn, TOut>(
335+
this ValueTask<Result<TIn>> resultTask,
336+
Func<TIn, ValueTask<TOut>> onSuccess,
337+
Func<Problem, ValueTask<TOut>> onFailure)
338+
{
339+
ArgumentNullException.ThrowIfNull(onSuccess);
340+
ArgumentNullException.ThrowIfNull(onFailure);
341+
342+
var result = await resultTask.ConfigureAwait(false);
343+
return result.IsSuccess
344+
? await onSuccess(result.Value).ConfigureAwait(false)
345+
: await onFailure(result.Problem!).ConfigureAwait(false);
346+
}
347+
}

0 commit comments

Comments
 (0)