Skip to content

Commit

Permalink
Add methods to Results to make error handling simpler. (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
Choc13 authored Aug 19, 2019
1 parent 33dab61 commit 3fc8e84
Show file tree
Hide file tree
Showing 8 changed files with 786 additions and 57 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ public Person GetAdult(int id)
```

This implementation has two major drawbacks:
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.
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.
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.

These can both be resolved by using a `Result` type and re-writing the method like this:
Expand Down
1 change: 1 addition & 0 deletions Winton.DomainModelling.Abstractions.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,7 @@
<s:String x:Key="/Default/CodeStyle/Naming/XamlNaming/UserRules/=XAML_005FRESOURCE/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /&gt;</s:String>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpKeepExistingMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpPlaceEmbeddedOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002ECSharpPlaceAttributeOnSameLineMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
Expand Down
270 changes: 244 additions & 26 deletions src/Winton.DomainModelling.Abstractions/AsyncResultExtensions.cs

Large diffs are not rendered by default.

54 changes: 52 additions & 2 deletions src/Winton.DomainModelling.Abstractions/Failure.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ public Failure(Error error)
/// </summary>
public Error Error { get; }

/// <inheritdoc />
public override Result<TData> Catch(Func<Error, Result<TData>> onFailure)
{
return onFailure(Error);
}

/// <inheritdoc />
public override Task<Result<TData>> Catch(Func<Error, Task<Result<TData>>> onFailure)
{
return onFailure(Error);
}

/// <inheritdoc />
public override Result<TNewData> Combine<TOtherData, TNewData>(
Result<TOtherData> other,
Expand All @@ -44,6 +56,32 @@ public override T Match<T>(Func<TData, T> onSuccess, Func<Error, T> onFailure)
return onFailure(Error);
}

/// <inheritdoc />
public override Result<TData> OnFailure(Action onFailure)
{
return OnFailure(_ => onFailure());
}

/// <inheritdoc />
public override Result<TData> OnFailure(Action<Error> onFailure)
{
onFailure(Error);
return this;
}

/// <inheritdoc />
public override Task<Result<TData>> OnFailure(Func<Task> onFailure)
{
return OnFailure(_ => onFailure());
}

/// <inheritdoc />
public override async Task<Result<TData>> OnFailure(Func<Error, Task> onFailure)
{
await onFailure(Error);
return this;
}

/// <inheritdoc />
public override Result<TData> OnSuccess(Action onSuccess)
{
Expand All @@ -69,17 +107,29 @@ public override Task<Result<TData>> OnSuccess(Func<TData, Task> onSuccess)
}

/// <inheritdoc />
public override Result<TNewData> Select<TNewData>(Func<TData, TNewData> selectData)
public override Result<TNewData> Select<TNewData>(Func<TData, TNewData> selector)
{
return new Failure<TNewData>(Error);
}

/// <inheritdoc />
public override Task<Result<TNewData>> Select<TNewData>(Func<TData, Task<TNewData>> selectData)
public override Task<Result<TNewData>> Select<TNewData>(Func<TData, Task<TNewData>> selector)
{
return Task.FromResult<Result<TNewData>>(new Failure<TNewData>(Error));
}

/// <inheritdoc />
public override Result<TData> SelectError(Func<Error, Error> selector)
{
return new Failure<TData>(selector(Error));
}

/// <inheritdoc />
public override async Task<Result<TData>> SelectError(Func<Error, Task<Error>> selector)
{
return new Failure<TData>(await selector(Error));
}

/// <inheritdoc />
public override Result<TNextData> Then<TNextData>(Func<TData, Result<TNextData>> onSuccess)
{
Expand Down
164 changes: 144 additions & 20 deletions src/Winton.DomainModelling.Abstractions/Result.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,40 @@ namespace Winton.DomainModelling
/// </typeparam>
public abstract class Result<TData>
{
/// <summary>
/// Invokes another result generating function which takes as input the error of this result
/// if it is a failure.
/// </summary>
/// <remarks>
/// If this result is a success then this is a no-op and the original success is retained.
/// This is useful for handling errors.
/// </remarks>
/// <param name="onFailure">
/// The function that is invoked if this result is a failure.
/// </param>
/// <returns>
/// If this result is a failure, then the result of the <paramref>onFailure</paramref> function;
/// otherwise the original error.
/// </returns>
public abstract Result<TData> Catch(Func<Error, Result<TData>> onFailure);

/// <summary>
/// Invokes another result generating function which takes as input the error of this result
/// if it is a failure.
/// </summary>
/// <remarks>
/// If this result is a success then this is a no-op and the original success is retained.
/// This is useful for handling errors.
/// </remarks>
/// <param name="onFailure">
/// The function that is invoked if this result is a failure.
/// </param>
/// <returns>
/// If this result is a failure, then the result of the <paramref>onFailure</paramref> function;
/// otherwise the original error.
/// </returns>
public abstract Task<Result<TData>> Catch(Func<Error, Task<Result<TData>>> onFailure);

/// <summary>
/// Combines this result with another.
/// If both are successful then <paramref>combineData</paramref> is invoked;
Expand Down Expand Up @@ -63,11 +97,71 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
public abstract T Match<T>(Func<TData, T> onSuccess, Func<Error, T> onFailure);

/// <summary>
/// Invokes the specified action if the result was successful and returns the original result.
/// Invokes the specified action if the result is a failure and returns the original result.
/// </summary>
/// <remarks>
/// If this result is a success then this is a no-op and the original success is retained.
/// This is useful for publishing domain model notifications when an operation fails.
/// </remarks>
/// <param name="onFailure">
/// The action that will be invoked if this result is a failure.
/// </param>
/// <returns>
/// The original result.
/// </returns>
public abstract Result<TData> OnFailure(Action onFailure);

/// <summary>
/// Invokes the specified action if the result is a failure and returns the original result.
/// </summary>
/// <remarks>
/// If this result is a success then this is a no-op and the original success is retained.
/// This is useful for publishing domain model notifications when an operation fails.
/// </remarks>
/// <param name="onFailure">
/// The action that will be invoked if this result is a failure.
/// </param>
/// <returns>
/// The original result.
/// </returns>
public abstract Result<TData> OnFailure(Action<Error> onFailure);

/// <summary>
/// Invokes the specified action if the result is a failure and returns the original result.
/// </summary>
/// <remarks>
/// If this result is a success then this is a no-op and the original success is retained.
/// This is useful for publishing domain model notifications when an operation fails.
/// </remarks>
/// <param name="onFailure">
/// The asynchronous action that will be invoked if this result is a failure.
/// </param>
/// <returns>
/// The original result.
/// </returns>
public abstract Task<Result<TData>> OnFailure(Func<Task> onFailure);

/// <summary>
/// Invokes the specified action if the result is a failure and returns the original result.
/// </summary>
/// <remarks>
/// If this result is a success then this is a no-op and the original success is retained.
/// This is useful for publishing domain model notifications when an operation fails.
/// </remarks>
/// <param name="onFailure">
/// The asynchronous action that will be invoked if this result is a failure.
/// </param>
/// <returns>
/// The original result.
/// </returns>
public abstract Task<Result<TData>> OnFailure(Func<Error, Task> onFailure);

/// <summary>
/// Invokes the specified action if the result is a success and returns the original result.
/// </summary>
/// <remarks>
/// If this result is a failure then this is a no-op and the original failure is retained.
/// This is useful for publishing domain model notifications when an operation has been successful.
/// This is useful for publishing domain model notifications when an operation succeeds.
/// </remarks>
/// <param name="onSuccess">
/// The action that will be invoked if this result represents a success.
Expand All @@ -78,11 +172,11 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
public abstract Result<TData> OnSuccess(Action onSuccess);

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

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

/// <summary>
/// Invokes the specified action if the result was successful and returns the original result.
/// Invokes the specified action if the result is a success and returns the original result.
/// </summary>
/// <remarks>
/// If this result is a failure then this is a no-op and the original failure is retained.
/// This is useful for publishing domain model notifications when an operation has been successful.
/// This is useful for publishing domain model notifications when an operation succeeds.
/// </remarks>
/// <param name="onSuccess">
/// The asynchronous action that will be invoked if this result represents a success.
Expand All @@ -131,14 +225,14 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
/// <typeparam name="TNewData">
/// The type of data in the new result.
/// </typeparam>
/// <param name="selectData">
/// <param name="selector">
/// The function that is invoked to select the data.
/// </param>
/// <returns>
/// A new result containing either; the output of the <paramref>selectData</paramref> function
/// if this result is a success, otherwise the original error.
/// A new result containing either; the output of the <paramref>selector</paramref> function
/// if this result is a success, otherwise the original failure.
/// </returns>
public abstract Result<TNewData> Select<TNewData>(Func<TData, TNewData> selectData);
public abstract Result<TNewData> Select<TNewData>(Func<TData, TNewData> selector);

/// <summary>
/// Projects a successful result's data from one type to another.
Expand All @@ -149,18 +243,48 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
/// <typeparam name="TNewData">
/// The type of data in the new result.
/// </typeparam>
/// <param name="selectData">
/// <param name="selector">
/// The asynchronous function that is invoked to select the data.
/// </param>
/// <returns>
/// A new result containing either; the output of the <paramref>selectData</paramref> function
/// if this result is a success, otherwise the original error.
/// A new result containing either; the output of the <paramref>selector</paramref> function
/// if this result is a success, otherwise the original failure.
/// </returns>
public abstract Task<Result<TNewData>> Select<TNewData>(Func<TData, Task<TNewData>> selector);

/// <summary>
/// Projects a failed result's error from one type to another.
/// </summary>
/// <remarks>
/// If this result is a success then this is a no-op.
/// </remarks>
/// <param name="selector">
/// The function that is invoked to select the error.
/// </param>
/// <returns>
/// A new result containing either; the output of the <paramref>selector</paramref> function
/// if this result is a failure, otherwise the original success.
/// </returns>
public abstract Task<Result<TNewData>> Select<TNewData>(Func<TData, Task<TNewData>> selectData);
public abstract Result<TData> SelectError(Func<Error, Error> selector);

/// <summary>
/// Projects a failed result's error from one type to another.
/// </summary>
/// <remarks>
/// If this result is a success then this is a no-op.
/// </remarks>
/// <param name="selector">
/// The asynchronous function that is invoked to select the error.
/// </param>
/// <returns>
/// A new result containing either; the output of the <paramref>selector</paramref> function
/// if this result is a failure, otherwise the original success.
/// </returns>
public abstract Task<Result<TData>> SelectError(Func<Error, Task<Error>> selector);

/// <summary>
/// Invokes another result generating function which takes as input the data of this result
/// if it was successful.
/// if it is a success.
/// </summary>
/// <remarks>
/// If this result is a failure then this is a no-op and the original failure is retained.
Expand All @@ -170,7 +294,7 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
/// The type of data in the new result.
/// </typeparam>
/// <param name="onSuccess">
/// The function that is invoked if this result represents a success.
/// The function that is invoked if this result is a success.
/// </param>
/// <returns>
/// If this result is a success, then the result of <paramref>onSuccess</paramref> function;
Expand All @@ -180,7 +304,7 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(

/// <summary>
/// Invokes another result generating function which takes as input the data of this result
/// if it was successful.
/// if it is a success.
/// </summary>
/// <remarks>
/// If this result is a failure then this is a no-op and the original failure is retained.
Expand All @@ -190,7 +314,7 @@ public abstract Result<TNewData> Combine<TOtherData, TNewData>(
/// The type of data in the new result.
/// </typeparam>
/// <param name="onSuccess">
/// The asynchronous function that is invoked if this result represents a success.
/// The asynchronous function that is invoked if this result is a success.
/// </param>
/// <returns>
/// If this result is a success, then the result of <paramref>onSuccess</paramref> function;
Expand Down
Loading

0 comments on commit 3fc8e84

Please sign in to comment.