add fp3 sample
This commit is contained in:
@@ -0,0 +1,32 @@
|
|||||||
|
using CsharpFp3.Domain;
|
||||||
|
using CsharpFp3.Infrastructure;
|
||||||
|
using CsharpFp3.Library;
|
||||||
|
|
||||||
|
namespace CsharpFp3.Application;
|
||||||
|
|
||||||
|
using CreateResultType = Result<Func<Guid, decimal, Result<Account, AccountError>>, AppError>;
|
||||||
|
|
||||||
|
public abstract record AppError
|
||||||
|
{
|
||||||
|
public sealed record RepositoryCreationFailed(string Message) : AppError;
|
||||||
|
|
||||||
|
public sealed record InnerAccountError(AccountError InnerError) : AppError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AccountApplication
|
||||||
|
{
|
||||||
|
public static CreateResultType CreateWithdrawMoney(Repository repo) =>
|
||||||
|
CreateResultType.Ok(
|
||||||
|
(accountId, amount) =>
|
||||||
|
repo.LoadAccount(accountId)
|
||||||
|
.Log("App load", "Account loaded")
|
||||||
|
.Log("App exec wdrwl", $"[App] Executing withdrawal of {amount:0.00}...")
|
||||||
|
.Bind(account => AccountDomain.Withdraw(account, new Money(amount)))
|
||||||
|
.Log(
|
||||||
|
"App wdrwl done",
|
||||||
|
a => $"Withdrawal applied. New balance: {a.Balance.Amount:0.00}"
|
||||||
|
)
|
||||||
|
.Bind(repo.SaveAccount)
|
||||||
|
.Log("App acc svd", "[App] Account persisted.")
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using CsharpFp3.Library;
|
||||||
|
|
||||||
|
namespace CsharpFp3.Domain;
|
||||||
|
|
||||||
|
public sealed record Account(Guid Id, Money Balance);
|
||||||
|
|
||||||
|
public abstract record AccountError
|
||||||
|
{
|
||||||
|
public sealed record OpeningBalanceMustBeNonNegative : AccountError;
|
||||||
|
|
||||||
|
public sealed record AmountMustBePositive : AccountError;
|
||||||
|
|
||||||
|
public sealed record InsufficientBalance(Money Balance, Money Amount) : AccountError;
|
||||||
|
|
||||||
|
public sealed record AccountNotFound(Guid AccountId) : AccountError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class AccountDomain
|
||||||
|
{
|
||||||
|
public static Result<Account, AccountError> Open(Guid id, Money openingBalance) =>
|
||||||
|
openingBalance.Amount < 0m
|
||||||
|
? Result<Account, AccountError>.Fail(new AccountError.OpeningBalanceMustBeNonNegative())
|
||||||
|
: Result<Account, AccountError>.Ok(new Account(id, openingBalance));
|
||||||
|
|
||||||
|
public static Result<Account, AccountError> Withdraw(Account account, Money amount) =>
|
||||||
|
(account.Balance.Amount, amount.Amount) switch
|
||||||
|
{
|
||||||
|
(_, <= 0m) => Result<Account, AccountError>.Fail(
|
||||||
|
new AccountError.AmountMustBePositive()
|
||||||
|
),
|
||||||
|
|
||||||
|
var (balance, transaction) when balance < transaction => Result<
|
||||||
|
Account,
|
||||||
|
AccountError
|
||||||
|
>.Fail(new AccountError.InsufficientBalance(account.Balance, amount)),
|
||||||
|
|
||||||
|
var (balance, transaction) => Result<Account, AccountError>.Ok(
|
||||||
|
account with
|
||||||
|
{
|
||||||
|
Balance = new Money(balance - transaction),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace CsharpFp3.Domain;
|
||||||
|
|
||||||
|
public sealed record Money(decimal Amount);
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
using CsharpFp3.Domain;
|
||||||
|
using CsharpFp3.Library;
|
||||||
|
using static CsharpFp3.Library.Logging;
|
||||||
|
|
||||||
|
namespace CsharpFp3.Infrastructure;
|
||||||
|
|
||||||
|
public sealed record Repository(
|
||||||
|
Func<Guid, Result<Account, AccountError>> LoadAccount,
|
||||||
|
Func<Account, Result<Account, AccountError>> SaveAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
public static class InMemoryAccountRepository
|
||||||
|
{
|
||||||
|
public static Repository Create()
|
||||||
|
{
|
||||||
|
Dictionary<Guid, Account> store = new();
|
||||||
|
|
||||||
|
Result<Account, AccountError> GetById(Guid id) =>
|
||||||
|
store.TryGetValue(id, out var account)
|
||||||
|
? LogReturn(
|
||||||
|
$"[Repo] Loaded account {id}",
|
||||||
|
Result<Account, AccountError>.Ok(account)
|
||||||
|
)
|
||||||
|
: LogReturn(
|
||||||
|
$"[Repo] Account {id} not found",
|
||||||
|
Result<Account, AccountError>.Fail(new AccountError.AccountNotFound(id))
|
||||||
|
);
|
||||||
|
|
||||||
|
Result<Account, AccountError> Save(Account account)
|
||||||
|
{
|
||||||
|
store[account.Id] = account;
|
||||||
|
|
||||||
|
// no failure modes here
|
||||||
|
return LogReturn(
|
||||||
|
$"[Repo] Saved account {account.Id} with balance {account.Balance.Amount:0.00}",
|
||||||
|
Result<Account, AccountError>.Ok(account)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// What if something goes wrong?
|
||||||
|
// throw new Exception("Something went wrong");
|
||||||
|
return new Repository(GetById, Save);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
namespace CsharpFp3.Library;
|
||||||
|
|
||||||
|
public static class Logging
|
||||||
|
{
|
||||||
|
public static T LogReturn<T>(string message, T r)
|
||||||
|
{
|
||||||
|
Console.WriteLine(message);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Action<T> Output<T>(string src, string message) =>
|
||||||
|
x =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[{src}] {message} | {x}");
|
||||||
|
};
|
||||||
|
|
||||||
|
public static void OutputError<T>(string src, T error)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"\e[1;31m[{src} ERROR]\e[0m {error}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
namespace CsharpFp3.Library;
|
||||||
|
|
||||||
|
public readonly record struct Result<T, E>
|
||||||
|
{
|
||||||
|
private readonly T _value;
|
||||||
|
|
||||||
|
private readonly E _error;
|
||||||
|
|
||||||
|
public bool IsSuccess { get; }
|
||||||
|
|
||||||
|
public bool IsFailure => !IsSuccess;
|
||||||
|
|
||||||
|
public T Value =>
|
||||||
|
IsSuccess
|
||||||
|
? _value
|
||||||
|
: throw new InvalidOperationException("No value present for failed result.");
|
||||||
|
|
||||||
|
public E Error =>
|
||||||
|
IsFailure
|
||||||
|
? _error
|
||||||
|
: throw new InvalidOperationException("No error present for successful result.");
|
||||||
|
|
||||||
|
private Result(T value)
|
||||||
|
{
|
||||||
|
_value = value;
|
||||||
|
_error = default!;
|
||||||
|
IsSuccess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result(E error)
|
||||||
|
{
|
||||||
|
_value = default!;
|
||||||
|
_error = error;
|
||||||
|
IsSuccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result<T, E> Ok(T value) => new(value);
|
||||||
|
|
||||||
|
public static Result<T, E> Fail(E error) => new(error);
|
||||||
|
|
||||||
|
public TResult Match<TResult>(Func<T, TResult> onSuccess, Func<E, TResult> onFailure) =>
|
||||||
|
IsSuccess ? onSuccess(_value) : onFailure(_error);
|
||||||
|
|
||||||
|
public void Switch(Action<T> onSuccess, Action<E> onFailure)
|
||||||
|
{
|
||||||
|
if (IsSuccess)
|
||||||
|
onSuccess(_value);
|
||||||
|
else
|
||||||
|
onFailure(_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result<T, E> Catch(Func<T> f, Func<Exception, E> exceptionMapper)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = f();
|
||||||
|
return Result<T, E>.Ok(result);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return Result<T, E>.Fail(exceptionMapper(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ResultExtensions
|
||||||
|
{
|
||||||
|
public static Result<TOut, E> Bind<TIn, TOut, E>(
|
||||||
|
this Result<TIn, E> result,
|
||||||
|
Func<TIn, Result<TOut, E>> binder
|
||||||
|
) => result.IsSuccess ? binder(result.Value) : Result<TOut, E>.Fail(result.Error);
|
||||||
|
|
||||||
|
public static Result<TOut, E> Map<TIn, TOut, E>(
|
||||||
|
this Result<TIn, E> result,
|
||||||
|
Func<TIn, TOut> mapper
|
||||||
|
) =>
|
||||||
|
result.IsSuccess
|
||||||
|
? Result<TOut, E>.Ok(mapper(result.Value))
|
||||||
|
: Result<TOut, E>.Fail(result.Error);
|
||||||
|
|
||||||
|
public static Result<T, E> Tap<T, E>(this Result<T, E> result, Action<T> action)
|
||||||
|
{
|
||||||
|
if (result.IsSuccess)
|
||||||
|
action(result.Value);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result<T, E> TapError<T, E>(this Result<T, E> result, Action<E> action)
|
||||||
|
{
|
||||||
|
if (result.IsFailure)
|
||||||
|
action(result.Error);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Result<T, EOut> MapError<T, EIn, EOut>(
|
||||||
|
this Result<T, EIn> result,
|
||||||
|
Func<EIn, EOut> map
|
||||||
|
) =>
|
||||||
|
result.IsSuccess
|
||||||
|
? Result<T, EOut>.Ok(result.Value)
|
||||||
|
: Result<T, EOut>.Fail(map(result.Error));
|
||||||
|
|
||||||
|
public static Result<T, E> Log<T, E>(this Result<T, E> result, string src, string msg) =>
|
||||||
|
Tap(result, Logging.Output<T>(src, msg)).TapError(m => Logging.OutputError(src, m));
|
||||||
|
|
||||||
|
public static Result<T, E> Log<T, E>(
|
||||||
|
this Result<T, E> result,
|
||||||
|
string src,
|
||||||
|
Func<T, string> renderText
|
||||||
|
) =>
|
||||||
|
Tap(result, x => Logging.Output<T>(src, renderText(x))(x))
|
||||||
|
.TapError(m => Logging.OutputError(src, m));
|
||||||
|
|
||||||
|
public static Result<TOut, E> Select<TIn, TOut, E>(
|
||||||
|
this Result<TIn, E> result,
|
||||||
|
Func<TIn, TOut> mapper
|
||||||
|
) => result.Map(mapper);
|
||||||
|
|
||||||
|
public static Result<TOut, E> SelectMany<TIn, TIntermediate, TOut, E>(
|
||||||
|
this Result<TIn, E> result,
|
||||||
|
Func<TIn, Result<TIntermediate, E>> binder,
|
||||||
|
Func<TIn, TIntermediate, TOut> projector
|
||||||
|
) => result.Bind(x => binder(x).Map(y => projector(x, y)));
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
using CsharpFp3.Application;
|
||||||
|
using CsharpFp3.Domain;
|
||||||
|
using CsharpFp3.Infrastructure;
|
||||||
|
using CsharpFp3.Library;
|
||||||
|
|
||||||
|
Console.WriteLine("[csharp-fp3] Starting withdraw money demo...");
|
||||||
|
|
||||||
|
var accountId = Guid.NewGuid();
|
||||||
|
|
||||||
|
// try changing this to 250m to see an error being handled
|
||||||
|
var withdrawalAmount = 100m;
|
||||||
|
|
||||||
|
Result<Repository, AppError>
|
||||||
|
.Catch(
|
||||||
|
InMemoryAccountRepository.Create,
|
||||||
|
ex => new AppError.RepositoryCreationFailed(ex.Message)
|
||||||
|
)
|
||||||
|
.Bind(repo =>
|
||||||
|
AccountApplication
|
||||||
|
.CreateWithdrawMoney(repo)
|
||||||
|
.Bind(withdrawMoney =>
|
||||||
|
AccountDomain
|
||||||
|
.Open(accountId, new Money(200m))
|
||||||
|
.Log(
|
||||||
|
"csharp-fp3 seed",
|
||||||
|
account =>
|
||||||
|
$"Seeding account {account.Id} with opening balance {account.Balance}"
|
||||||
|
)
|
||||||
|
.Bind(repo.SaveAccount)
|
||||||
|
.Log(
|
||||||
|
"csharp-fp3 exec",
|
||||||
|
$"Executing withdrawal {withdrawalAmount:0.00} from account {accountId}"
|
||||||
|
)
|
||||||
|
.Bind<Account, Account, AccountError>(account =>
|
||||||
|
withdrawMoney(account.Id, withdrawalAmount)
|
||||||
|
)
|
||||||
|
.Log("csharp-fp3 new balance", account => $"New balance is {account.Balance}")
|
||||||
|
.MapError(ae => (AppError)new AppError.InnerAccountError(ae))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.Log("csharp-fp3 done", "Demo completed.");
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<RootNamespace>CsharpFp3</RootNamespace>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
+15
@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp-fp2", "csharp-fp2\cs
|
|||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp-fp2.Tests", "csharp-fp2.Tests\csharp-fp2.Tests.csproj", "{4AFF063B-7125-4783-BE84-E1E9B4E2A462}"
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp-fp2.Tests", "csharp-fp2.Tests\csharp-fp2.Tests.csproj", "{4AFF063B-7125-4783-BE84-E1E9B4E2A462}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "csharp-fp3", "csharp-fp3\csharp-fp3.csproj", "{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -115,6 +117,18 @@ Global
|
|||||||
{4AFF063B-7125-4783-BE84-E1E9B4E2A462}.Release|x64.Build.0 = Release|Any CPU
|
{4AFF063B-7125-4783-BE84-E1E9B4E2A462}.Release|x64.Build.0 = Release|Any CPU
|
||||||
{4AFF063B-7125-4783-BE84-E1E9B4E2A462}.Release|x86.ActiveCfg = Release|Any CPU
|
{4AFF063B-7125-4783-BE84-E1E9B4E2A462}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
{4AFF063B-7125-4783-BE84-E1E9B4E2A462}.Release|x86.Build.0 = Release|Any CPU
|
{4AFF063B-7125-4783-BE84-E1E9B4E2A462}.Release|x86.Build.0 = Release|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Debug|x64.Build.0 = Debug|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Debug|x86.Build.0 = Debug|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Release|x64.ActiveCfg = Release|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Release|x64.Build.0 = Release|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Release|x86.ActiveCfg = Release|Any CPU
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7}.Release|x86.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
@@ -127,5 +141,6 @@ Global
|
|||||||
{C9D46510-994A-4C43-BA8F-33CA9BED79D0} = {89C53051-77F9-4C1C-ABE5-6D54AB398471}
|
{C9D46510-994A-4C43-BA8F-33CA9BED79D0} = {89C53051-77F9-4C1C-ABE5-6D54AB398471}
|
||||||
{10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A} = {89C53051-77F9-4C1C-ABE5-6D54AB398471}
|
{10A6FD3E-117A-4C9E-98E2-DAC31E7CE78A} = {89C53051-77F9-4C1C-ABE5-6D54AB398471}
|
||||||
{4AFF063B-7125-4783-BE84-E1E9B4E2A462} = {89C53051-77F9-4C1C-ABE5-6D54AB398471}
|
{4AFF063B-7125-4783-BE84-E1E9B4E2A462} = {89C53051-77F9-4C1C-ABE5-6D54AB398471}
|
||||||
|
{83AFEF87-AAC9-45F4-B803-1BCDF0443BF7} = {89C53051-77F9-4C1C-ABE5-6D54AB398471}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
EndGlobal
|
EndGlobal
|
||||||
|
|||||||
Reference in New Issue
Block a user