Skip to content

Commit 0f9b20d

Browse files
Add transaction capabilities
1 parent 5dc06f6 commit 0f9b20d

19 files changed

+353
-122
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace Nameless.Orleans.Client.Contracts;
2+
3+
public record RecurringPayment {
4+
public decimal Amount { get; init; }
5+
public int PeriodInMinutes { get; set; }
6+
}

Nameless.Orleans.Client/Nameless.Orleans.Client.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
<ItemGroup>
1010
<PackageReference Include="Microsoft.Orleans.Client" Version="9.1.2" />
1111
<PackageReference Include="Microsoft.Orleans.Clustering.AzureStorage" Version="9.1.2" />
12+
<PackageReference Include="Microsoft.Orleans.Transactions" Version="9.1.2" />
1213
</ItemGroup>
1314

1415
<ItemGroup>

Nameless.Orleans.Client/Program.cs

+94-25
Original file line numberDiff line numberDiff line change
@@ -18,57 +18,126 @@ public static void Main(string[] args) {
1818
options.ClusterId = "Nameless_Orleans_Cluster";
1919
options.ServiceId = "Nameless_Orleans_Service";
2020
});
21+
22+
client.UseTransactions();
2123
});
2224

2325
var app = builder.Build();
2426

25-
app.MapGet("/account/{accountId}/balance", async (Guid accountId, IClusterClient clusterClient) => {
26-
var checkingAccountGrain = clusterClient.GetGrain<ICheckingAccountGrain>(accountId);
27-
var balance = await checkingAccountGrain.GetBalanceAsync();
27+
// Creates a new ATM
28+
app.MapPost("/atm", async (CreateAtm createAtm, IClusterClient clusterClient) => {
29+
var atmId = Guid.NewGuid();
30+
var atmGrain = clusterClient.GetGrain<IAtmGrain>(atmId);
31+
32+
await atmGrain.InitializeAsync(createAtm.OpeningBalance);
2833

29-
return TypedResults.Ok(balance);
34+
return TypedResults.Created($"/atm/{atmId}", new { AtmId = atmId });
3035
});
3136

32-
app.MapPost("/account", async (CreateAccount createAccount, IClusterClient clusterClient) => {
37+
// Retrieves the ATM current balance
38+
app.MapGet("/atm/{atmId}/balance", async (Guid atmId, ITransactionClient transactionClient, IClusterClient clusterClient) => {
39+
var currentBalance = 0m;
40+
await transactionClient.RunTransaction(TransactionOption.Create, async () => {
41+
var atmGrain = clusterClient.GetGrain<IAtmGrain>(atmId);
42+
currentBalance = await atmGrain.GetCurrentBalanceAsync();
43+
});
44+
45+
return TypedResults.Ok(new {
46+
AtmId = atmId,
47+
CurrentBalance = currentBalance
48+
});
49+
});
50+
51+
// Withdraw from ATM
52+
app.MapPost("/atm/{atmId}/withdraw", async (Guid atmId, AtmWithdraw atmWithdraw, IClusterClient clusterClient) => {
53+
var atmGrain = clusterClient.GetGrain<IAtmGrain>(atmId);
54+
var accountGrain = clusterClient.GetGrain<IAccountGrain>(atmWithdraw.AccountId);
55+
56+
await atmGrain.WithdrawAsync(atmWithdraw.AccountId, atmWithdraw.Amount);
57+
var currentAtmBalance = await atmGrain.GetCurrentBalanceAsync();
58+
var currentAccountBalance = await accountGrain.GetCurrentBalanceAsync();
59+
60+
return TypedResults.Ok(new {
61+
AtmId = atmId,
62+
AtmCurrentBalance = currentAtmBalance,
63+
WithdrawAmout = atmWithdraw.Amount,
64+
atmWithdraw.AccountId,
65+
AccountCurrentBalance = currentAccountBalance
66+
});
67+
});
68+
69+
// Creates a new Account
70+
app.MapPost("/account", async (CreateAccount createAccount, ITransactionClient transactionClient, IClusterClient clusterClient) => {
3371
var accountId = Guid.NewGuid();
34-
var checkingAccountGrain = clusterClient.GetGrain<ICheckingAccountGrain>(accountId);
3572

36-
await checkingAccountGrain.InitializeAsync(createAccount.OpeningBalance);
73+
await transactionClient.RunTransaction(TransactionOption.Create, async () => {
74+
var accountGrain = clusterClient.GetGrain<IAccountGrain>(accountId);
75+
76+
await accountGrain.InitializeAsync(createAccount.OpeningBalance);
77+
});
3778

38-
return TypedResults.Created($"/account/{accountId}", new { AccountId = accountId });
79+
return TypedResults.Created($"/account/{accountId}", new {
80+
AccountId = accountId,
81+
CurrentBalance = createAccount.OpeningBalance
82+
});
3983
});
4084

41-
app.MapPost("/account/{accountId}/debit", async (Guid accountId, Debit input, IClusterClient clusterClient) => {
42-
var checkingAccountGrain = clusterClient.GetGrain<ICheckingAccountGrain>(accountId);
85+
// Retrieves Account current balance
86+
app.MapGet("/account/{accountId}/balance", async (Guid accountId, ITransactionClient transactionClient, IClusterClient clusterClient) => {
87+
var currentBalance = 0m;
4388

44-
await checkingAccountGrain.DebitAsync(input.Amount);
89+
await transactionClient.RunTransaction(TransactionOption.Create, async () => {
90+
var accountGrain = clusterClient.GetGrain<IAccountGrain>(accountId);
91+
currentBalance = await accountGrain.GetCurrentBalanceAsync();
92+
});
4593

46-
return TypedResults.Ok(input);
94+
return TypedResults.Ok(new {
95+
AccountId = accountId,
96+
CurrentBalance = currentBalance
97+
});
4798
});
4899

49-
app.MapPost("/account/{accountId}/credit", async (Guid accountId, Credit input, IClusterClient clusterClient) => {
50-
var checkingAccountGrain = clusterClient.GetGrain<ICheckingAccountGrain>(accountId);
100+
// Creates a debit into an Account
101+
app.MapPost("/account/{accountId}/debit", async (Guid accountId, Debit input, IClusterClient clusterClient) => {
102+
var accountGrain = clusterClient.GetGrain<IAccountGrain>(accountId);
103+
104+
await accountGrain.DebitAsync(input.Amount);
51105

52-
await checkingAccountGrain.CreditAsync(input.Amount);
106+
var currentBalance = await accountGrain.GetCurrentBalanceAsync();
53107

54-
return TypedResults.Ok(input);
108+
return TypedResults.Ok(new {
109+
AccountId = accountId,
110+
Debit = input.Amount,
111+
CurrentBalance = currentBalance
112+
});
55113
});
56114

57-
app.MapPost("/atm", async (CreateAtm createAtm, IClusterClient clusterClient) => {
58-
var atmId = Guid.NewGuid();
59-
var atmGrain = clusterClient.GetGrain<IAtmGrain>(atmId);
115+
// Creates a credit into an Account
116+
app.MapPost("/account/{accountId}/credit", async (Guid accountId, Credit input, IClusterClient clusterClient) => {
117+
var accountGrain = clusterClient.GetGrain<IAccountGrain>(accountId);
60118

61-
await atmGrain.InitializeAsync(createAtm.OpeningBalance);
119+
await accountGrain.CreditAsync(input.Amount);
62120

63-
return TypedResults.Created($"/atm/{atmId}", new { AtmId = atmId });
121+
var currentBalance = await accountGrain.GetCurrentBalanceAsync();
122+
123+
return TypedResults.Ok(new {
124+
AccountId = accountId,
125+
Credit = input.Amount,
126+
CurrentBalance = currentBalance
127+
});
64128
});
65129

66-
app.MapPost("/atm/{atmId}/withdraw", async (Guid atmId, AtmWithdraw atmWithdraw, IClusterClient clusterClient) => {
67-
var atmGrain = clusterClient.GetGrain<IAtmGrain>(atmId);
130+
// Creates a recurring payment on an Account
131+
app.MapPost("/account/{accountId}/recurring_payment", async (Guid accountId, RecurringPayment input, IClusterClient clusterClient) => {
132+
var accountGrain = clusterClient.GetGrain<IAccountGrain>(accountId);
68133

69-
await atmGrain.WithdrawAsync(atmWithdraw.AccountId, atmWithdraw.Amount);
134+
await accountGrain.AddRecurringPaymentAsync(input.Amount, input.PeriodInMinutes);
70135

71-
return TypedResults.Ok(atmWithdraw);
136+
return TypedResults.Ok(new {
137+
AccountId = accountId,
138+
RecurringAmout = input.Amount,
139+
RecurringPeriodInMinutes = input.PeriodInMinutes
140+
});
72141
});
73142

74143
app.Run();

Nameless.Orleans.Client/Properties/launchSettings.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"commandName": "Project",
66
"dotnetRunMessages": true,
77
"launchBrowser": true,
8-
"applicationUrl": "http://localhost:5298",
8+
"applicationUrl": "http://localhost:5080",
99
"environmentVariables": {
1010
"ASPNETCORE_ENVIRONMENT": "Development"
1111
}
@@ -14,7 +14,7 @@
1414
"commandName": "Project",
1515
"dotnetRunMessages": true,
1616
"launchBrowser": true,
17-
"applicationUrl": "https://localhost:7257;http://localhost:5298",
17+
"applicationUrl": "https://localhost:5443;http://localhost:5080",
1818
"environmentVariables": {
1919
"ASPNETCORE_ENVIRONMENT": "Development"
2020
}

Nameless.Orleans.Client/atm.http

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Create a new ATM
2+
# @name CreateAtm
3+
4+
POST {{HostAddress}}/atm
5+
Content-Type: application/json
6+
7+
{
8+
"OpeningBalance": 15000.00
9+
}
10+
11+
###
12+
13+
# Get ATM current balance
14+
15+
GET {{HostAddress}}/atm/{{CreateAtm.response.body.$.AtmId}}/balance
16+
17+
###
18+
19+
# Create Account
20+
# @name CreateAccount
21+
22+
POST {{HostAddress}}/account
23+
Content-Type: application/json
24+
25+
{
26+
"OpeningBalance": 5000.00
27+
}
28+
29+
###
30+
31+
# Get Account current balance
32+
33+
GET {{HostAddress}}/account/{{CreateAccount.response.body.$.AccountId}}/balance
34+
35+
###
36+
37+
# Withdraw from ATM
38+
39+
POST {{HostAddress}}/atm/{{CreateAtm.response.body.$.AtmId}}/withdraw
40+
Content-Type: application/json
41+
42+
{
43+
"AccountId": "{{CreateAccount.response.body.$.AccountId}}",
44+
"Amount": 50.00
45+
}
46+
47+
###
48+
49+
# Add recurring payment for Account
50+
51+
POST {{HostAddress}}/account/{{CreateAccount.response.body.$.AccountId}}/recurring_payment
52+
Content-Type: application/json
53+
54+
{
55+
"Amount": 5.00,
56+
"PeriodInMinutes": 1
57+
}
58+
59+
###
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"development": {
3+
"HostAddress": "https://localhost:5443"
4+
},
5+
"remote": {
6+
"HostAddress": "https://my-orleans-sample.com"
7+
}
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
namespace Nameless.Orleans.Grains.Abstractions;
2+
3+
public interface IAccountGrain : IGrainWithGuidKey {
4+
[Transaction(TransactionOption.Create)]
5+
Task InitializeAsync(decimal openingBalance);
6+
7+
[Transaction(TransactionOption.Create)]
8+
Task<decimal> GetCurrentBalanceAsync();
9+
10+
// Withdraw method in AtmGrain could have
11+
// created a transaction, so that's why
12+
// we are specifying CreateOrJoin
13+
[Transaction(TransactionOption.CreateOrJoin)]
14+
Task DebitAsync(decimal value);
15+
16+
[Transaction(TransactionOption.CreateOrJoin)]
17+
Task CreditAsync(decimal value);
18+
19+
Task AddRecurringPaymentAsync(decimal amount, int periodInMinutes);
20+
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
namespace Nameless.Orleans.Grains.Abstractions;
22

33
public interface IAtmGrain : IGrainWithGuidKey {
4+
[Transaction(TransactionOption.Create)]
45
Task InitializeAsync(decimal openingBalance);
6+
7+
[Transaction(TransactionOption.CreateOrJoin)]
58
Task WithdrawAsync(Guid accountId, decimal amount);
9+
10+
[Transaction(TransactionOption.Create)]
11+
Task<decimal> GetCurrentBalanceAsync();
612
}

Nameless.Orleans.Grains/Abstractions/ICheckingAccountGrain.cs

-8
This file was deleted.
+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using Nameless.Orleans.Grains.Abstractions;
2+
using Nameless.Orleans.Grains.States;
3+
using Orleans.Concurrency;
4+
using Orleans.Transactions.Abstractions;
5+
6+
namespace Nameless.Orleans.Grains;
7+
8+
[Reentrant]
9+
public class AccountGrain : Grain, IAccountGrain, IRemindable {
10+
private readonly ITransactionClient _transactionClient;
11+
private readonly ITransactionalState<BalanceState> _balanceState;
12+
private readonly IPersistentState<AccountState> _accountState;
13+
14+
public AccountGrain(
15+
ITransactionClient transactionClient,
16+
17+
[TransactionalState(nameof(BalanceState))]
18+
ITransactionalState<BalanceState> balanceState,
19+
20+
[PersistentState(nameof(AccountState), "blobStorage")]
21+
IPersistentState<AccountState> accountState) {
22+
_transactionClient = transactionClient;
23+
_balanceState = balanceState;
24+
_accountState = accountState;
25+
}
26+
27+
public async Task InitializeAsync(decimal openingBalance) {
28+
_accountState.State.AccountId = this.GetGrainId().GetGuidKey();
29+
_accountState.State.OpenedAtUtc = DateTime.UtcNow;
30+
_accountState.State.Type = "Default";
31+
32+
await _balanceState.PerformUpdate(state => {
33+
state.Amount = openingBalance;
34+
});
35+
36+
await _accountState.WriteStateAsync();
37+
}
38+
39+
// Although the BalanceState does not have an Id
40+
// field, Orleans makes sure that this object belongs
41+
// to the current identity (AccountGrain)
42+
public Task<decimal> GetCurrentBalanceAsync()
43+
=> _balanceState.PerformRead(state => state.Amount);
44+
45+
public Task DebitAsync(decimal value)
46+
=> _balanceState.PerformUpdate(state => {
47+
state.Amount -= value;
48+
});
49+
50+
public Task CreditAsync(decimal value)
51+
=> _balanceState.PerformUpdate(state => {
52+
state.Amount += value;
53+
});
54+
55+
public async Task AddRecurringPaymentAsync(decimal amount, int periodInMinutes) {
56+
var recurringPayment = new RecurringPayment {
57+
Id = Guid.NewGuid(),
58+
Amount = amount,
59+
Period = TimeSpan.FromMinutes(periodInMinutes)
60+
};
61+
62+
_accountState.State.RecurringPayments.Add(recurringPayment);
63+
64+
await _accountState.WriteStateAsync();
65+
66+
await this.RegisterOrUpdateReminder(reminderName: $"{nameof(RecurringPayment)}::{recurringPayment.Id}",
67+
dueTime: recurringPayment.Period,
68+
period: recurringPayment.Period);
69+
}
70+
71+
public Task ReceiveReminder(string reminderName, TickStatus status) {
72+
// implement any strategy that makes sense to identify the reminder
73+
// origin.
74+
if (!reminderName.StartsWith(nameof(RecurringPayment))) {
75+
return Task.CompletedTask;
76+
}
77+
78+
var recurringPaymentId = Guid.Parse(reminderName.Split("::").Last());
79+
var recurringPayment = _accountState.State
80+
.RecurringPayments
81+
.Single(item => item.Id == recurringPaymentId);
82+
83+
return _transactionClient.RunTransaction(transactionOption: TransactionOption.Create,
84+
transactionDelegate: () => DebitAsync(recurringPayment.Amount));
85+
}
86+
}

0 commit comments

Comments
 (0)