From 9685292419c266c60d3350d567d6ae034a45b1d9 Mon Sep 17 00:00:00 2001
From: Dima <72738687+Dima-X@users.noreply.github.com>
Date: Mon, 30 Sep 2024 13:34:44 +0900
Subject: [PATCH 1/2] [ScalarDB .NET Client] Add microservice transactions
sample with Shared-cluster pattern with LINQ
---
.gitignore | 406 +++++++++++++++++-
.../.dockerignore | 3 +
.../Client/Client.csproj | 23 +
.../Binders/CustomerServiceClientBinder.cs | 22 +
.../Binders/OrderServiceClientBinder.cs | 22 +
.../Client/Commands/GetCustomerInfoCommand.cs | 37 ++
.../Client/Commands/GetOrderCommand.cs | 37 ++
.../Client/Commands/GetOrdersCommand.cs | 37 ++
.../Client/Commands/PlaceOrderCommand.cs | 67 +++
.../Client/Commands/RepaymentCommand.cs | 42 ++
.../Client/Logging.cs | 13 +
.../Client/Program.cs | 21 +
.../Common/Common.csproj | 14 +
.../Common/CustomerService/Customer.cs | 21 +
.../Common/FailedPreconditionException.cs | 10 +
.../Common/InternalException.cs | 10 +
.../Common/NotFoundException.cs | 10 +
.../Common/OrderService/Item.cs | 20 +
.../Common/OrderService/Order.cs | 20 +
.../Common/OrderService/Statement.cs | 19 +
.../CustomerService/CustomerDbContext.cs | 11 +
.../CustomerService/CustomerService.cs | 239 +++++++++++
.../CustomerService/CustomerService.csproj | 23 +
.../CustomerService/Program.cs | 33 ++
.../Properties/launchSettings.json | 20 +
.../CustomerService/appsettings.json | 21 +
.../DataLoader/DataLoader.cs | 94 ++++
.../DataLoader/DataLoader.csproj | 27 ++
.../DataLoader/Program.cs | 50 +++
.../DataLoader/SchemaCreator.cs | 52 +++
.../DataLoader/UsersCreator.cs | 67 +++
.../DataLoader/scalardb-options.json | 8 +
.../Dockerfile-CustomerService | 20 +
.../Dockerfile-OrderService | 20 +
.../MicroserviceTransactionsSample-LINQ.sln | 65 +++
.../OrderService/OrderDbContext.cs | 13 +
.../OrderService/OrderService.cs | 291 +++++++++++++
.../OrderService/OrderService.csproj | 23 +
.../OrderService/Program.cs | 40 ++
.../Properties/launchSettings.json | 20 +
.../OrderService/appsettings.json | 22 +
.../Rpc/Protos/sample.proto | 178 ++++++++
.../Rpc/Rpc.csproj | 24 ++
.../docker-compose.yml | 69 +++
.../scalardb-cluster-node.properties | 30 ++
45 files changed, 2311 insertions(+), 3 deletions(-)
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/.dockerignore
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Client.csproj
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/Binders/CustomerServiceClientBinder.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/Binders/OrderServiceClientBinder.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetCustomerInfoCommand.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrderCommand.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrdersCommand.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/PlaceOrderCommand.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/RepaymentCommand.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Logging.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Program.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/Common.csproj
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/CustomerService/Customer.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/FailedPreconditionException.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/InternalException.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/NotFoundException.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Item.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Order.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Statement.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerDbContext.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.csproj
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/Program.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/Properties/launchSettings.json
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/appsettings.json
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/DataLoader.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/DataLoader.csproj
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/Program.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/SchemaCreator.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/UsersCreator.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/scalardb-options.json
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Dockerfile-CustomerService
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Dockerfile-OrderService
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/MicroserviceTransactionsSample-LINQ.sln
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderDbContext.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderService.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderService.csproj
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/Program.cs
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/Properties/launchSettings.json
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/appsettings.json
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Rpc/Protos/sample.proto
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Rpc/Rpc.csproj
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/docker-compose.yml
create mode 100644 dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/scalardb-cluster-node.properties
diff --git a/.gitignore b/.gitignore
index d15f8e67..0c35d68b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,5 @@
-# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,intellij
-# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,intellij
+# Created by https://www.toptal.com/developers/gitignore/api/java,gradle,intellij,visualstudio
+# Edit at https://www.toptal.com/developers/gitignore?templates=java,gradle,intellij,visualstudio
### Intellij ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
@@ -132,4 +132,404 @@ gradle-app.setting
### Gradle Patch ###
**/build/
-# End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij
+### VisualStudio ###
+## Ignore Visual Studio temporary files, build results, and
+## files generated by popular Visual Studio add-ons.
+##
+## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
+
+# User-specific files
+*.rsuser
+*.suo
+*.user
+*.userosscache
+*.sln.docstates
+
+# User-specific files (MonoDevelop/Xamarin Studio)
+*.userprefs
+
+# Mono auto generated files
+mono_crash.*
+
+# Build results
+[Dd]ebug/
+[Dd]ebugPublic/
+[Rr]elease/
+[Rr]eleases/
+x64/
+x86/
+[Ww][Ii][Nn]32/
+[Aa][Rr][Mm]/
+[Aa][Rr][Mm]64/
+bld/
+[Bb]in/
+[Oo]bj/
+[Ll]og/
+[Ll]ogs/
+
+# Visual Studio 2015/2017 cache/options directory
+.vs/
+# Uncomment if you have tasks that create the project's static files in wwwroot
+#wwwroot/
+
+# Visual Studio 2017 auto generated files
+Generated\ Files/
+
+# MSTest test Results
+[Tt]est[Rr]esult*/
+[Bb]uild[Ll]og.*
+
+# NUnit
+*.VisualState.xml
+TestResult.xml
+nunit-*.xml
+
+# Build Results of an ATL Project
+[Dd]ebugPS/
+[Rr]eleasePS/
+dlldata.c
+
+# Benchmark Results
+BenchmarkDotNet.Artifacts/
+
+# .NET Core
+project.lock.json
+project.fragment.lock.json
+artifacts/
+
+# ASP.NET Scaffolding
+ScaffoldingReadMe.txt
+
+# StyleCop
+StyleCopReport.xml
+
+# Files built by Visual Studio
+*_i.c
+*_p.c
+*_h.h
+*.ilk
+*.meta
+*.obj
+*.iobj
+*.pch
+*.pdb
+*.ipdb
+*.pgc
+*.pgd
+*.rsp
+*.sbr
+*.tlb
+*.tli
+*.tlh
+*.tmp
+*.tmp_proj
+*_wpftmp.csproj
+*.tlog
+*.vspscc
+*.vssscc
+.builds
+*.pidb
+*.svclog
+*.scc
+
+# Chutzpah Test files
+_Chutzpah*
+
+# Visual C++ cache files
+ipch/
+*.aps
+*.ncb
+*.opendb
+*.opensdf
+*.sdf
+*.cachefile
+*.VC.db
+*.VC.VC.opendb
+
+# Visual Studio profiler
+*.psess
+*.vsp
+*.vspx
+*.sap
+
+# Visual Studio Trace Files
+*.e2e
+
+# TFS 2012 Local Workspace
+$tf/
+
+# Guidance Automation Toolkit
+*.gpState
+
+# ReSharper is a .NET coding add-in
+_ReSharper*/
+*.[Rr]e[Ss]harper
+*.DotSettings.user
+
+# TeamCity is a build add-in
+_TeamCity*
+
+# DotCover is a Code Coverage Tool
+*.dotCover
+
+# AxoCover is a Code Coverage Tool
+.axoCover/*
+!.axoCover/settings.json
+
+# Coverlet is a free, cross platform Code Coverage Tool
+coverage*.json
+coverage*.xml
+coverage*.info
+
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
+# NCrunch
+_NCrunch_*
+.*crunch*.local.xml
+nCrunchTemp_*
+
+# MightyMoose
+*.mm.*
+AutoTest.Net/
+
+# Web workbench (sass)
+.sass-cache/
+
+# Installshield output folder
+[Ee]xpress/
+
+# DocProject is a documentation generator add-in
+DocProject/buildhelp/
+DocProject/Help/*.HxT
+DocProject/Help/*.HxC
+DocProject/Help/*.hhc
+DocProject/Help/*.hhk
+DocProject/Help/*.hhp
+DocProject/Help/Html2
+DocProject/Help/html
+
+# Click-Once directory
+publish/
+
+# Publish Web Output
+*.[Pp]ublish.xml
+*.azurePubxml
+# Note: Comment the next line if you want to checkin your web deploy settings,
+# but database connection strings (with potential passwords) will be unencrypted
+*.pubxml
+*.publishproj
+
+# Microsoft Azure Web App publish settings. Comment the next line if you want to
+# checkin your Azure Web App publish settings, but sensitive information contained
+# in these scripts will be unencrypted
+PublishScripts/
+
+# NuGet Packages
+*.nupkg
+# NuGet Symbol Packages
+*.snupkg
+# The packages folder can be ignored because of Package Restore
+**/[Pp]ackages/*
+# except build/, which is used as an MSBuild target.
+!**/[Pp]ackages/build/
+# Uncomment if necessary however generally it will be regenerated when needed
+#!**/[Pp]ackages/repositories.config
+# NuGet v3's project.json files produces more ignorable files
+*.nuget.props
+*.nuget.targets
+
+# Microsoft Azure Build Output
+csx/
+*.build.csdef
+
+# Microsoft Azure Emulator
+ecf/
+rcf/
+
+# Windows Store app package directories and files
+AppPackages/
+BundleArtifacts/
+Package.StoreAssociation.xml
+_pkginfo.txt
+*.appx
+*.appxbundle
+*.appxupload
+
+# Visual Studio cache files
+# files ending in .cache can be ignored
+*.[Cc]ache
+# but keep track of directories ending in .cache
+!?*.[Cc]ache/
+
+# Others
+ClientBin/
+~$*
+*~
+*.dbmdl
+*.dbproj.schemaview
+*.jfm
+*.pfx
+*.publishsettings
+orleans.codegen.cs
+
+# Including strong name files can present a security risk
+# (https://github.com/github/gitignore/pull/2483#issue-259490424)
+#*.snk
+
+# Since there are multiple workflows, uncomment next line to ignore bower_components
+# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
+#bower_components/
+
+# RIA/Silverlight projects
+Generated_Code/
+
+# Backup & report files from converting an old project file
+# to a newer Visual Studio version. Backup files are not needed,
+# because we have git ;-)
+_UpgradeReport_Files/
+Backup*/
+UpgradeLog*.XML
+UpgradeLog*.htm
+ServiceFabricBackup/
+*.rptproj.bak
+
+# SQL Server files
+*.mdf
+*.ldf
+*.ndf
+
+# Business Intelligence projects
+*.rdl.data
+*.bim.layout
+*.bim_*.settings
+*.rptproj.rsuser
+*- [Bb]ackup.rdl
+*- [Bb]ackup ([0-9]).rdl
+*- [Bb]ackup ([0-9][0-9]).rdl
+
+# Microsoft Fakes
+FakesAssemblies/
+
+# GhostDoc plugin setting file
+*.GhostDoc.xml
+
+# Node.js Tools for Visual Studio
+.ntvs_analysis.dat
+node_modules/
+
+# Visual Studio 6 build log
+*.plg
+
+# Visual Studio 6 workspace options file
+*.opt
+
+# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
+*.vbw
+
+# Visual Studio 6 auto-generated project file (contains which files were open etc.)
+*.vbp
+
+# Visual Studio 6 workspace and project file (working project files containing files to include in project)
+*.dsw
+*.dsp
+
+# Visual Studio 6 technical files
+
+# Visual Studio LightSwitch build output
+**/*.HTMLClient/GeneratedArtifacts
+**/*.DesktopClient/GeneratedArtifacts
+**/*.DesktopClient/ModelManifest.xml
+**/*.Server/GeneratedArtifacts
+**/*.Server/ModelManifest.xml
+_Pvt_Extensions
+
+# Paket dependency manager
+.paket/paket.exe
+paket-files/
+
+# FAKE - F# Make
+.fake/
+
+# CodeRush personal settings
+.cr/personal
+
+# Python Tools for Visual Studio (PTVS)
+__pycache__/
+*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/**
+# !tools/packages.config
+
+# Tabs Studio
+*.tss
+
+# Telerik's JustMock configuration file
+*.jmconfig
+
+# BizTalk build output
+*.btp.cs
+*.btm.cs
+*.odx.cs
+*.xsd.cs
+
+# OpenCover UI analysis results
+OpenCover/
+
+# Azure Stream Analytics local run output
+ASALocalRun/
+
+# MSBuild Binary and Structured Log
+*.binlog
+
+# NVidia Nsight GPU debugger configuration file
+*.nvuser
+
+# MFractors (Xamarin productivity tool) working folder
+.mfractor/
+
+# Local History for Visual Studio
+.localhistory/
+
+# Visual Studio History (VSHistory) files
+.vshistory/
+
+# BeatPulse healthcheck temp database
+healthchecksdb
+
+# Backup folder for Package Reference Convert tool in Visual Studio 2017
+MigrationBackup/
+
+# Ionide (cross platform F# VS Code tools) working folder
+.ionide/
+
+# Fody - auto-generated XML schema
+FodyWeavers.xsd
+
+# VS Code files for those working on multiple tools
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+# Local History for Visual Studio Code
+.history/
+
+# Windows Installer files from build outputs
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# JetBrains Rider
+*.sln.iml
+
+### VisualStudio Patch ###
+# Additional files built by Visual Studio
+
+# End of https://www.toptal.com/developers/gitignore/api/java,gradle,intellij,visualstudio
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/.dockerignore b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/.dockerignore
new file mode 100644
index 00000000..1a89e0f7
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/.dockerignore
@@ -0,0 +1,3 @@
+**/bin/
+**/obj/
+**/.DS_Store
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Client.csproj b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Client.csproj
new file mode 100644
index 00000000..3eb14fac
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Client.csproj
@@ -0,0 +1,23 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ 12
+ MicroserviceTransactionsSample.Client
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/Binders/CustomerServiceClientBinder.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/Binders/CustomerServiceClientBinder.cs
new file mode 100644
index 00000000..628d283a
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/Binders/CustomerServiceClientBinder.cs
@@ -0,0 +1,22 @@
+using System.CommandLine.Binding;
+using Grpc.Net.Client;
+using static MicroserviceTransactionsSample.Rpc.CustomerService;
+
+namespace MicroserviceTransactionsSample.Client.Commands.Binders;
+
+public class CustomerServiceClientBinder : BinderBase
+{
+ private const string CustomerServiceUrl = "http://localhost:10010";
+
+ protected override CustomerServiceClient GetBoundValue(BindingContext bindingContext)
+ {
+ var customerServiceUrl = Environment.GetEnvironmentVariable("CUSTOMER_SERVICE_URL");
+ if (String.IsNullOrEmpty(customerServiceUrl))
+ customerServiceUrl = CustomerServiceUrl;
+
+ var grpcOptions = new GrpcChannelOptions { LoggerFactory = Logging.GetLoggerFactory() };
+ var grpcChannel = GrpcChannel.ForAddress(customerServiceUrl, grpcOptions);
+
+ return new CustomerServiceClient(grpcChannel);
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/Binders/OrderServiceClientBinder.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/Binders/OrderServiceClientBinder.cs
new file mode 100644
index 00000000..d433c287
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/Binders/OrderServiceClientBinder.cs
@@ -0,0 +1,22 @@
+using System.CommandLine.Binding;
+using Grpc.Net.Client;
+using static MicroserviceTransactionsSample.Rpc.OrderService;
+
+namespace MicroserviceTransactionsSample.Client.Commands.Binders;
+
+public class OrderServiceClientBinder : BinderBase
+{
+ private const string OrderServiceUrl = "http://localhost:10020";
+
+ protected override OrderServiceClient GetBoundValue(BindingContext bindingContext)
+ {
+ var orderServiceUrl = Environment.GetEnvironmentVariable("ORDER_SERVICE_URL");
+ if (String.IsNullOrEmpty(orderServiceUrl))
+ orderServiceUrl = OrderServiceUrl;
+
+ var grpcOptions = new GrpcChannelOptions { LoggerFactory = Logging.GetLoggerFactory() };
+ var grpcChannel = GrpcChannel.ForAddress(orderServiceUrl, grpcOptions);
+
+ return new OrderServiceClient(grpcChannel);
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetCustomerInfoCommand.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetCustomerInfoCommand.cs
new file mode 100644
index 00000000..21035615
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetCustomerInfoCommand.cs
@@ -0,0 +1,37 @@
+using System.CommandLine;
+using MicroserviceTransactionsSample.Client.Commands.Binders;
+using MicroserviceTransactionsSample.Rpc;
+using static MicroserviceTransactionsSample.Rpc.CustomerService;
+
+namespace MicroserviceTransactionsSample.Client.Commands;
+
+public static class GetCustomerInfoCommand
+{
+ private const string Name = "GetCustomerInfo";
+ private const string Description = "Get customer information";
+
+ private const string ArgName = "id";
+ private const string ArgDescription = "customer ID";
+
+ public static Command Create()
+ {
+ var customerIdArg = new Argument(ArgName, ArgDescription);
+ var getCustomerInfoCommand = new Command(Name, Description)
+ {
+ customerIdArg
+ };
+
+ getCustomerInfoCommand.SetHandler(getCustomerInfo,
+ customerIdArg,
+ new CustomerServiceClientBinder());
+ return getCustomerInfoCommand;
+ }
+
+ private static async Task getCustomerInfo(int customerId, CustomerServiceClient client)
+ {
+ var request = new GetCustomerInfoRequest { CustomerId = customerId };
+ var response = await client.GetCustomerInfoAsync(request);
+
+ Console.WriteLine(response);
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrderCommand.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrderCommand.cs
new file mode 100644
index 00000000..1a1974d4
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrderCommand.cs
@@ -0,0 +1,37 @@
+using System.CommandLine;
+using MicroserviceTransactionsSample.Client.Commands.Binders;
+using MicroserviceTransactionsSample.Rpc;
+using static MicroserviceTransactionsSample.Rpc.OrderService;
+
+namespace MicroserviceTransactionsSample.Client.Commands;
+
+public static class GetOrderCommand
+{
+ private const string Name = "GetOrder";
+ private const string Description = "Get order information by order ID";
+
+ private const string ArgName = "id";
+ private const string ArgDescription = "order ID";
+
+ public static Command Create()
+ {
+ var orderIdArg = new Argument(ArgName, ArgDescription);
+ var getOrderCommand = new Command(Name, Description)
+ {
+ orderIdArg
+ };
+
+ getOrderCommand.SetHandler(getOrder,
+ orderIdArg,
+ new OrderServiceClientBinder());
+ return getOrderCommand;
+ }
+
+ private static async Task getOrder(string orderId, OrderServiceClient client)
+ {
+ var request = new GetOrderRequest { OrderId = orderId };
+ var response = await client.GetOrderAsync(request);
+
+ Console.WriteLine(response);
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrdersCommand.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrdersCommand.cs
new file mode 100644
index 00000000..63254583
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrdersCommand.cs
@@ -0,0 +1,37 @@
+using System.CommandLine;
+using MicroserviceTransactionsSample.Client.Commands.Binders;
+using MicroserviceTransactionsSample.Rpc;
+using static MicroserviceTransactionsSample.Rpc.OrderService;
+
+namespace MicroserviceTransactionsSample.Client.Commands;
+
+public static class GetOrdersCommand
+{
+ private const string Name = "GetOrders";
+ private const string Description = "Get orders information by customer ID";
+
+ private const string ArgName = "customer_id";
+ private const string ArgDescription = "customer ID";
+
+ public static Command Create()
+ {
+ var customerIdArg = new Argument(ArgName, ArgDescription);
+ var getOrdersCommand = new Command(Name, Description)
+ {
+ customerIdArg
+ };
+
+ getOrdersCommand.SetHandler(getOrders,
+ customerIdArg,
+ new OrderServiceClientBinder());
+ return getOrdersCommand;
+ }
+
+ private static async Task getOrders(int customerId, OrderServiceClient client)
+ {
+ var request = new GetOrdersRequest { CustomerId = customerId };
+ var response = await client.GetOrdersAsync(request);
+
+ Console.WriteLine(response);
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/PlaceOrderCommand.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/PlaceOrderCommand.cs
new file mode 100644
index 00000000..e78bf890
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/PlaceOrderCommand.cs
@@ -0,0 +1,67 @@
+using System.CommandLine;
+using MicroserviceTransactionsSample.Client.Commands.Binders;
+using MicroserviceTransactionsSample.Rpc;
+using Microsoft.Extensions.DependencyInjection;
+using static MicroserviceTransactionsSample.Rpc.OrderService;
+
+namespace MicroserviceTransactionsSample.Client.Commands;
+
+public static class PlaceOrderCommand
+{
+ private const string Name = "PlaceOrder";
+ private const string Description = "Place an order";
+
+ private const string ArgName1 = "customer_id";
+ private const string ArgDescription1 = "customer ID";
+ private const string ArgName2 = "orders";
+ private const string ArgDescription2 = "orders. The format is \"- :,
- :,...\"";
+
+ public static Command Create()
+ {
+ var customerIdArg = new Argument(ArgName1, ArgDescription1);
+ var ordersArg = new Argument>(
+ name: ArgName2,
+ parse: arg =>
+ {
+ var argStr = arg.Tokens.First().Value;
+ var orders = argStr
+ .Split(',')
+ .Select(s => s.Split(':'))
+ .ToDictionary(
+ s => Int32.Parse(s[0]),
+ s => Int32.Parse(s[1])
+ );
+
+ return orders;
+ },
+ description: ArgDescription2)
+ {
+ Arity = ArgumentArity.ExactlyOne
+ };
+
+ var placeOrderCommand = new Command(Name, Description)
+ {
+ customerIdArg,
+ ordersArg
+ };
+
+ placeOrderCommand.SetHandler(placeOrder,
+ customerIdArg,
+ ordersArg,
+ new OrderServiceClientBinder());
+ return placeOrderCommand;
+ }
+
+ private static async Task placeOrder(int customerId,
+ Dictionary orders,
+ OrderServiceClient client)
+ {
+ var request = new PlaceOrderRequest { CustomerId = customerId };
+ foreach (var order in orders)
+ request.ItemOrder.Add(new ItemOrder { ItemId = order.Key, Count = order.Value });
+
+ var response = await client.PlaceOrderAsync(request);
+
+ Console.WriteLine(response);
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/RepaymentCommand.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/RepaymentCommand.cs
new file mode 100644
index 00000000..e1dea888
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/RepaymentCommand.cs
@@ -0,0 +1,42 @@
+using System.CommandLine;
+using MicroserviceTransactionsSample.Client.Commands.Binders;
+using MicroserviceTransactionsSample.Rpc;
+using static MicroserviceTransactionsSample.Rpc.CustomerService;
+
+namespace MicroserviceTransactionsSample.Client.Commands;
+
+public static class RepaymentCommand
+{
+ private const string Name = "Repayment";
+ private const string Description = "Make a repayment";
+
+ private const string ArgName1 = "customer_id";
+ private const string ArgDescription1 = "customer ID";
+ private const string ArgName2 = "amount";
+ private const string ArgDescription2 = "repayment amount";
+
+ public static Command Create()
+ {
+ var customerIdArg = new Argument(ArgName1, ArgDescription1);
+ var amountArg = new Argument(ArgName2, ArgDescription2);
+ var repaymentCommand = new Command(Name, Description)
+ {
+ customerIdArg,
+ amountArg
+ };
+
+ repaymentCommand.SetHandler(repay,
+ customerIdArg,
+ amountArg,
+ new CustomerServiceClientBinder());
+ return repaymentCommand;
+ }
+
+ private static async Task repay(int customerId, int amount, CustomerServiceClient client)
+ {
+ var request = new RepaymentRequest { CustomerId = customerId, Amount = amount };
+ var response = await client.RepaymentAsync(request);
+
+ Console.WriteLine(response);
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Logging.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Logging.cs
new file mode 100644
index 00000000..38482a1d
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Logging.cs
@@ -0,0 +1,13 @@
+using Microsoft.Extensions.Logging;
+
+namespace MicroserviceTransactionsSample.Client;
+
+public static class Logging
+{
+ public static ILoggerFactory GetLoggerFactory()
+ => LoggerFactory.Create(builder =>
+ {
+ builder.SetMinimumLevel(LogLevel.Warning);
+ builder.AddSimpleConsole(options => { options.TimestampFormat = "HH:mm:ss "; });
+ });
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Program.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Program.cs
new file mode 100644
index 00000000..95e0887b
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Program.cs
@@ -0,0 +1,21 @@
+using System.CommandLine;
+using MicroserviceTransactionsSample.Client.Commands;
+
+namespace MicroserviceTransactionsSample.Client;
+
+class Program
+{
+ static async Task Main(string[] args)
+ {
+ var rootCommand = new RootCommand("MicroserviceTransactionsSample.Client")
+ {
+ GetCustomerInfoCommand.Create(),
+ GetOrderCommand.Create(),
+ GetOrdersCommand.Create(),
+ PlaceOrderCommand.Create(),
+ RepaymentCommand.Create()
+ };
+
+ await rootCommand.InvokeAsync(args);
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/Common.csproj b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/Common.csproj
new file mode 100644
index 00000000..05f267e4
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/Common.csproj
@@ -0,0 +1,14 @@
+
+
+
+ net8.0
+ enable
+ enable
+ MicroserviceTransactionsSample.Common
+
+
+
+
+
+
+
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/CustomerService/Customer.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/CustomerService/Customer.cs
new file mode 100644
index 00000000..ec061070
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/CustomerService/Customer.cs
@@ -0,0 +1,21 @@
+using System.ComponentModel.DataAnnotations.Schema;
+using ScalarDB.Client.DataAnnotations;
+
+namespace MicroserviceTransactionsSample.Common.CustomerService;
+
+[Table("customer_service.customers")]
+public class Customer
+{
+ [PartitionKey]
+ [Column("customer_id", Order = 0)]
+ public int Id { get; set; }
+
+ [Column("name", Order = 1)]
+ public string Name { get; set; } = "";
+
+ [Column("credit_limit", Order = 2)]
+ public int CreditLimit { get; set; }
+
+ [Column("credit_total", Order = 3)]
+ public int CreditTotal { get; set; }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/FailedPreconditionException.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/FailedPreconditionException.cs
new file mode 100644
index 00000000..40d42e54
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/FailedPreconditionException.cs
@@ -0,0 +1,10 @@
+using Grpc.Core;
+
+namespace MicroserviceTransactionsSample.Common;
+
+public class FailedPreconditionException : RpcException
+{
+ public FailedPreconditionException(string message)
+ : base(new Status(StatusCode.FailedPrecondition, message))
+ { }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/InternalException.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/InternalException.cs
new file mode 100644
index 00000000..0e145c36
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/InternalException.cs
@@ -0,0 +1,10 @@
+using Grpc.Core;
+
+namespace MicroserviceTransactionsSample.Common;
+
+public class InternalException : RpcException
+{
+ public InternalException(string message, Exception? innerEx)
+ : base(new Status(StatusCode.Internal, message, innerEx))
+ { }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/NotFoundException.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/NotFoundException.cs
new file mode 100644
index 00000000..eacafb49
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/NotFoundException.cs
@@ -0,0 +1,10 @@
+using Grpc.Core;
+
+namespace MicroserviceTransactionsSample.Common;
+
+public class NotFoundException : RpcException
+{
+ public NotFoundException(string message)
+ : base(new Status(StatusCode.NotFound, message))
+ { }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Item.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Item.cs
new file mode 100644
index 00000000..8a3123fc
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Item.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations.Schema;
+using ScalarDB.Client.DataAnnotations;
+
+namespace MicroserviceTransactionsSample.Common.OrderService;
+
+[Table("order_service.items")]
+public class Item
+{
+ public static readonly Item EmptyItem = new() { Id = -1, Name = "", Price = 0 };
+
+ [PartitionKey]
+ [Column("item_id", Order = 0)]
+ public int Id { get; set; }
+
+ [Column("name", Order = 1)]
+ public string Name { get; set; } = "";
+
+ [Column("price", Order = 2)]
+ public int Price { get; set; }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Order.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Order.cs
new file mode 100644
index 00000000..44dcf54d
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Order.cs
@@ -0,0 +1,20 @@
+using System.ComponentModel.DataAnnotations.Schema;
+using ScalarDB.Client.DataAnnotations;
+
+namespace MicroserviceTransactionsSample.Common.OrderService;
+
+[Table("order_service.orders")]
+public class Order
+{
+ [PartitionKey]
+ [Column("customer_id", Order = 0)]
+ public int CustomerId { get; set; }
+
+ [ClusteringKey]
+ [Column("timestamp", Order = 1)]
+ public long Timestamp { get; set; }
+
+ [SecondaryIndex]
+ [Column("order_id", Order = 2)]
+ public string Id { get; set; } = "";
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Statement.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Statement.cs
new file mode 100644
index 00000000..131ec742
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Common/OrderService/Statement.cs
@@ -0,0 +1,19 @@
+using System.ComponentModel.DataAnnotations.Schema;
+using ScalarDB.Client.DataAnnotations;
+
+namespace MicroserviceTransactionsSample.Common.OrderService;
+
+[Table("order_service.statements")]
+public class Statement
+{
+ [PartitionKey]
+ [Column("order_id", Order = 0)]
+ public string OrderId { get; set; } = "";
+
+ [ClusteringKey]
+ [Column("item_id", Order = 1)]
+ public int ItemId { get; set; }
+
+ [Column("count", Order = 2)]
+ public int Count { get; set; }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerDbContext.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerDbContext.cs
new file mode 100644
index 00000000..f4c60761
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerDbContext.cs
@@ -0,0 +1,11 @@
+using MicroserviceTransactionsSample.Common.CustomerService;
+using ScalarDB.Client;
+
+namespace MicroserviceTransactionsSample.CustomerService;
+
+public class CustomerDbContext : ScalarDbContext
+{
+#nullable disable
+ public ScalarDbSet Customers { get; set; }
+#nullable restore
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.cs
new file mode 100644
index 00000000..6bd7ca6d
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.cs
@@ -0,0 +1,239 @@
+using Grpc.Core;
+using MicroserviceTransactionsSample.Rpc;
+using MicroserviceTransactionsSample.Common;
+using ScalarDB.Client.Exceptions;
+using static MicroserviceTransactionsSample.Rpc.CustomerService;
+
+namespace MicroserviceTransactionsSample.CustomerService;
+
+public class CustomerService : CustomerServiceBase
+{
+ private readonly CustomerDbContext _db;
+ private readonly ILogger _logger;
+
+ public CustomerService(CustomerDbContext db,
+ ILogger logger)
+ {
+ _db = db;
+ _logger = logger;
+ }
+
+ ///
+ /// Get customer information. This function processing operations can be used in both a normal
+ /// transaction and a global transaction.
+ ///
+ public override async Task GetCustomerInfo(GetCustomerInfoRequest request,
+ ServerCallContext context)
+ {
+ const string funcName = "Getting customer info";
+
+ var operations = () =>
+ {
+ // Retrieve the customer info for the specified customer ID
+ var customer = _db.Customers.FirstOrDefault(c => c.Id == request.CustomerId);
+ if (customer == null)
+ throw new NotFoundException($"Customer not found (id: {request.CustomerId})");
+
+ return new GetCustomerInfoResponse
+ {
+ Id = customer.Id,
+ Name = customer.Name,
+ CreditLimit = customer.CreditLimit,
+ CreditTotal = customer.CreditTotal
+ };
+ };
+
+ if (request.HasTransactionId)
+ {
+ // For a global transaction, execute the operations as a participant
+ return await execOperationsAsParticipant(funcName, request.TransactionId, operations);
+ }
+ else
+ {
+ // For a normal transaction, execute the operations
+ return await execOperations(funcName, operations);
+ }
+ }
+
+ ///
+ /// Credit card payment. It's for a global transaction that spans OrderService and CustomerService.
+ ///
+ public override async Task Payment(PaymentRequest request, ServerCallContext context)
+ => await execOperationsAsParticipant(
+ "Payment",
+ request.TransactionId,
+ async () =>
+ {
+ // Retrieve the customer info for the customer ID
+ var customer = _db.Customers.FirstOrDefault(c => c.Id == request.CustomerId);
+ if (customer == null)
+ throw new NotFoundException($"Customer not found (id: {request.CustomerId})");
+
+ // Update credit_total for the customer
+ // and check if the credit total exceeds the credit limit after payment
+ customer.CreditTotal += request.Amount;
+ if (customer.CreditTotal > customer.CreditLimit)
+ {
+ throw new FailedPreconditionException(
+ $"Credit limit exceeded ({customer.CreditTotal} > {customer.CreditLimit})");
+ }
+
+ // Save changes to the customer
+ await _db.Customers.UpdateAsync(customer);
+
+ return new PaymentResponse();
+ });
+
+ ///
+ /// Credit card repayment.
+ ///
+ public override async Task Repayment(RepaymentRequest request, ServerCallContext context)
+ => await execOperations(
+ "Repayment",
+ async () =>
+ {
+ // Retrieve the customer info for the specified customer ID
+ var customer = _db.Customers.FirstOrDefault(c => c.Id == request.CustomerId);
+ if (customer == null)
+ throw new NotFoundException($"Customer not found (id: {request.CustomerId})");
+
+ // Reduce credit_total for the customer
+ // and check if over-repayment or not
+ customer.CreditTotal -= request.Amount;
+ if (customer.CreditTotal < 0)
+ throw new FailedPreconditionException($"Over repayment ({customer.CreditTotal})");
+
+ // Save changes to the customer
+ await _db.Customers.UpdateAsync(customer);
+
+ return new RepaymentResponse();
+ });
+
+ private Task execOperations(string funcName,
+ Func operations)
+ => execOperations(funcName,
+ () => Task.FromResult(operations()));
+
+ private async Task execOperations(string funcName,
+ Func> operations)
+ {
+ var retryCount = 0;
+ Exception? lastException = null;
+
+ while (true)
+ {
+ if (retryCount++ > 0)
+ {
+ // Retry the transaction three times maximum.
+ if (retryCount >= 3)
+ {
+ // If the transaction failed three times, return an error.
+ _logger.LogError(lastException, "{funcName} failed", funcName);
+ throw new InternalException(funcName + " failed", lastException);
+ }
+
+ _logger.LogWarning(lastException, "Retrying the transaction after 100 milliseconds: {funcName}", funcName);
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100));
+ }
+
+ try
+ {
+ // Begin a transaction
+ await _db.BeginTransactionAsync();
+
+ // Execute operations
+ var response = await operations();
+
+ // Commit the transaction (even when the transaction is read-only, we need to commit)
+ await _db.CommitTransactionAsync();
+
+ // Return the response
+ return response;
+ }
+ catch (UnknownTransactionStatusException ex)
+ {
+ // If you catch `UnknownTransactionStatusException`, it indicates that the status of the
+ // transaction, whether it has succeeded or not, is unknown. In such a case, you need to
+ // check if the transaction is committed successfully or not and retry it if it failed.
+ // How to identify a transaction status is delegated to users
+
+ _logger.LogError(ex, "{funcName} failed", funcName);
+ throw new InternalException(funcName + " failed", ex);
+ }
+ catch (TransactionException ex)
+ {
+ // For other cases, you can try retrying the transaction
+
+ // Rollback the transaction
+ await tryRollbackTransaction();
+
+ // The thrown exception can be retryable. In such case, you can basically retry the
+ // transaction. However, for the other exceptions, the transaction may still fail if the
+ // cause of the exception is nontransient. For such a case, you need to limit the number
+ // of retries and give up retrying
+ lastException = ex;
+ }
+ catch (Exception ex)
+ {
+ // If exception is not inherited from `TransactionException` you cannot retry the transaction
+ _logger.LogError(ex, "{funcName} failed", funcName);
+
+ // Rollback the transaction
+ await tryRollbackTransaction();
+
+ throw;
+ }
+ }
+ }
+
+ private Task execOperationsAsParticipant(string funcName,
+ string transactionId,
+ Func operations)
+ => execOperationsAsParticipant(funcName,
+ transactionId,
+ () => Task.FromResult(operations()));
+
+ private async Task execOperationsAsParticipant(string funcName,
+ string transactionId,
+ Func> operations)
+ {
+ try
+ {
+ // Join the transaction
+ await _db.JoinTransactionAsync(transactionId);
+
+ // Execute operations and return the result
+ return await operations();
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "{funcName} failed", funcName);
+
+ if (ex is TransactionException)
+ throw new InternalException(funcName + " failed", ex);
+ else
+ throw;
+ }
+ }
+
+ private async Task tryRollbackTransaction()
+ {
+ if (String.IsNullOrEmpty(_db.CurrentTransactionId))
+ return;
+
+ try
+ {
+ await _db.RollbackTransactionAsync();
+ }
+ catch (TransactionException ex)
+ {
+ _logger.LogWarning(ex, "Rollback failed");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Rollback failed");
+ throw;
+ }
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.csproj b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.csproj
new file mode 100644
index 00000000..d4e450bd
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net8.0
+ enable
+ enable
+ MicroserviceTransactionsSample.CustomerService
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/Program.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/Program.cs
new file mode 100644
index 00000000..f1dd0f53
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/Program.cs
@@ -0,0 +1,33 @@
+using ScalarDB.Client.Extensions;
+
+namespace MicroserviceTransactionsSample.CustomerService;
+
+public class Program
+{
+ public static async Task Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ builder.Services.AddLogging(o =>
+ {
+ o.AddSimpleConsole(options =>
+ {
+ options.TimestampFormat = "HH:mm:ss ";
+ });
+ });
+
+ // Add services to the container.
+ builder.Services.AddGrpc();
+
+ // Add CustomerDbContext to the container.
+ builder.Services.AddScalarDbContext();
+
+ var app = builder.Build();
+
+ // Configure the HTTP request pipeline.
+ app.MapGrpcService();
+ app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
+
+ await app.RunAsync();
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/Properties/launchSettings.json b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/Properties/launchSettings.json
new file mode 100644
index 00000000..8e3ad2d1
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/Properties/launchSettings.json
@@ -0,0 +1,20 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "applicationUrl": "http://localhost:5248",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true
+ },
+ "https": {
+ "commandName": "Project",
+ "applicationUrl": "https://localhost:7291;http://localhost:5248",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true
+ }
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/appsettings.json b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/appsettings.json
new file mode 100644
index 00000000..e324b418
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/appsettings.json
@@ -0,0 +1,21 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "ScalarDB.Client": "Debug"
+ }
+ },
+ "ScalarDbOptions": {
+ "AuthEnabled": true,
+ "Username": "customer-service",
+ "Password": "customer-service"
+ },
+ "AllowedHosts": "*",
+ "Urls": "http://*:10010",
+ "Kestrel": {
+ "EndpointDefaults": {
+ "Protocols": "Http2"
+ }
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/DataLoader.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/DataLoader.cs
new file mode 100644
index 00000000..1b4c7ee1
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/DataLoader.cs
@@ -0,0 +1,94 @@
+using ScalarDB.Client;
+using ScalarDB.Client.Extensions;
+using MicroserviceTransactionsSample.Common.CustomerService;
+using MicroserviceTransactionsSample.Common.OrderService;
+using Microsoft.Extensions.Logging;
+using ScalarDB.Client.Exceptions;
+
+namespace MicroserviceTransactionsSample.DataLoader;
+
+public class DataLoader
+{
+ private readonly IDistributedTransactionManager _manager;
+ private readonly ILogger _logger;
+
+ public DataLoader(IDistributedTransactionManager manager,
+ ILoggerFactory loggerFactory)
+ {
+ _manager = manager;
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ public async Task Load()
+ {
+ var attempts = Program.RetryCount;
+
+ while (true)
+ {
+ IDistributedTransaction? transaction = null;
+ try
+ {
+ // fill the data
+ transaction = await _manager.BeginAsync();
+
+ // Customers
+ var customers = new[]
+ {
+ new Customer { Id = 1, Name = "Yamada Taro", CreditLimit = 10000, CreditTotal = 0 },
+ new Customer { Id = 2, Name = "Yamada Hanako", CreditLimit = 10000, CreditTotal = 0 },
+ new Customer { Id = 3, Name = "Suzuki Ichiro", CreditLimit = 10000, CreditTotal = 0 }
+ };
+
+ foreach (var customer in customers)
+ {
+ var key = new Dictionary { { nameof(Customer.Id), customer.Id } };
+ if (await transaction.GetAsync(key) == null)
+ await transaction.InsertAsync(customer);
+ }
+
+ // Items
+ var items = new[]
+ {
+ new Item { Id = 1, Name = "Apple", Price = 1000 },
+ new Item { Id = 2, Name = "Orange", Price = 2000 },
+ new Item { Id = 3, Name = "Grape", Price = 2500 },
+ new Item { Id = 4, Name = "Mango", Price = 5000 },
+ new Item { Id = 5, Name = "Melon", Price = 3000 }
+ };
+
+ foreach (var item in items)
+ {
+ var key = new Dictionary { { nameof(Item.Id), item.Id } };
+ if (await transaction.GetAsync
- (key) == null)
+ await transaction.InsertAsync(item);
+ }
+
+ await transaction.CommitAsync();
+
+ _logger.LogInformation("Initial data loaded");
+ return;
+ }
+ catch (IllegalArgumentException) when (--attempts > 0)
+ {
+ // there's can be a lag until ScalarDB Cluster recognize namespaces and tables
+ // created in Cassandra, so if this method is called after `SchemaCreator.Create()`
+ // the first attempts can fail with 'The namespace does not exist' error
+
+ _logger.LogWarning("Loading initial data failed. "
+ + "Retrying after {interval}...", Program.RetryInternal);
+
+ transaction?.RollbackAsync();
+
+ await Task.Delay(Program.RetryInternal);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Loading initial data failed");
+
+ transaction?.RollbackAsync();
+
+ throw;
+ }
+ }
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/DataLoader.csproj b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/DataLoader.csproj
new file mode 100644
index 00000000..9201b0bf
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/DataLoader.csproj
@@ -0,0 +1,27 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+ MicroserviceTransactionsSample.DataLoader
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/Program.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/Program.cs
new file mode 100644
index 00000000..b9710856
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/Program.cs
@@ -0,0 +1,50 @@
+using System.CommandLine;
+using Microsoft.Extensions.Logging;
+using ScalarDB.Client;
+
+namespace MicroserviceTransactionsSample.DataLoader;
+
+class Program
+{
+ internal const int RetryCount = 10;
+ internal static readonly TimeSpan RetryInternal = TimeSpan.FromSeconds(1);
+
+ static async Task Main(string[] args)
+ {
+ var configOption = new Option("--config", "Path to the config file") { IsRequired = true };
+ var resetDataOptions = new Option("--reset-data", "Recreate tables and other data");
+ var logLevelOption = new Option(name: "--log-level",
+ description: "Minimum LogLevel",
+ getDefaultValue: () => LogLevel.Information);
+
+ var rootCommand = new RootCommand("MicroserviceTransactionsSample.DataLoader")
+ {
+ configOption,
+ resetDataOptions,
+ logLevelOption
+ };
+
+ rootCommand.SetHandler(loadData, configOption, resetDataOptions, logLevelOption);
+ await rootCommand.InvokeAsync(args);
+ }
+
+ private static async Task loadData(string configFilePath, bool resetData, LogLevel logLevel)
+ {
+ var loggerFactory = getDefaultLoggerFactory(logLevel);
+ var factory = TransactionFactory.Create(configFilePath, loggerFactory);
+
+ using var admin = factory.GetTransactionAdmin();
+ await (new SchemaCreator(admin, loggerFactory)).Create(resetData);
+ await (new UsersCreator(admin, loggerFactory)).Create();
+
+ using var manager = factory.GetTransactionManager();
+ await (new DataLoader(manager, loggerFactory)).Load();
+ }
+
+ private static ILoggerFactory getDefaultLoggerFactory(LogLevel logLevel)
+ => LoggerFactory.Create(builder =>
+ {
+ builder.SetMinimumLevel(logLevel);
+ builder.AddSimpleConsole(options => { options.TimestampFormat = "HH:mm:ss.fff "; });
+ });
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/SchemaCreator.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/SchemaCreator.cs
new file mode 100644
index 00000000..2ef69063
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/SchemaCreator.cs
@@ -0,0 +1,52 @@
+using MicroserviceTransactionsSample.Common.CustomerService;
+using MicroserviceTransactionsSample.Common.OrderService;
+using Microsoft.Extensions.Logging;
+using ScalarDB.Client;
+using ScalarDB.Client.Extensions;
+
+namespace MicroserviceTransactionsSample.DataLoader;
+
+public class SchemaCreator
+{
+ private readonly IDistributedTransactionAdmin _admin;
+ private readonly ILogger _logger;
+
+ public SchemaCreator(IDistributedTransactionAdmin admin,
+ ILoggerFactory loggerFactory)
+ {
+ _admin = admin;
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ public async Task Create(bool resetIfNeeded)
+ {
+ try
+ {
+ // create tables etc.
+ await _admin.CreateCoordinatorTablesAsync(true);
+
+ await _admin.CreateNamespaceAsync(true);
+ await _admin.CreateNamespaceAsync(true);
+
+ if (resetIfNeeded)
+ {
+ await _admin.DropTableAsync(true);
+ await _admin.DropTableAsync(true);
+ await _admin.DropTableAsync(true);
+ await _admin.DropTableAsync
- (true);
+ }
+
+ await _admin.CreateTableAsync(true);
+ await _admin.CreateTableAsync(true);
+ await _admin.CreateTableAsync(true);
+ await _admin.CreateTableAsync
- (true);
+
+ _logger.LogInformation("Database schema initialized");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Initializing database schema failed");
+ throw;
+ }
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/UsersCreator.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/UsersCreator.cs
new file mode 100644
index 00000000..55ada722
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/UsersCreator.cs
@@ -0,0 +1,67 @@
+using Microsoft.Extensions.Logging;
+using ScalarDB.Client;
+using ScalarDB.Client.Core.Admin;
+using ScalarDB.Client.Exceptions;
+
+namespace MicroserviceTransactionsSample.DataLoader;
+
+public class UsersCreator
+{
+ private const string CustomerServiceUsername = "customer-service";
+ private const string CustomerServicePassword = "customer-service";
+ private const string CustomerServiceNamespace = "customer_service";
+ private const string OrderServiceUsername = "order-service";
+ private const string OrderServicePassword = "order-service";
+ private const string OrderServiceNamespace = "order_service";
+
+ private readonly Privilege[] _privileges = [Privilege.Read, Privilege.Create, Privilege.Write, Privilege.Delete];
+
+ private readonly IDistributedTransactionAdmin _admin;
+ private readonly ILogger _logger;
+
+ public UsersCreator(IDistributedTransactionAdmin admin,
+ ILoggerFactory loggerFactory)
+ {
+ _admin = admin;
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ public async Task Create()
+ {
+ var attempts = Program.RetryCount;
+
+ while (true)
+ {
+ try
+ {
+ if (await _admin.GetUserAsync(CustomerServiceUsername) == null)
+ await _admin.CreateUserAsync(CustomerServiceUsername, CustomerServicePassword);
+
+ if (await _admin.GetUserAsync(OrderServiceUsername) == null)
+ await _admin.CreateUserAsync(OrderServiceUsername, OrderServicePassword);
+
+ await _admin.GrantAsync(CustomerServiceUsername, CustomerServiceNamespace, null, _privileges);
+ await _admin.GrantAsync(OrderServiceUsername, OrderServiceNamespace, null, _privileges);
+
+ _logger.LogInformation("Privileges initialized");
+ return;
+ }
+ catch (IllegalArgumentException) when (--attempts > 0)
+ {
+ // there's can be a lag until ScalarDB Cluster recognize namespaces and tables
+ // created in Cassandra, so if this method is called after `SchemaCreator.Create()`
+ // the first attempts can fail with 'The namespace does not exist' error
+
+ _logger.LogWarning("Initializing privileges failed. "
+ + "Retrying after {interval}...", Program.RetryInternal);
+
+ await Task.Delay(Program.RetryInternal);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Initializing privileges failed");
+ throw;
+ }
+ }
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/scalardb-options.json b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/scalardb-options.json
new file mode 100644
index 00000000..7514d77c
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/scalardb-options.json
@@ -0,0 +1,8 @@
+{
+ "ScalarDbOptions": {
+ "Address": "http://localhost:60053",
+ "AuthEnabled": true,
+ "Username": "admin",
+ "Password": "admin"
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Dockerfile-CustomerService b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Dockerfile-CustomerService
new file mode 100644
index 00000000..876bc5bf
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Dockerfile-CustomerService
@@ -0,0 +1,20 @@
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
+
+WORKDIR /src
+COPY Rpc/Rpc.csproj Rpc/
+COPY Common/Common.csproj Common/
+COPY CustomerService/CustomerService.csproj CustomerService/
+
+WORKDIR /src/CustomerService
+RUN dotnet restore
+
+COPY Rpc ../Rpc
+COPY Common ../Common
+COPY CustomerService .
+RUN dotnet publish -c Release -o /publish --no-restore
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
+WORKDIR /app
+COPY --from=build-env /publish .
+EXPOSE 10010
+ENTRYPOINT ["dotnet", "CustomerService.dll"]
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Dockerfile-OrderService b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Dockerfile-OrderService
new file mode 100644
index 00000000..14ddf1e6
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Dockerfile-OrderService
@@ -0,0 +1,20 @@
+FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build-env
+
+WORKDIR /src
+COPY Rpc/Rpc.csproj Rpc/
+COPY Common/Common.csproj Common/
+COPY OrderService/OrderService.csproj OrderService/
+
+WORKDIR /src/OrderService
+RUN dotnet restore
+
+COPY Rpc ../Rpc
+COPY Common ../Common
+COPY OrderService .
+RUN dotnet publish -c Release -o /publish --no-restore
+
+FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
+WORKDIR /app
+COPY --from=build-env /publish .
+EXPOSE 10020
+ENTRYPOINT ["dotnet", "OrderService.dll"]
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/MicroserviceTransactionsSample-LINQ.sln b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/MicroserviceTransactionsSample-LINQ.sln
new file mode 100644
index 00000000..df95658d
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/MicroserviceTransactionsSample-LINQ.sln
@@ -0,0 +1,65 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 25.0.1706.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rpc", "Rpc\Rpc.csproj", "{4E5BBF91-3192-4B9C-9F3F-F17B9EB325CC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomerService", "CustomerService\CustomerService.csproj", "{3CBEDF8B-3F8E-4709-BE3B-53417A8454BA}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OrderService", "OrderService\OrderService.csproj", "{5DACEEAA-9E81-477E-8166-1D4FD3614717}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{D19408A7-98AB-4A98-A3FD-6240CBD33B60}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{3A94D79F-51B2-43EC-9EC0-05903114054D}"
+ ProjectSection(SolutionItems) = preProject
+ Dockerfile-OrderService = Dockerfile-OrderService
+ Dockerfile-CustomerService = Dockerfile-CustomerService
+ scalardb-cluster-node.properties = scalardb-cluster-node.properties
+ docker-compose.yml = docker-compose.yml
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{3A24DEF6-8412-4960-9CA6-E4608C1530FC}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataLoader", "DataLoader\DataLoader.csproj", "{9B114E0D-F905-447B-864B-D658B506F1A7}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {4E5BBF91-3192-4B9C-9F3F-F17B9EB325CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4E5BBF91-3192-4B9C-9F3F-F17B9EB325CC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4E5BBF91-3192-4B9C-9F3F-F17B9EB325CC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4E5BBF91-3192-4B9C-9F3F-F17B9EB325CC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3CBEDF8B-3F8E-4709-BE3B-53417A8454BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3CBEDF8B-3F8E-4709-BE3B-53417A8454BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3CBEDF8B-3F8E-4709-BE3B-53417A8454BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3CBEDF8B-3F8E-4709-BE3B-53417A8454BA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {5DACEEAA-9E81-477E-8166-1D4FD3614717}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {5DACEEAA-9E81-477E-8166-1D4FD3614717}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {5DACEEAA-9E81-477E-8166-1D4FD3614717}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {5DACEEAA-9E81-477E-8166-1D4FD3614717}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D19408A7-98AB-4A98-A3FD-6240CBD33B60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D19408A7-98AB-4A98-A3FD-6240CBD33B60}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D19408A7-98AB-4A98-A3FD-6240CBD33B60}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D19408A7-98AB-4A98-A3FD-6240CBD33B60}.Release|Any CPU.Build.0 = Release|Any CPU
+ {3A24DEF6-8412-4960-9CA6-E4608C1530FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {3A24DEF6-8412-4960-9CA6-E4608C1530FC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {3A24DEF6-8412-4960-9CA6-E4608C1530FC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {3A24DEF6-8412-4960-9CA6-E4608C1530FC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9B114E0D-F905-447B-864B-D658B506F1A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9B114E0D-F905-447B-864B-D658B506F1A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9B114E0D-F905-447B-864B-D658B506F1A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9B114E0D-F905-447B-864B-D658B506F1A7}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {6DFADFD7-F984-49A1-9E33-C86076348167}
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ EndGlobalSection
+EndGlobal
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderDbContext.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderDbContext.cs
new file mode 100644
index 00000000..7d8ea28e
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderDbContext.cs
@@ -0,0 +1,13 @@
+using MicroserviceTransactionsSample.Common.OrderService;
+using ScalarDB.Client;
+
+namespace MicroserviceTransactionsSample.OrderService;
+
+public class OrderDbContext : ScalarDbContext
+{
+#nullable disable
+ public ScalarDbSet Orders { get; set; }
+ public ScalarDbSet Statements { get; set; }
+ public ScalarDbSet
- Items { get; set; }
+#nullable restore
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderService.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderService.cs
new file mode 100644
index 00000000..d4696797
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderService.cs
@@ -0,0 +1,291 @@
+using Grpc.Core;
+using MicroserviceTransactionsSample.Rpc;
+using MicroserviceTransactionsSample.Common;
+using MicroserviceTransactionsSample.Common.OrderService;
+using ScalarDB.Client.Exceptions;
+using static MicroserviceTransactionsSample.Rpc.OrderService;
+using static MicroserviceTransactionsSample.Rpc.CustomerService;
+using Order = MicroserviceTransactionsSample.Common.OrderService.Order;
+using Statement = MicroserviceTransactionsSample.Common.OrderService.Statement;
+
+namespace MicroserviceTransactionsSample.OrderService;
+
+public class OrderService : OrderServiceBase
+{
+ private readonly OrderDbContext _db;
+ private readonly CustomerServiceClient _customerClient;
+ private readonly ILogger _logger;
+
+ public OrderService(OrderDbContext db,
+ CustomerServiceClient customerClient,
+ ILogger logger)
+ {
+ _db = db;
+ _customerClient = customerClient;
+ _logger = logger;
+ }
+
+ ///
+ /// Place an order. It's a transaction that spans OrderService and CustomerService
+ ///
+ public override async Task PlaceOrder(PlaceOrderRequest request,
+ ServerCallContext context)
+ => await execOperationsAsCoordinator(
+ "Placing an order",
+ async () =>
+ {
+ var orderId = Guid.NewGuid().ToString();
+
+ // Insert the order info into the orders table
+ var order = new Order
+ {
+ Id = orderId,
+ CustomerId = request.CustomerId,
+ Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
+ };
+ await _db.Orders.AddAsync(order);
+
+ var amount = 0;
+ foreach (var itemOrder in request.ItemOrder)
+ {
+ // Insert the order statement into the statements table
+ var statement = new Statement
+ {
+ OrderId = orderId,
+ ItemId = itemOrder.ItemId,
+ Count = itemOrder.Count
+ };
+ await _db.Statements.AddAsync(statement);
+
+ // Retrieve the item info from the items table
+ var item = _db.Items.FirstOrDefault(i => i.Id == itemOrder.ItemId);
+ if (item == null)
+ throw new NotFoundException("Item not found");
+
+ // Calculate the total amount
+ amount += item.Price * itemOrder.Count;
+ }
+
+ // Call the payment endpoint of Customer service
+ var paymentRequest = new PaymentRequest
+ {
+ TransactionId = _db.CurrentTransactionId,
+ CustomerId = request.CustomerId,
+ Amount = amount
+ };
+ await _customerClient.PaymentAsync(paymentRequest);
+
+ // Return the order id
+ return new PlaceOrderResponse { OrderId = orderId };
+ });
+
+ ///
+ /// Get Order information by order ID
+ ///
+ public override async Task GetOrder(GetOrderRequest request,
+ ServerCallContext context)
+ => await execOperationsAsCoordinator(
+ "Getting an order",
+ async () =>
+ {
+ // Retrieve the order info for the specified order ID
+ var order = _db.Orders.FirstOrDefault(o => o.Id == request.OrderId);
+ if (order == null)
+ throw new NotFoundException("Order not found");
+
+ // Get the customer name from the Customer service
+ var customerName = await getCustomerName(_db.CurrentTransactionId,
+ order.CustomerId);
+
+ // Make an order protobuf to return
+ var rpcOrder = getRpcOrder(order, customerName);
+ return new GetOrderResponse { Order = rpcOrder };
+ });
+
+ ///
+ /// Get Order information by customer ID
+ ///
+ public override async Task GetOrders(GetOrdersRequest request,
+ ServerCallContext context)
+ => await execOperationsAsCoordinator(
+ "Getting orders",
+ async () =>
+ {
+ // Get the customer name from the Customer service
+ var customerName = await getCustomerName(_db.CurrentTransactionId,
+ request.CustomerId);
+
+ // Retrieve the order info for the specified customer ID
+ var response = new GetOrdersResponse();
+ var orders = _db.Orders.Where(order => order.CustomerId == request.CustomerId);
+
+ foreach (var order in orders)
+ {
+ // Make an order protobuf to return
+ var rpcOrder = getRpcOrder(order, customerName);
+ response.Order.Add(rpcOrder);
+ }
+
+ return response;
+ });
+
+ private Rpc.Order getRpcOrder(Order order, string customerName)
+ {
+ var rpcOrder = new Rpc.Order
+ {
+ OrderId = order.Id,
+ CustomerId = order.CustomerId,
+ CustomerName = customerName,
+ Timestamp = order.Timestamp
+ };
+
+ var total = 0;
+
+ // Create statements for the order ID with data from Statements and Items tables
+ var rpcStatements = from statement in _db.Statements
+ join item in _db.Items
+ on statement.ItemId equals item.Id into items
+ from statItem in items.DefaultIfEmpty(Item.EmptyItem)
+ where statement.OrderId == order.Id
+ select new Rpc.Statement
+ {
+ ItemId = statItem.Id,
+ ItemName = statItem.Name,
+ Price = statItem.Price,
+ Count = statement.Count,
+ Total = statItem.Price * statement.Count
+ };
+
+ foreach (var rpcStatement in rpcStatements)
+ {
+ if (rpcStatement.ItemId == Item.EmptyItem.Id)
+ throw new NotFoundException($"Item not found");
+
+ rpcOrder.Statement.Add(rpcStatement);
+
+ total += rpcStatement.Total;
+ }
+
+ rpcOrder.Total = total;
+ return rpcOrder;
+ }
+
+ private async Task getCustomerName(string transactionId, int customerId)
+ {
+ var request = new GetCustomerInfoRequest
+ {
+ TransactionId = transactionId,
+ CustomerId = customerId
+ };
+ var customerInfo = await _customerClient.GetCustomerInfoAsync(request);
+
+ return customerInfo.Name;
+ }
+
+ private async Task execOperationsAsCoordinator(string funcName,
+ Func> operations)
+ {
+ var retryCount = 0;
+ Exception? lastException = null;
+
+ while (true)
+ {
+ if (retryCount++ > 0)
+ {
+ // Retry the transaction three times maximum.
+ if (retryCount >= 3)
+ {
+ // If the transaction failed three times, return an error.
+ _logger.LogError(lastException, "{funcName} failed", funcName);
+
+ if (lastException is RpcException)
+ throw lastException;
+ else
+ throw new InternalException(funcName + " failed", lastException);
+ }
+
+ _logger.LogWarning(lastException, "Retrying the transaction after 100 milliseconds: {funcName}", funcName);
+
+ await Task.Delay(TimeSpan.FromMilliseconds(100));
+ }
+
+ try
+ {
+ // Begin a transaction
+ await _db.BeginTransactionAsync();
+
+ // Execute operations
+ var response = await operations();
+
+ // Commit the transaction
+ await _db.CommitTransactionAsync();
+
+ // Return the response
+ return response;
+ }
+ catch (UnknownTransactionStatusException ex)
+ {
+ // If you catch `UnknownTransactionStatusException`, it indicates that the status of the
+ // transaction, whether it has succeeded or not, is unknown. In such a case, you need to
+ // check if the transaction is committed successfully or not and retry it if it failed.
+ // How to identify a transaction status is delegated to users
+
+ _logger.LogError(ex, "{funcName} failed", funcName);
+ throw new InternalException(funcName + " failed", ex);
+ }
+ catch (RpcException ex)
+ when (ex.StatusCode is StatusCode.NotFound or StatusCode.FailedPrecondition)
+ {
+ // For `NOT_FOUND` and `FAILED_PRECONDITION` gRPC errors, you cannot retry the transaction
+
+ // Rollback the transaction
+ await tryRollbackTransaction();
+ throw;
+ }
+ catch (Exception ex)
+ when (ex is TransactionException or RpcException)
+ {
+ // For other `TransactionException` or gRPC cases, you can try retrying the transaction
+
+ // Rollback the transaction
+ await tryRollbackTransaction();
+
+ // The thrown exception can be retryable. In such case, you can basically retry the
+ // transaction. However, for the other exceptions, the transaction may still fail if the
+ // cause of the exception is nontransient. For such a case, you need to limit the number
+ // of retries and give up retrying
+ lastException = ex;
+ }
+ catch (Exception ex)
+ {
+ // If exception is not inherited from `TransactionException` you cannot retry the transaction
+ _logger.LogError(ex, "{funcName} failed", funcName);
+
+ // Rollback the transaction
+ await tryRollbackTransaction();
+
+ throw;
+ }
+ }
+ }
+
+ private async Task tryRollbackTransaction()
+ {
+ if (String.IsNullOrEmpty(_db.CurrentTransactionId))
+ return;
+
+ try
+ {
+ await _db.RollbackTransactionAsync();
+ }
+ catch (TransactionException ex)
+ {
+ _logger.LogWarning(ex, "Rollback failed");
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "Rollback failed");
+ throw;
+ }
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderService.csproj b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderService.csproj
new file mode 100644
index 00000000..22e54c31
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/OrderService.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net8.0
+ enable
+ enable
+ MicroserviceTransactionsSample.OrderService
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/Program.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/Program.cs
new file mode 100644
index 00000000..51d07bdc
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/Program.cs
@@ -0,0 +1,40 @@
+using ScalarDB.Client.Extensions;
+using static MicroserviceTransactionsSample.Rpc.CustomerService;
+
+namespace MicroserviceTransactionsSample.OrderService;
+
+public class Program
+{
+ public static async Task Main(string[] args)
+ {
+ var customerServiceUrl = Environment.GetEnvironmentVariable("CUSTOMER_SERVICE_URL")!;
+
+ var builder = WebApplication.CreateBuilder(args);
+
+ builder.Services.AddLogging(o =>
+ {
+ o.AddSimpleConsole(options =>
+ {
+ options.TimestampFormat = "HH:mm:ss ";
+ });
+ });
+
+ // Add services to the container.
+ builder.Services.AddGrpc();
+ builder.Services.AddGrpcClient(o =>
+ {
+ o.Address = new Uri(customerServiceUrl);
+ });
+
+ // Add OrderDbContext to the container.
+ builder.Services.AddScalarDbContext();
+
+ var app = builder.Build();
+
+ // Configure the HTTP request pipeline.
+ app.MapGrpcService();
+ app.MapGet("/", () => "Communication with gRPC endpoints must be made through a gRPC client. To learn how to create a client, visit: https://go.microsoft.com/fwlink/?linkid=2086909");
+
+ await app.RunAsync();
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/Properties/launchSettings.json b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/Properties/launchSettings.json
new file mode 100644
index 00000000..bce0acdd
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/Properties/launchSettings.json
@@ -0,0 +1,20 @@
+{
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "applicationUrl": "http://localhost:5167",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true
+ },
+ "https": {
+ "commandName": "Project",
+ "applicationUrl": "https://localhost:7278;http://localhost:5167",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "dotnetRunMessages": true
+ }
+ }
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/appsettings.json b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/appsettings.json
new file mode 100644
index 00000000..777b4b05
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/OrderService/appsettings.json
@@ -0,0 +1,22 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning",
+ "ScalarDB.Client": "Debug"
+ }
+ },
+ "ScalarDbOptions": {
+ "AuthEnabled": true,
+ "Username": "order-service",
+ "Password": "order-service"
+ },
+ "AllowedHosts": "*",
+ "Urls": "http://*:10020",
+ "Kestrel": {
+ "EndpointDefaults": {
+ "Protocols": "Http2"
+ }
+ }
+}
+
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Rpc/Protos/sample.proto b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Rpc/Protos/sample.proto
new file mode 100644
index 00000000..0f46fbfa
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Rpc/Protos/sample.proto
@@ -0,0 +1,178 @@
+syntax = "proto3";
+
+option java_multiple_files = true;
+option java_package = "sample.rpc";
+option java_outer_classname = "Sample";
+option csharp_namespace = "MicroserviceTransactionsSample.Rpc";
+
+package rpc;
+
+// Order Service.
+service OrderService {
+ // Places an order. It's for a global transaction that spans OrderService and CustomerService.
+ rpc PlaceOrder(PlaceOrderRequest) returns (PlaceOrderResponse) {
+ }
+
+ // Retrieves order information by order ID.
+ rpc GetOrder(GetOrderRequest) returns (GetOrderResponse) {
+ }
+
+ // Retrieves order information by customer ID.
+ rpc GetOrders(GetOrdersRequest) returns (GetOrdersResponse) {
+ }
+}
+
+// An item to order.
+message ItemOrder {
+ // The item ID of the item to order.
+ int32 item_id = 1;
+
+ // The number of items to order.
+ int32 count = 2;
+}
+
+// Request message for OrderService.PlaceOrder.
+message PlaceOrderRequest {
+ // The customer ID of the customer who places the order.
+ int32 customer_id = 1;
+
+ // The items to order.
+ repeated ItemOrder item_order = 2;
+}
+
+// Response message for OrderService.PlaceOrder.
+message PlaceOrderResponse {
+ string order_id = 1;
+}
+
+// An order information.
+message Order {
+ // The order ID.
+ string order_id = 1;
+
+ // The timestamp when the order is placed.
+ int64 timestamp = 2;
+
+ // The customer ID of the customer who places the order.
+ int32 customer_id = 3;
+
+ // The name of the customer.
+ string customer_name = 4;
+
+ // The statements of the order.
+ repeated Statement statement = 5;
+
+ // The total price of the order.
+ int32 total = 6;
+}
+
+// A statement of an order.
+message Statement {
+ // The item ID of the item to order.
+ int32 item_id = 1;
+
+ // The name of the item to order.
+ string item_name = 2;
+
+ // The price of the item to order.
+ int32 price = 3;
+
+ // The number of items to order.
+ int32 count = 4;
+
+ // The total price of the item to order.
+ int32 total = 5;
+}
+
+// Request message for OrderService.GetOrder.
+message GetOrderRequest {
+ // The order ID.
+ string order_id = 1;
+}
+
+// Response message for OrderService.GetOrder.
+message GetOrderResponse {
+ // The order information.
+ Order order = 1;
+}
+
+// Request message for OrderService.GetOrders.
+message GetOrdersRequest {
+ // The customer ID of the customer.
+ int32 customer_id = 1;
+}
+
+// Response message for OrderService.GetOrders.
+message GetOrdersResponse {
+ // The order information.
+ repeated Order order = 1;
+}
+
+// Customer Service.
+service CustomerService {
+ // Get customer information. This function processing operations can be used in both a normal
+ // transaction and a global transaction.
+ rpc GetCustomerInfo(GetCustomerInfoRequest) returns (GetCustomerInfoResponse) {
+ }
+
+ // Credit card payment. It's for a global transaction that spans OrderService and CustomerService.
+ rpc Payment(PaymentRequest) returns (PaymentResponse) {
+ }
+
+ // Credit card repayment.
+ rpc Repayment(RepaymentRequest) returns (RepaymentResponse) {
+ }
+}
+
+// Request message for CustomerService.GetCustomerInfo.
+message GetCustomerInfoRequest {
+ // The global transaction ID.
+ optional string transaction_id = 1;
+
+ // The customer ID of the customer.
+ int32 customer_id = 2;
+}
+
+// Response message for CustomerService.GetCustomerInfo.
+message GetCustomerInfoResponse {
+ // The ID of the customer.
+ int32 id = 1;
+
+ // The name of the customer.
+ string name = 2;
+
+ // The credit limit of the customer.
+ int32 credit_limit = 3;
+
+ // The total credit of the customer.
+ int32 credit_total = 4;
+}
+
+// Request message for CustomerService.Payment.
+message PaymentRequest {
+ // The global transaction ID.
+ string transaction_id = 1;
+
+ // The customer ID of the customer.
+ int32 customer_id = 2;
+
+ // The amount of the payment.
+ int32 amount = 3;
+}
+
+// Response message for CustomerService.Payment.
+message PaymentResponse {
+}
+
+// Request message for CustomerService.Repayment.
+message RepaymentRequest {
+ // The customer ID of the customer.
+ int32 customer_id = 1;
+
+ // The amount of the repayment.
+ int32 amount = 2;
+}
+
+// Response message for CustomerService.Repayment.
+message RepaymentResponse {
+}
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Rpc/Rpc.csproj b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Rpc/Rpc.csproj
new file mode 100644
index 00000000..80045655
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Rpc/Rpc.csproj
@@ -0,0 +1,24 @@
+
+
+
+ net8.0
+ enable
+ enable
+ MicroserviceTransactionsSample.Rpc
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/docker-compose.yml b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/docker-compose.yml
new file mode 100644
index 00000000..ca2e3f39
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/docker-compose.yml
@@ -0,0 +1,69 @@
+services:
+
+ mysql:
+ image: mysql:8.0
+ environment:
+ MYSQL_ROOT_PASSWORD: mysql
+ container_name: "mysql-1"
+ restart: always
+ ports:
+ - "3306:3306"
+ networks:
+ - sample-network
+
+ cassandra:
+ image: cassandra:3.11
+ container_name: "cassandra-1"
+ restart: always
+ ports:
+ - "9042:9042"
+ networks:
+ - sample-network
+
+ scalardb-cluster-node:
+ image: ghcr.io/scalar-labs/scalardb-cluster-node-byol-premium:3.13.0
+ container_name: "scalardb-cluster-node-1"
+ restart: always
+ depends_on:
+ - mysql
+ - cassandra
+ ports:
+ - "60053:60053"
+ - "9080:9080"
+ volumes:
+ - ./scalardb-cluster-node.properties:/scalardb-cluster/node/scalardb-cluster-node.properties
+ networks:
+ - sample-network
+
+ customer-service:
+ build:
+ context: .
+ dockerfile: Dockerfile-CustomerService
+ container_name: "customer-service-1"
+ entrypoint: ["dotnet", "CustomerService.dll"]
+ environment:
+ - ScalarDbOptions__Address=http://scalardb-cluster-node:60053
+ restart: "always"
+ ports:
+ - "10010:10010"
+ networks:
+ - sample-network
+
+ order-service:
+ build:
+ context: .
+ dockerfile: Dockerfile-OrderService
+ container_name: "order-service-1"
+ entrypoint: ["dotnet", "OrderService.dll"]
+ environment:
+ - ScalarDbOptions__Address=http://scalardb-cluster-node:60053
+ - CUSTOMER_SERVICE_URL=http://customer-service:10010
+ restart: "always"
+ ports:
+ - "10020:10020"
+ networks:
+ - sample-network
+
+networks:
+ sample-network:
+ name: sample-network
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/scalardb-cluster-node.properties b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/scalardb-cluster-node.properties
new file mode 100644
index 00000000..0ccd294f
--- /dev/null
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/scalardb-cluster-node.properties
@@ -0,0 +1,30 @@
+scalar.db.storage=multi-storage
+scalar.db.multi_storage.storages=cassandra,mysql
+
+# Cassandra for the order service tables
+scalar.db.multi_storage.storages.cassandra.storage=cassandra
+scalar.db.multi_storage.storages.cassandra.contact_points=cassandra
+scalar.db.multi_storage.storages.cassandra.username=cassandra
+scalar.db.multi_storage.storages.cassandra.password=cassandra
+
+# MySQL for the customer service tables
+scalar.db.multi_storage.storages.mysql.storage=jdbc
+scalar.db.multi_storage.storages.mysql.contact_points=jdbc:mysql://mysql:3306/
+scalar.db.multi_storage.storages.mysql.username=root
+scalar.db.multi_storage.storages.mysql.password=mysql
+
+scalar.db.multi_storage.namespace_mapping=coordinator:cassandra,order_service:cassandra,customer_service:mysql
+scalar.db.multi_storage.default_storage=mysql
+
+# Enable SQL
+scalar.db.sql.enabled=true
+
+# Enable standalone mode
+scalar.db.cluster.node.standalone_mode.enabled=true
+
+# Enable ScalarDB Auth
+scalar.db.cluster.auth.enabled=true
+
+# License key configurations
+scalar.db.cluster.node.licensing.license_key=
+scalar.db.cluster.node.licensing.license_check_cert_pem=
\ No newline at end of file
From 9003658078af3ff245c6e6f81157f4320c14c6b2 Mon Sep 17 00:00:00 2001
From: Dima <72738687+Dima-X@users.noreply.github.com>
Date: Tue, 1 Oct 2024 10:39:12 +0900
Subject: [PATCH 2/2] Apply suggestions from code review
Co-authored-by: Josh Wong
---
.../Client/Commands/GetOrdersCommand.cs | 2 +-
.../CustomerService/CustomerService.cs | 2 +-
.../DataLoader/Program.cs | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrdersCommand.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrdersCommand.cs
index 63254583..71073739 100644
--- a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrdersCommand.cs
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/Client/Commands/GetOrdersCommand.cs
@@ -8,7 +8,7 @@ namespace MicroserviceTransactionsSample.Client.Commands;
public static class GetOrdersCommand
{
private const string Name = "GetOrders";
- private const string Description = "Get orders information by customer ID";
+ private const string Description = "Get information about orders by customer ID";
private const string ArgName = "customer_id";
private const string ArgDescription = "customer ID";
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.cs
index 6bd7ca6d..c03ca777 100644
--- a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.cs
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/CustomerService/CustomerService.cs
@@ -101,7 +101,7 @@ public override async Task Repayment(RepaymentRequest request
// and check if over-repayment or not
customer.CreditTotal -= request.Amount;
if (customer.CreditTotal < 0)
- throw new FailedPreconditionException($"Over repayment ({customer.CreditTotal})");
+ throw new FailedPreconditionException($"Over-repayment ({customer.CreditTotal})");
// Save changes to the customer
await _db.Customers.UpdateAsync(customer);
diff --git a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/Program.cs b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/Program.cs
index b9710856..8142100a 100644
--- a/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/Program.cs
+++ b/dotnet/microservice-transactions-sample-with-shared-cluster-with-linq/DataLoader/Program.cs
@@ -12,7 +12,7 @@ class Program
static async Task Main(string[] args)
{
var configOption = new Option("--config", "Path to the config file") { IsRequired = true };
- var resetDataOptions = new Option("--reset-data", "Recreate tables and other data");
+ var resetDataOptions = new Option("--reset-data", "Re-create tables and other data");
var logLevelOption = new Option(name: "--log-level",
description: "Minimum LogLevel",
getDefaultValue: () => LogLevel.Information);