Skip to content

Commit 3fc8e84

Browse files
authored
Add methods to Results to make error handling simpler. (#11)
1 parent 33dab61 commit 3fc8e84

File tree

8 files changed

+786
-57
lines changed

8 files changed

+786
-57
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public Person GetAdult(int id)
4141
```
4242

4343
This implementation has two major drawbacks:
44-
1) From a client's perspective, the API is not experessive enough. The method signature gives no indication that it might throw, so the client would need to peek inside to find that out.
44+
1) From a client's perspective, the API is not expressive enough. The method signature gives no indication that it might throw, so the client would need to peek inside to find that out.
4545
2) From an implementer's perspective, the error checking, whilst simple enough in this example, can often grow quite complex. This makes the implementation of the method hard to follow due to the number of conditional branches. We may try factoring out the condition checking blocks into separate methods to solve this problem. This would also allow us to share some of this logic with other parts of the code base. These factored-out methods would then have a signature like `void CheckPersonExists(Person person)`. Again, this signature tells us nothing about the fact that the method might throw an exception. Currently, the compiler is also not able to do the flow analysis necessary to determine that the `person` is not `null` after calling such a method and so we may be left with warnings in the original call site about possible null references, even though we know we've checked for that condition.
4646

4747
These can both be resolved by using a `Result` type and re-writing the method like this:

Winton.DomainModelling.Abstractions.sln.DotSettings

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,7 @@
461461
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
462462
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
463463
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
464+
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
464465
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
465466
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
466467
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>

src/Winton.DomainModelling.Abstractions/AsyncResultExtensions.cs

Lines changed: 244 additions & 26 deletions
Large diffs are not rendered by default.

src/Winton.DomainModelling.Abstractions/Failure.cs

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,18 @@ public Failure(Error error)
2727
/// </summary>
2828
public Error Error { get; }
2929

30+
/// <inheritdoc />
31+
public override Result<TData> Catch(Func<Error, Result<TData>> onFailure)
32+
{
33+
return onFailure(Error);
34+
}
35+
36+
/// <inheritdoc />
37+
public override Task<Result<TData>> Catch(Func<Error, Task<Result<TData>>> onFailure)
38+
{
39+
return onFailure(Error);
40+
}
41+
3042
/// <inheritdoc />
3143
public override Result<TNewData> Combine<TOtherData, TNewData>(
3244
Result<TOtherData> other,
@@ -44,6 +56,32 @@ public override T Match<T>(Func<TData, T> onSuccess, Func<Error, T> onFailure)
4456
return onFailure(Error);
4557
}
4658

59+
/// <inheritdoc />
60+
public override Result<TData> OnFailure(Action onFailure)
61+
{
62+
return OnFailure(_ => onFailure());
63+
}
64+
65+
/// <inheritdoc />
66+
public override Result<TData> OnFailure(Action<Error> onFailure)
67+
{
68+
onFailure(Error);
69+
return this;
70+
}
71+
72+
/// <inheritdoc />
73+
public override Task<Result<TData>> OnFailure(Func<Task> onFailure)
74+
{
75+
return OnFailure(_ => onFailure());
76+
}
77+
78+
/// <inheritdoc />
79+
public override async Task<Result<TData>> OnFailure(Func<Error, Task> onFailure)
80+
{
81+
await onFailure(Error);
82+
return this;
83+
}
84+
4785
/// <inheritdoc />
4886
public override Result<TData> OnSuccess(Action onSuccess)
4987
{
@@ -69,17 +107,29 @@ public override Task<Result<TData>> OnSuccess(Func<TData, Task> onSuccess)
69107
}
70108

71109
/// <inheritdoc />
72-
public override Result<TNewData> Select<TNewData>(Func<TData, TNewData> selectData)
110+
public override Result<TNewData> Select<TNewData>(Func<TData, TNewData> selector)
73111
{
74112
return new Failure<TNewData>(Error);
75113
}
76114

77115
/// <inheritdoc />
78-
public override Task<Result<TNewData>> Select<TNewData>(Func<TData, Task<TNewData>> selectData)
116+
public override Task<Result<TNewData>> Select<TNewData>(Func<TData, Task<TNewData>> selector)
79117
{
80118
return Task.FromResult<Result<TNewData>>(new Failure<TNewData>(Error));
81119
}
82120

121+
/// <inheritdoc />
122+
public override Result<TData> SelectError(Func<Error, Error> selector)
123+
{
124+
return new Failure<TData>(selector(Error));
125+
}
126+
127+
/// <inheritdoc />
128+
public override async Task<Result<TData>> SelectError(Func<Error, Task<Error>> selector)
129+
{
130+
return new Failure<TData>(await selector(Error));
131+
}
132+
83133
/// <inheritdoc />
84134
public override Result<TNextData> Then<TNextData>(Func<TData, Result<TNextData>> onSuccess)
85135
{

src/Winton.DomainModelling.Abstractions/Result.cs

Lines changed: 144 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,40 @@ namespace Winton.DomainModelling
1717
/// </typeparam>
1818
public abstract class Result<TData>
1919
{
20+
/// <summary>
21+
/// Invokes another result generating function which takes as input the error of this result
22+
/// if it is a failure.
23+
/// </summary>
24+
/// <remarks>
25+
/// If this result is a success then this is a no-op and the original success is retained.
26+
/// This is useful for handling errors.
27+
/// </remarks>
28+
/// <param name="onFailure">
29+
/// The function that is invoked if this result is a failure.
30+
/// </param>
31+
/// <returns>
32+
/// If this result is a failure, then the result of the <paramref>onFailure</paramref> function;
33+
/// otherwise the original error.
34+
/// </returns>
35+
public abstract Result<TData> Catch(Func<Error, Result<TData>> onFailure);
36+
37+
/// <summary>
38+
/// Invokes another result generating function which takes as input the error of this result
39+
/// if it is a failure.
40+
/// </summary>
41+
/// <remarks>
42+
/// If this result is a success then this is a no-op and the original success is retained.
43+
/// This is useful for handling errors.
44+
/// </remarks>
45+
/// <param name="onFailure">
46+
/// The function that is invoked if this result is a failure.
47+
/// </param>
48+
/// <returns>
49+
/// If this result is a failure, then the result of the <paramref>onFailure</paramref> function;
50+
/// otherwise the original error.
51+
/// </returns>
52+
public abstract Task<Result<TData>> Catch(Func<Error, Task<Result<TData>>> onFailure);
53+
2054
/// <summary>
2155
/// Combines this result with another.
2256
/// If both are successful then <paramref>combineData</paramref> is invoked;
@@ -63,11 +97,71 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
6397
public abstract T Match<T>(Func<TData, T> onSuccess, Func<Error, T> onFailure);
6498

6599
/// <summary>
66-
/// Invokes the specified action if the result was successful and returns the original result.
100+
/// Invokes the specified action if the result is a failure and returns the original result.
101+
/// </summary>
102+
/// <remarks>
103+
/// If this result is a success then this is a no-op and the original success is retained.
104+
/// This is useful for publishing domain model notifications when an operation fails.
105+
/// </remarks>
106+
/// <param name="onFailure">
107+
/// The action that will be invoked if this result is a failure.
108+
/// </param>
109+
/// <returns>
110+
/// The original result.
111+
/// </returns>
112+
public abstract Result<TData> OnFailure(Action onFailure);
113+
114+
/// <summary>
115+
/// Invokes the specified action if the result is a failure and returns the original result.
116+
/// </summary>
117+
/// <remarks>
118+
/// If this result is a success then this is a no-op and the original success is retained.
119+
/// This is useful for publishing domain model notifications when an operation fails.
120+
/// </remarks>
121+
/// <param name="onFailure">
122+
/// The action that will be invoked if this result is a failure.
123+
/// </param>
124+
/// <returns>
125+
/// The original result.
126+
/// </returns>
127+
public abstract Result<TData> OnFailure(Action<Error> onFailure);
128+
129+
/// <summary>
130+
/// Invokes the specified action if the result is a failure and returns the original result.
131+
/// </summary>
132+
/// <remarks>
133+
/// If this result is a success then this is a no-op and the original success is retained.
134+
/// This is useful for publishing domain model notifications when an operation fails.
135+
/// </remarks>
136+
/// <param name="onFailure">
137+
/// The asynchronous action that will be invoked if this result is a failure.
138+
/// </param>
139+
/// <returns>
140+
/// The original result.
141+
/// </returns>
142+
public abstract Task<Result<TData>> OnFailure(Func<Task> onFailure);
143+
144+
/// <summary>
145+
/// Invokes the specified action if the result is a failure and returns the original result.
146+
/// </summary>
147+
/// <remarks>
148+
/// If this result is a success then this is a no-op and the original success is retained.
149+
/// This is useful for publishing domain model notifications when an operation fails.
150+
/// </remarks>
151+
/// <param name="onFailure">
152+
/// The asynchronous action that will be invoked if this result is a failure.
153+
/// </param>
154+
/// <returns>
155+
/// The original result.
156+
/// </returns>
157+
public abstract Task<Result<TData>> OnFailure(Func<Error, Task> onFailure);
158+
159+
/// <summary>
160+
/// Invokes the specified action if the result is a success and returns the original result.
67161
/// </summary>
68162
/// <remarks>
69163
/// If this result is a failure then this is a no-op and the original failure is retained.
70-
/// This is useful for publishing domain model notifications when an operation has been successful.
164+
/// This is useful for publishing domain model notifications when an operation succeeds.
71165
/// </remarks>
72166
/// <param name="onSuccess">
73167
/// The action that will be invoked if this result represents a success.
@@ -78,11 +172,11 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
78172
public abstract Result<TData> OnSuccess(Action onSuccess);
79173

80174
/// <summary>
81-
/// Invokes the specified action if the result was successful and returns the original result.
175+
/// Invokes the specified action if the result is a success and returns the original result.
82176
/// </summary>
83177
/// <remarks>
84178
/// If this result is a failure then this is a no-op and the original failure is retained.
85-
/// This is useful for publishing domain model notifications when an operation has been successful.
179+
/// This is useful for publishing domain model notifications when an operation succeeds.
86180
/// </remarks>
87181
/// <param name="onSuccess">
88182
/// The action that will be invoked if this result represents a success.
@@ -93,11 +187,11 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
93187
public abstract Result<TData> OnSuccess(Action<TData> onSuccess);
94188

95189
/// <summary>
96-
/// Invokes the specified action if the result was successful and returns the original result.
190+
/// Invokes the specified action if the result is a success and returns the original result.
97191
/// </summary>
98192
/// <remarks>
99193
/// If this result is a failure then this is a no-op and the original failure is retained.
100-
/// This is useful for publishing domain model notifications when an operation has been successful.
194+
/// This is useful for publishing domain model notifications when an operation succeeds.
101195
/// </remarks>
102196
/// <param name="onSuccess">
103197
/// The asynchronous action that will be invoked if this result represents a success.
@@ -108,11 +202,11 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
108202
public abstract Task<Result<TData>> OnSuccess(Func<Task> onSuccess);
109203

110204
/// <summary>
111-
/// Invokes the specified action if the result was successful and returns the original result.
205+
/// Invokes the specified action if the result is a success and returns the original result.
112206
/// </summary>
113207
/// <remarks>
114208
/// If this result is a failure then this is a no-op and the original failure is retained.
115-
/// This is useful for publishing domain model notifications when an operation has been successful.
209+
/// This is useful for publishing domain model notifications when an operation succeeds.
116210
/// </remarks>
117211
/// <param name="onSuccess">
118212
/// The asynchronous action that will be invoked if this result represents a success.
@@ -131,14 +225,14 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
131225
/// <typeparam name="TNewData">
132226
/// The type of data in the new result.
133227
/// </typeparam>
134-
/// <param name="selectData">
228+
/// <param name="selector">
135229
/// The function that is invoked to select the data.
136230
/// </param>
137231
/// <returns>
138-
/// A new result containing either; the output of the <paramref>selectData</paramref> function
139-
/// if this result is a success, otherwise the original error.
232+
/// A new result containing either; the output of the <paramref>selector</paramref> function
233+
/// if this result is a success, otherwise the original failure.
140234
/// </returns>
141-
public abstract Result<TNewData> Select<TNewData>(Func<TData, TNewData> selectData);
235+
public abstract Result<TNewData> Select<TNewData>(Func<TData, TNewData> selector);
142236

143237
/// <summary>
144238
/// Projects a successful result's data from one type to another.
@@ -149,18 +243,48 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
149243
/// <typeparam name="TNewData">
150244
/// The type of data in the new result.
151245
/// </typeparam>
152-
/// <param name="selectData">
246+
/// <param name="selector">
153247
/// The asynchronous function that is invoked to select the data.
154248
/// </param>
155249
/// <returns>
156-
/// A new result containing either; the output of the <paramref>selectData</paramref> function
157-
/// if this result is a success, otherwise the original error.
250+
/// A new result containing either; the output of the <paramref>selector</paramref> function
251+
/// if this result is a success, otherwise the original failure.
252+
/// </returns>
253+
public abstract Task<Result<TNewData>> Select<TNewData>(Func<TData, Task<TNewData>> selector);
254+
255+
/// <summary>
256+
/// Projects a failed result's error from one type to another.
257+
/// </summary>
258+
/// <remarks>
259+
/// If this result is a success then this is a no-op.
260+
/// </remarks>
261+
/// <param name="selector">
262+
/// The function that is invoked to select the error.
263+
/// </param>
264+
/// <returns>
265+
/// A new result containing either; the output of the <paramref>selector</paramref> function
266+
/// if this result is a failure, otherwise the original success.
158267
/// </returns>
159-
public abstract Task<Result<TNewData>> Select<TNewData>(Func<TData, Task<TNewData>> selectData);
268+
public abstract Result<TData> SelectError(Func<Error, Error> selector);
269+
270+
/// <summary>
271+
/// Projects a failed result's error from one type to another.
272+
/// </summary>
273+
/// <remarks>
274+
/// If this result is a success then this is a no-op.
275+
/// </remarks>
276+
/// <param name="selector">
277+
/// The asynchronous function that is invoked to select the error.
278+
/// </param>
279+
/// <returns>
280+
/// A new result containing either; the output of the <paramref>selector</paramref> function
281+
/// if this result is a failure, otherwise the original success.
282+
/// </returns>
283+
public abstract Task<Result<TData>> SelectError(Func<Error, Task<Error>> selector);
160284

161285
/// <summary>
162286
/// Invokes another result generating function which takes as input the data of this result
163-
/// if it was successful.
287+
/// if it is a success.
164288
/// </summary>
165289
/// <remarks>
166290
/// If this result is a failure then this is a no-op and the original failure is retained.
@@ -170,7 +294,7 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
170294
/// The type of data in the new result.
171295
/// </typeparam>
172296
/// <param name="onSuccess">
173-
/// The function that is invoked if this result represents a success.
297+
/// The function that is invoked if this result is a success.
174298
/// </param>
175299
/// <returns>
176300
/// If this result is a success, then the result of <paramref>onSuccess</paramref> function;
@@ -180,7 +304,7 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
180304

181305
/// <summary>
182306
/// Invokes another result generating function which takes as input the data of this result
183-
/// if it was successful.
307+
/// if it is a success.
184308
/// </summary>
185309
/// <remarks>
186310
/// If this result is a failure then this is a no-op and the original failure is retained.
@@ -190,7 +314,7 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
190314
/// The type of data in the new result.
191315
/// </typeparam>
192316
/// <param name="onSuccess">
193-
/// The asynchronous function that is invoked if this result represents a success.
317+
/// The asynchronous function that is invoked if this result is a success.
194318
/// </param>
195319
/// <returns>
196320
/// If this result is a success, then the result of <paramref>onSuccess</paramref> function;

0 commit comments

Comments
 (0)