Skip to content

Commit

Permalink
Created new sample project for presentation
Browse files Browse the repository at this point in the history
  • Loading branch information
stidsborg committed Jan 6, 2025
1 parent 50dd83f commit a7c9b8c
Show file tree
Hide file tree
Showing 45 changed files with 1,191 additions and 7 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Cleipnir.Flows.Sample.MicrosoftOpen.Clients;
using Cleipnir.Flows.Sample.MicrosoftOpen.Flows.MessageDriven;
using Cleipnir.ResilientFunctions.Domain;
using Polly;
using Polly.Retry;

Expand All @@ -14,10 +15,13 @@ ILogisticsClient logisticsClient
{
public override async Task Run(Order order)
{
var transactionId = Guid.NewGuid();
var transactionId = await Effect.Capture(Guid.NewGuid);

await paymentProviderClient.Reserve(transactionId, order.CustomerId, order.TotalPrice);
var trackAndTrace = await logisticsClient.ShipProducts(order.CustomerId, order.ProductIds);
var trackAndTrace = await Effect.Capture(
() => logisticsClient.ShipProducts(order.CustomerId, order.ProductIds),
ResiliencyLevel.AtMostOnce
);
await paymentProviderClient.Capture(transactionId);
await emailClient.SendOrderConfirmation(order.CustomerId, trackAndTrace, order.ProductIds);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>Cleipnir.Flows.Sample.MicrosoftOpen</RootNamespace>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="9.0.0" />
<PackageReference Include="Polly.Core" Version="8.5.0" />
<PackageReference Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.2.0" />
<PackageReference Include="Cleipnir.Flows.PostgresSql" Version="4.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Cleipnir.Flows.Sample.MicrosoftOpen.Clients;

public static class ClientSettings
{
public static TimeSpan Delay { get; } = TimeSpan.FromMilliseconds(100);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Serilog;

namespace Cleipnir.Flows.Sample.MicrosoftOpen.Clients;

public interface IEmailClient
{
Task SendOrderConfirmation(Guid customerId, TrackAndTrace trackAndTrace, IEnumerable<Guid> productIds);

Task<bool> IsServiceDown();
}

public class EmailClientStub : IEmailClient
{
public Task SendOrderConfirmation(Guid customerId, TrackAndTrace trackAndTrace, IEnumerable<Guid> productIds)
=> Task.Delay(ClientSettings.Delay).ContinueWith(_ =>
Log.Logger.ForContext<IEmailClient>().Information("EMAIL_SERVER: Order confirmation emailed")
);

public Task<bool> IsServiceDown() => Task.FromResult(false);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Serilog;

namespace Cleipnir.Flows.Sample.MicrosoftOpen.Clients;

public interface ILogisticsClient
{
Task<TrackAndTrace> ShipProducts(Guid customerId, IEnumerable<Guid> productIds);
Task CancelShipment(TrackAndTrace trackAndTrace);
}

public record TrackAndTrace(string Value);

public class LogisticsClientStub : ILogisticsClient
{
public Task<TrackAndTrace> ShipProducts(Guid customerId, IEnumerable<Guid> productIds)
=> Task.Delay(ClientSettings.Delay).ContinueWith(_ =>
{
Log.Logger.ForContext<ILogisticsClient>().Information("LOGISTICS_SERVER: Products shipped");
return new TrackAndTrace(Guid.NewGuid().ToString());
}
);

public Task CancelShipment(TrackAndTrace trackAndTrace)
=> Task.Delay(ClientSettings.Delay).ContinueWith(_ =>
{
Log.Logger.ForContext<ILogisticsClient>().Information("LOGISTICS_SERVER: Products shipment cancelled");
return new TrackAndTrace(Guid.NewGuid().ToString());
}
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
using Serilog;

namespace Cleipnir.Flows.Sample.MicrosoftOpen.Clients;

public interface IPaymentProviderClient
{
Task Reserve(Guid transactionId, Guid customerId, decimal amount);
Task Capture(Guid transactionId);
Task CancelReservation(Guid transactionId);
Task Reverse(Guid transactionId);
bool IsServiceDown();
}

public class PaymentProviderClientStub : IPaymentProviderClient
{
public Task Reserve(Guid transactionId, Guid customerId, decimal amount)
=> Task.Delay(ClientSettings.Delay).ContinueWith(_ =>
Log.Logger.ForContext<IPaymentProviderClient>().Information($"PAYMENT_PROVIDER: Reserved '{amount}'")
);

public Task Capture(Guid transactionId)
=> Task.Delay(ClientSettings.Delay).ContinueWith(_ =>
Log.Logger.ForContext<IPaymentProviderClient>().Information("PAYMENT_PROVIDER: Reserved amount captured")
);
public Task CancelReservation(Guid transactionId)
=> Task.Delay(ClientSettings.Delay).ContinueWith(_ =>
Log.Logger.ForContext<IPaymentProviderClient>().Information("PAYMENT_PROVIDER: Reservation cancelled")
);

public Task Reverse(Guid transactionId)
=> Task.Delay(ClientSettings.Delay).ContinueWith(_ =>
Log.Logger.ForContext<IPaymentProviderClient>().Information("PAYMENT_PROVIDER: Reservation reversed")
);

public bool IsServiceDown() => false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Cleipnir.Flows.Sample.MicrosoftOpen.Flows.Batch;
using Microsoft.AspNetCore.Mvc;

namespace Cleipnir.Flows.Sample.MicrosoftOpen.Controllers;

[ApiController]
[Route("[controller]")]
public class BatchOrderController(BatchOrderFlows batchOrderFlows) : ControllerBase
{
[HttpPost]
public async Task<ActionResult> Post(OrdersBatch ordersBatch)
{
await batchOrderFlows.Schedule(ordersBatch.BatchId, ordersBatch.Orders);
return Ok();
}

[HttpGet]
public async Task<ActionResult> Get(string batchId)
{
var controlPanel = await batchOrderFlows.ControlPanel(batchId);
if (controlPanel is null)
return NotFound();

return Ok(await controlPanel.ToPrettyString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Cleipnir.Flows.Sample.MicrosoftOpen.Flows.Invoice;
using Microsoft.AspNetCore.Mvc;

namespace Cleipnir.Flows.Sample.MicrosoftOpen.Controllers;

[ApiController]
[Route("[controller]")]
public class CustomerController(InvoiceFlows invoiceFlows) : ControllerBase
{
[HttpPost]
public async Task<ActionResult> Post(int customerNumber)
{
await invoiceFlows.Schedule(customerNumber.ToString(), new CustomerNumber(customerNumber));
return Ok();
}

[HttpGet]
public async Task<ActionResult> Get(int customerNumber)
{
var controlPanel = await invoiceFlows.ControlPanel(customerNumber.ToString());
if (controlPanel is null)
return NotFound();

var str = await controlPanel.ToPrettyString();
return Ok(str);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using Cleipnir.Flows.Sample.MicrosoftOpen.Clients;
using Cleipnir.Flows.Sample.MicrosoftOpen.Flows;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using ILogger = Serilog.ILogger;

namespace Cleipnir.Flows.Sample.MicrosoftOpen.Controllers;

[ApiController]
[Route("[controller]")]
public class OrderController(Flows.Rpc.OrderFlows orderFlows) : ControllerBase
{
private readonly ILogger _logger = Log.Logger.ForContext<OrderController>();

[HttpPost]
public async Task<ActionResult> Post(Order order)
{
_logger.Information("Started processing {OrderId}", order.OrderId);
await orderFlows.Run(order.OrderId, order);
_logger.Information("Completed processing {OrderId}", order.OrderId);
return Ok();
}

[HttpPost("RetryShipProducts")]
public async Task<ActionResult> Post(string orderNumber, string? trackAndTraceNumber)
{
var controlPanel = await orderFlows.ControlPanel(orderNumber);
if (controlPanel is null)
return NotFound();

if (trackAndTraceNumber == null)
await controlPanel.Effects.Remove("ShipProducts");
else
await controlPanel.Effects.SetSucceeded("ShipProducts", new TrackAndTrace(trackAndTraceNumber));

await controlPanel.Restart();
return Ok();
}

[HttpGet]
public async Task<ActionResult> Get(string orderId)
{
var controlPanel = await orderFlows.ControlPanel(orderId);
if (controlPanel is null)
return NotFound();

var str = await controlPanel.ToPrettyString();
return Ok(str);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Cleipnir.Flows.Sample.MicrosoftOpen.Flows;
using Cleipnir.Flows.Sample.MicrosoftOpen.Flows.MessageDriven;
using Microsoft.AspNetCore.Mvc;
using Serilog;
using ILogger = Serilog.ILogger;

namespace Cleipnir.Flows.Sample.MicrosoftOpen.Controllers;

[ApiController]
[Route("[controller]")]
public class OrderMessageDrivenController(MessageDrivenOrderFlows orderFlows) : ControllerBase
{
private readonly ILogger _logger = Log.Logger.ForContext<OrderController>();

[HttpPost]
public async Task<ActionResult> Post(Order order)
{
_logger.Information("Started processing {OrderId}", order.OrderId);
await orderFlows.Schedule(order.OrderId, order);
_logger.Information("Completed processing {OrderId}", order.OrderId);

return Ok();
}

[HttpGet]
public async Task<ActionResult> Get(string orderId)
{
var controlPanel = await orderFlows.ControlPanel(orderId);
if (controlPanel is null)
return NotFound();


return Ok(controlPanel.ToPrettyString());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Text;
using System.Text.Json;
using Cleipnir.ResilientFunctions.Domain;
using Cleipnir.ResilientFunctions.Helpers;

namespace Cleipnir.Flows.Sample.MicrosoftOpen.Controllers;

public static class Utils
{
public static async Task<string> ToPrettyString<TParam, TReturn>(this BaseControlPanel<TParam, TReturn> controlPanel)
{
var effects = controlPanel.Effects;
var effectIds = (await effects.AllIds).OrderBy(effectId => effectId.Serialize()).ToList();

var stringBuilder = new StringBuilder();
stringBuilder.AppendLine("Effects:");
foreach (var effectId in effectIds)
stringBuilder.AppendLine(new
{
Id = effectId.Context == "" ? effectId.Id : effectId.Serialize(),
Result = (await effects.GetResultBytes(effectId))?.ToStringFromUtf8Bytes() ?? "[EMPTY]"
}.ToString()
);
if (!effectIds.Any())
stringBuilder.AppendLine("[None]");

var messages = await controlPanel.Messages.AsObjects;
stringBuilder.AppendLine();
stringBuilder.AppendLine("Messages:");
foreach (var message in messages)
stringBuilder.AppendLine(JsonSerializer.Serialize(message));
if (!messages.Any())
stringBuilder.AppendLine("[None]");

return stringBuilder.ToString();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Cleipnir.ResilientFunctions.Helpers;

namespace Cleipnir.Flows.Sample.MicrosoftOpen.Flows.BankTransfer;

public interface IBankCentralClient
{
Task PostTransaction(Guid transactionId, string account, decimal amount);
Task<decimal> GetAvailableFunds(string account);
}

public class BankCentralClient : IBankCentralClient
{
public Task PostTransaction(Guid transactionId, string account, decimal amount)
{
Console.WriteLine($"POSTING: {amount} to {account} account");
return Task.Delay(1_000).ContinueWith(_ => true);
}

public Task<decimal> GetAvailableFunds(string account) => 100M.ToTask();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace Cleipnir.Flows.Sample.MicrosoftOpen.Flows.BankTransfer;

public record Transfer(
Guid TransactionId,
string FromAccount,
string ToAccount,
decimal Amount
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Cleipnir.ResilientFunctions.Domain;

namespace Cleipnir.Flows.Sample.MicrosoftOpen.Flows.BankTransfer;

[GenerateFlows]
public class TransferFlow : Flow<Transfer>
{
public override async Task Run(Transfer transfer)
{
var availableFunds = await _bankCentralClient.GetAvailableFunds(transfer.FromAccount);
if (availableFunds <= transfer.Amount)
throw new InvalidOperationException("Insufficient funds on from account");

await _bankCentralClient.PostTransaction(
transfer.TransactionId,
transfer.FromAccount,
-transfer.Amount
);

await _bankCentralClient.PostTransaction(
transfer.TransactionId,
transfer.ToAccount,
transfer.Amount
);
}

private DistributedSemaphore DistributedLock(string account)
=> Workflow.Semaphores.Create("BankTransfer", account, maximumCount: 1);

private readonly IBankCentralClient _bankCentralClient = new BankCentralClient();
}
Loading

0 comments on commit a7c9b8c

Please sign in to comment.