Initial commit
Some checks failed
Continuous Integration - Pull Request / code-tests (pull_request) Has been cancelled
Continuous Integration - Pull Request / deployment-tests (local-code) (pull_request) Has been cancelled
helm-chart-ci / helm-chart-ci (pull_request) Has been cancelled
kubevious-manifests-ci / kubevious-manifests-ci (pull_request) Has been cancelled
kustomize-build-ci / kustomize-build-ci (pull_request) Has been cancelled
terraform-validate-ci / terraform-validate-ci (pull_request) Has been cancelled
Clean up deployment / cleanup-namespace (pull_request) Has been cancelled
Continuous Integration - Main/Release / code-tests (push) Has been cancelled
Continuous Integration - Main/Release / deployment-tests (local-code) (push) Has been cancelled
helm-chart-ci / helm-chart-ci (push) Has been cancelled
kubevious-manifests-ci / kubevious-manifests-ci (push) Has been cancelled
kustomize-build-ci / kustomize-build-ci (push) Has been cancelled
terraform-validate-ci / terraform-validate-ci (push) Has been cancelled

This commit is contained in:
2026-02-04 20:47:56 +05:30
commit dafcd9777f
363 changed files with 52703 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.26124.0
MinimumVisualStudioVersion = 15.0.26124.0
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cartservice", "src\cartservice.csproj", "{2348C29F-E8D3-4955-916D-D609CBC97FCB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cartservice.tests", "tests\cartservice.tests.csproj", "{59825342-CE64-4AFA-8744-781692C0811B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x64.ActiveCfg = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x64.Build.0 = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x86.ActiveCfg = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Debug|x86.Build.0 = Debug|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|Any CPU.Build.0 = Release|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x64.ActiveCfg = Release|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x64.Build.0 = Release|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x86.ActiveCfg = Release|Any CPU
{2348C29F-E8D3-4955-916D-D609CBC97FCB}.Release|x86.Build.0 = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x64.ActiveCfg = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x64.Build.0 = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x86.ActiveCfg = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Debug|x86.Build.0 = Debug|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|Any CPU.Build.0 = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x64.ActiveCfg = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x64.Build.0 = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x86.ActiveCfg = Release|Any CPU
{59825342-CE64-4AFA-8744-781692C0811B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,6 @@
**/*.sh
**/*.bat
**/bin/
**/obj/
**/out/
Dockerfile*

View File

@@ -0,0 +1,41 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# https://mcr.microsoft.com/product/dotnet/sdk
FROM --platform=$BUILDPLATFORM mcr.microsoft.com/dotnet/sdk:10.0.100-noble@sha256:c7445f141c04f1a6b454181bd098dcfa606c61ba0bd213d0a702489e5bd4cd71 AS builder
ARG TARGETARCH
WORKDIR /app
COPY cartservice.csproj .
RUN dotnet restore cartservice.csproj \
-a $TARGETARCH
COPY . .
RUN dotnet publish cartservice.csproj \
-p:PublishSingleFile=true \
-a $TARGETARCH \
--self-contained true \
-p:PublishTrimmed=true \
-p:TrimMode=full \
-c release \
-o /cartservice
# https://mcr.microsoft.com/product/dotnet/runtime-deps
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0.0-noble-chiseled@sha256:b857c8cb8d929183cfe4c6dd9994abba92a2639dd2dbaf06005379f815991604
WORKDIR /app
COPY --from=builder /cartservice .
EXPOSE 7070
ENV DOTNET_EnableDiagnostics=0 \
ASPNETCORE_HTTP_PORTS=7070
USER 1000
ENTRYPOINT ["/app/cartservice"]

View File

@@ -0,0 +1,33 @@
# Copyright 2021 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
FROM mcr.microsoft.com/dotnet/sdk:10.0@sha256:25d14b400b75fa4e89d5bd4487a92a604a4e409ab65becb91821e7dc4ac7f81f AS build
WORKDIR /app
COPY . .
RUN dotnet restore cartservice.csproj
RUN dotnet build "./cartservice.csproj" -c Debug -o /out
FROM build AS publish
RUN dotnet publish cartservice.csproj -c Debug -o /out
# Building final image used in running container
FROM mcr.microsoft.com/dotnet/aspnet:10.0@sha256:1aacc8154bc3071349907dae26849df301188be1a2e1f4560b903fb6275e481a AS final
# Installing procps on the container to enable debugging of .NET Core
RUN apt-get update \
&& apt-get install -y unzip procps wget
WORKDIR /app
COPY --from=publish /out .
ENV ASPNETCORE_HTTP_PORTS=7070
ENTRYPOINT ["dotnet", "cartservice.dll"]

View File

@@ -0,0 +1,26 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using cartservice;
CreateHostBuilder(args).Build().Run();
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});

View File

@@ -0,0 +1,84 @@
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using cartservice.cartstore;
using cartservice.services;
using Microsoft.Extensions.Caching.StackExchangeRedis;
namespace cartservice
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
string redisAddress = Configuration["REDIS_ADDR"];
string spannerProjectId = Configuration["SPANNER_PROJECT"];
string spannerConnectionString = Configuration["SPANNER_CONNECTION_STRING"];
string alloyDBConnectionString = Configuration["ALLOYDB_PRIMARY_IP"];
if (!string.IsNullOrEmpty(redisAddress))
{
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = redisAddress;
});
services.AddSingleton<ICartStore, RedisCartStore>();
}
else if (!string.IsNullOrEmpty(spannerProjectId) || !string.IsNullOrEmpty(spannerConnectionString))
{
services.AddSingleton<ICartStore, SpannerCartStore>();
}
else if (!string.IsNullOrEmpty(alloyDBConnectionString))
{
Console.WriteLine("Creating AlloyDB cart store");
services.AddSingleton<ICartStore, AlloyDBCartStore>();
}
else
{
Console.WriteLine("Redis cache host(hostname+port) was not specified. Starting a cart service using in memory store");
services.AddDistributedMemoryCache();
services.AddSingleton<ICartStore, RedisCartStore>();
}
services.AddGrpc();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapGrpcService<CartService>();
endpoints.MapGrpcService<cartservice.services.HealthCheckService>();
endpoints.MapGet("/", async context =>
{
await context.Response.WriteAsync("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");
});
});
}
}
}

View File

@@ -0,0 +1,15 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*",
"Kestrel": {
"EndpointDefaults": {
"Protocols": "Http2"
}
}
}

View File

@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.AspNetCore" Version="2.76.0" />
<PackageReference Include="Grpc.HealthCheck" Version="2.76.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.2" />
<PackageReference Include="Google.Cloud.Spanner.Data" Version="5.12.0" />
<PackageReference Include="Npgsql" Version="10.0.1" />
<PackageReference Include="Google.Cloud.SecretManager.V1" Version="2.7.0" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="protos\Cart.proto" GrpcServices="Both" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,177 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using Grpc.Core;
using Npgsql;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
using Google.Api.Gax.ResourceNames;
using Google.Cloud.SecretManager.V1;
namespace cartservice.cartstore
{
public class AlloyDBCartStore : ICartStore
{
private readonly string tableName;
private readonly string connectionString;
public AlloyDBCartStore(IConfiguration configuration)
{
// Create a Cloud Secrets client.
SecretManagerServiceClient client = SecretManagerServiceClient.Create();
var projectId = configuration["PROJECT_ID"];
var secretId = configuration["ALLOYDB_SECRET_NAME"];
SecretVersionName secretVersionName = new SecretVersionName(projectId, secretId, "latest");
AccessSecretVersionResponse result = client.AccessSecretVersion(secretVersionName);
// Convert the payload to a string. Payloads are bytes by default.
string alloyDBPassword = result.Payload.Data.ToStringUtf8().TrimEnd('\r', '\n');
// TODO: Create a separate user for connecting within the application
// rather than using our superuser
string alloyDBUser = "postgres";
string databaseName = configuration["ALLOYDB_DATABASE_NAME"];
// TODO: Consider splitting workloads into read vs. write and take
// advantage of the AlloyDB read pools
string primaryIPAddress = configuration["ALLOYDB_PRIMARY_IP"];
connectionString = "Host=" +
primaryIPAddress +
";Username=" +
alloyDBUser +
";Password=" +
alloyDBPassword +
";Database=" +
databaseName;
tableName = configuration["ALLOYDB_TABLE_NAME"];
}
public async Task AddItemAsync(string userId, string productId, int quantity)
{
Console.WriteLine($"AddItemAsync for {userId} called");
try
{
await using var dataSource = NpgsqlDataSource.Create(connectionString);
var fetchCmd = $"SELECT quantity FROM {tableName} WHERE userID='{userId}' AND productID='{productId}'";
var currentQuantity = 0;
var cmdRead = dataSource.CreateCommand(fetchCmd);
await using (var reader = await cmdRead.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
currentQuantity += reader.GetInt32(0);
}
var totalQuantity = quantity + currentQuantity;
// Use INSERT ... ON CONFLICT to prevent duplicate key error
var insertCmd = $@"
INSERT INTO {tableName} (userId, productId, quantity)
VALUES ('{userId}', '{productId}', {totalQuantity})
ON CONFLICT (userId, productId)
DO UPDATE SET quantity = {totalQuantity};
";
await using (var cmdInsert = dataSource.CreateCommand(insertCmd))
{
await Task.Run(() =>
{
return cmdInsert.ExecuteNonQueryAsync();
});
}
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Unable to access cart storage due to an internal error. {ex}"));
}
}
public async Task<Hipstershop.Cart> GetCartAsync(string userId)
{
Console.WriteLine($"GetCartAsync called for userId={userId}");
Hipstershop.Cart cart = new();
cart.UserId = userId;
try
{
await using var dataSource = NpgsqlDataSource.Create(connectionString);
var cartFetchCmd = $"SELECT productId, quantity FROM {tableName} WHERE userId = '{userId}'";
var cmd = dataSource.CreateCommand(cartFetchCmd);
await using (var reader = await cmd.ExecuteReaderAsync())
{
while (await reader.ReadAsync())
{
Hipstershop.CartItem item = new()
{
ProductId = reader.GetString(0),
Quantity = reader.GetInt32(1)
};
cart.Items.Add(item);
}
}
await Task.Run(() =>
{
return cart;
});
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Unable to access cart storage due to an internal error. {ex}"));
}
return cart;
}
public async Task EmptyCartAsync(string userId)
{
Console.WriteLine($"EmptyCartAsync called for userId={userId}");
try
{
await using var dataSource = NpgsqlDataSource.Create(connectionString);
var deleteCmd = $"DELETE FROM {tableName} WHERE userID = '{userId}'";
await using (var cmd = dataSource.CreateCommand(deleteCmd))
{
await Task.Run(() =>
{
return cmd.ExecuteNonQueryAsync();
});
}
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Unable to access cart storage due to an internal error. {ex}"));
}
}
public bool Ping()
{
try
{
return true;
}
catch (Exception)
{
return false;
}
}
}
}

View File

@@ -0,0 +1,26 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System.Threading.Tasks;
namespace cartservice.cartstore
{
public interface ICartStore
{
Task AddItemAsync(string userId, string productId, int quantity);
Task EmptyCartAsync(string userId);
Task<Hipstershop.Cart> GetCartAsync(string userId);
bool Ping();
}
}

View File

@@ -0,0 +1,118 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Linq;
using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.Extensions.Caching.Distributed;
using Google.Protobuf;
namespace cartservice.cartstore
{
public class RedisCartStore : ICartStore
{
private readonly IDistributedCache _cache;
public RedisCartStore(IDistributedCache cache)
{
_cache = cache;
}
public async Task AddItemAsync(string userId, string productId, int quantity)
{
Console.WriteLine($"AddItemAsync called with userId={userId}, productId={productId}, quantity={quantity}");
try
{
Hipstershop.Cart cart;
var value = await _cache.GetAsync(userId);
if (value == null)
{
cart = new Hipstershop.Cart();
cart.UserId = userId;
cart.Items.Add(new Hipstershop.CartItem { ProductId = productId, Quantity = quantity });
}
else
{
cart = Hipstershop.Cart.Parser.ParseFrom(value);
var existingItem = cart.Items.SingleOrDefault(i => i.ProductId == productId);
if (existingItem == null)
{
cart.Items.Add(new Hipstershop.CartItem { ProductId = productId, Quantity = quantity });
}
else
{
existingItem.Quantity += quantity;
}
}
await _cache.SetAsync(userId, cart.ToByteArray());
}
catch (Exception ex)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}"));
}
}
public async Task EmptyCartAsync(string userId)
{
Console.WriteLine($"EmptyCartAsync called with userId={userId}");
try
{
var cart = new Hipstershop.Cart();
await _cache.SetAsync(userId, cart.ToByteArray());
}
catch (Exception ex)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}"));
}
}
public async Task<Hipstershop.Cart> GetCartAsync(string userId)
{
Console.WriteLine($"GetCartAsync called with userId={userId}");
try
{
// Access the cart from the cache
var value = await _cache.GetAsync(userId);
if (value != null)
{
return Hipstershop.Cart.Parser.ParseFrom(value);
}
// We decided to return empty cart in cases when user wasn't in the cache before
return new Hipstershop.Cart();
}
catch (Exception ex)
{
throw new RpcException(new Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}"));
}
}
public bool Ping()
{
try
{
return true;
}
catch (Exception)
{
return false;
}
}
}
}

View File

@@ -0,0 +1,185 @@
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using Google.Cloud.Spanner.Data;
using Grpc.Core;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks;
namespace cartservice.cartstore
{
public class SpannerCartStore : ICartStore
{
private static readonly string TableName = "CartItems";
private static readonly string DefaultInstanceName = "onlineboutique";
private static readonly string DefaultDatabaseName = "carts";
private readonly string databaseString;
public SpannerCartStore(IConfiguration configuration)
{
string spannerProjectId = configuration["SPANNER_PROJECT"];
string spannerInstanceId = configuration["SPANNER_INSTANCE"];
string spannerDatabaseId = configuration["SPANNER_DATABASE"];
string spannerConnectionString = configuration["SPANNER_CONNECTION_STRING"];
SpannerConnectionStringBuilder builder = new();
if (!string.IsNullOrEmpty(spannerConnectionString)) {
builder.DataSource = spannerConnectionString;
databaseString = builder.ToString();
Console.WriteLine($"Spanner connection string: ${databaseString}");
return;
}
if (string.IsNullOrEmpty(spannerInstanceId))
spannerInstanceId = DefaultInstanceName;
if (string.IsNullOrEmpty(spannerDatabaseId))
spannerDatabaseId = DefaultDatabaseName;
builder.DataSource =
$"projects/{spannerProjectId}/instances/{spannerInstanceId}/databases/{spannerDatabaseId}";
databaseString = builder.ToString();
Console.WriteLine($"Built Spanner connection string: '{databaseString}'");
}
public async Task AddItemAsync(string userId, string productId, int quantity)
{
Console.WriteLine($"AddItemAsync for {userId} called");
try
{
using SpannerConnection spannerConnection = new(databaseString);
await spannerConnection.RunWithRetriableTransactionAsync(async transaction =>
{
int currentQuantity = 0;
var quantityLookup = spannerConnection.CreateSelectCommand(
$"SELECT * FROM {TableName} WHERE userId = @userId AND productId = @productId",
new SpannerParameterCollection
{
{ "userId", SpannerDbType.String },
{ "productId", SpannerDbType.String }
});
quantityLookup.Parameters["userId"].Value = userId;
quantityLookup.Parameters["productId"].Value = productId;
quantityLookup.Transaction = transaction;
using (var reader = await quantityLookup.ExecuteReaderAsync())
{
while (await reader.ReadAsync()) {
currentQuantity += reader.GetFieldValue<int>("quantity");
}
}
var cmd = spannerConnection.CreateInsertOrUpdateCommand(TableName,
new SpannerParameterCollection
{
{ "userId", SpannerDbType.String },
{ "productId", SpannerDbType.String },
{ "quantity", SpannerDbType.Int64 }
});
cmd.Parameters["userId"].Value = userId;
cmd.Parameters["productId"].Value = productId;
cmd.Parameters["quantity"].Value = currentQuantity + quantity;
cmd.Transaction = transaction;
await Task.Run(() =>
{
return cmd.ExecuteNonQueryAsync();
});
});
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Can't access cart storage at {databaseString}. {ex}"));
}
}
public async Task<Hipstershop.Cart> GetCartAsync(string userId)
{
Console.WriteLine($"GetCartAsync called for userId={userId}");
Hipstershop.Cart cart = new();
try
{
using SpannerConnection spannerConnection = new(databaseString);
var cmd = spannerConnection.CreateSelectCommand(
$"SELECT * FROM {TableName} WHERE userId = @userId",
new SpannerParameterCollection {
{ "userId", SpannerDbType.String }
}
);
cmd.Parameters["userId"].Value = userId;
using var reader = await cmd.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
// Only add the userId if something is in the cart.
// This is based on how the cartservice example behaves.
// An empty cart has no userId attached.
cart.UserId = userId;
Hipstershop.CartItem item = new()
{
ProductId = reader.GetFieldValue<string>("productId"),
Quantity = reader.GetFieldValue<int>("quantity")
};
cart.Items.Add(item);
}
return cart;
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Can't access cart storage at {databaseString}. {ex}"));
}
}
public async Task EmptyCartAsync(string userId)
{
Console.WriteLine($"EmptyCartAsync called for userId={userId}");
try
{
using SpannerConnection spannerConnection = new(databaseString);
await Task.Run(() =>
{
var cmd = spannerConnection.CreateDmlCommand(
$"DELETE FROM {TableName} WHERE userId = @userId",
new SpannerParameterCollection
{
{ "userId", SpannerDbType.String }
});
cmd.Parameters["userId"].Value = userId;
return cmd.ExecuteNonQueryAsync();
});
}
catch (Exception ex)
{
throw new RpcException(
new Status(StatusCode.FailedPrecondition, $"Can't access cart storage at {databaseString}. {ex}"));
}
}
public bool Ping()
{
try
{
return true;
}
catch (Exception)
{
return false;
}
}
}
}

View File

@@ -0,0 +1,50 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
syntax = "proto3";
package hipstershop;
// -----------------Cart service-----------------
service CartService {
rpc AddItem(AddItemRequest) returns (Empty) {}
rpc GetCart(GetCartRequest) returns (Cart) {}
rpc EmptyCart(EmptyCartRequest) returns (Empty) {}
}
message CartItem {
string product_id = 1;
int32 quantity = 2;
}
message AddItemRequest {
string user_id = 1;
CartItem item = 2;
}
message EmptyCartRequest {
string user_id = 1;
}
message GetCartRequest {
string user_id = 1;
}
message Cart {
string user_id = 1;
repeated CartItem items = 2;
}
message Empty {}

View File

@@ -0,0 +1,51 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Microsoft.Extensions.Logging;
using cartservice.cartstore;
using Hipstershop;
namespace cartservice.services
{
public class CartService : Hipstershop.CartService.CartServiceBase
{
private readonly static Empty Empty = new Empty();
private readonly ICartStore _cartStore;
public CartService(ICartStore cartStore)
{
_cartStore = cartStore;
}
public async override Task<Empty> AddItem(AddItemRequest request, ServerCallContext context)
{
await _cartStore.AddItemAsync(request.UserId, request.Item.ProductId, request.Item.Quantity);
return Empty;
}
public override Task<Cart> GetCart(GetCartRequest request, ServerCallContext context)
{
return _cartStore.GetCartAsync(request.UserId);
}
public async override Task<Empty> EmptyCart(EmptyCartRequest request, ServerCallContext context)
{
await _cartStore.EmptyCartAsync(request.UserId);
return Empty;
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Threading.Tasks;
using Grpc.Core;
using Grpc.Health.V1;
using static Grpc.Health.V1.Health;
using cartservice.cartstore;
namespace cartservice.services
{
internal class HealthCheckService : HealthBase
{
private ICartStore _cartStore { get; }
public HealthCheckService (ICartStore cartStore)
{
_cartStore = cartStore;
}
public override Task<HealthCheckResponse> Check(HealthCheckRequest request, ServerCallContext context)
{
Console.WriteLine ("Checking CartService Health");
return Task.FromResult(new HealthCheckResponse {
Status = _cartStore.Ping() ? HealthCheckResponse.Types.ServingStatus.Serving : HealthCheckResponse.Types.ServingStatus.NotServing
});
}
}
}

View File

@@ -0,0 +1,160 @@
// Copyright 2018 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
using System;
using System.Threading.Tasks;
using Grpc.Net.Client;
using Hipstershop;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Hosting;
using Xunit;
using static Hipstershop.CartService;
namespace cartservice.tests
{
public class CartServiceTests
{
private readonly IHostBuilder _host;
public CartServiceTests()
{
_host = new HostBuilder().ConfigureWebHost(webBuilder =>
{
webBuilder
.UseStartup<Startup>()
.UseTestServer();
});
}
[Fact]
public async Task GetItem_NoAddItemBefore_EmptyCartReturned()
{
// Setup test server and client
using var server = await _host.StartAsync();
var httpClient = server.GetTestClient();
string userId = Guid.NewGuid().ToString();
// Create a GRPC communication channel between the client and the server
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
{
HttpClient = httpClient
});
var cartClient = new CartServiceClient(channel);
var request = new GetCartRequest
{
UserId = userId,
};
var cart = await cartClient.GetCartAsync(request);
Assert.NotNull(cart);
// All grpc objects implement IEquitable, so we can compare equality with by-value semantics
Assert.Equal(new Cart(), cart);
}
[Fact]
public async Task AddItem_ItemExists_Updated()
{
// Setup test server and client
using var server = await _host.StartAsync();
var httpClient = server.GetTestClient();
string userId = Guid.NewGuid().ToString();
// Create a GRPC communication channel between the client and the server
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
{
HttpClient = httpClient
});
var client = new CartServiceClient(channel);
var request = new AddItemRequest
{
UserId = userId,
Item = new CartItem
{
ProductId = "1",
Quantity = 1
}
};
// First add - nothing should fail
await client.AddItemAsync(request);
// Second add of existing product - quantity should be updated
await client.AddItemAsync(request);
var getCartRequest = new GetCartRequest
{
UserId = userId
};
var cart = await client.GetCartAsync(getCartRequest);
Assert.NotNull(cart);
Assert.Equal(userId, cart.UserId);
Assert.Single(cart.Items);
Assert.Equal(2, cart.Items[0].Quantity);
// Cleanup
await client.EmptyCartAsync(new EmptyCartRequest { UserId = userId });
}
[Fact]
public async Task AddItem_New_Inserted()
{
// Setup test server and client
using var server = await _host.StartAsync();
var httpClient = server.GetTestClient();
string userId = Guid.NewGuid().ToString();
// Create a GRPC communication channel between the client and the server
var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions
{
HttpClient = httpClient
});
// Create a proxy object to work with the server
var client = new CartServiceClient(channel);
var request = new AddItemRequest
{
UserId = userId,
Item = new CartItem
{
ProductId = "1",
Quantity = 1
}
};
await client.AddItemAsync(request);
var getCartRequest = new GetCartRequest
{
UserId = userId
};
var cart = await client.GetCartAsync(getCartRequest);
Assert.NotNull(cart);
Assert.Equal(userId, cart.UserId);
Assert.Single(cart.Items);
await client.EmptyCartAsync(new EmptyCartRequest { UserId = userId });
cart = await client.GetCartAsync(getCartRequest);
Assert.Empty(cart.Items);
}
}
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Grpc.Net.Client" Version="2.76.0" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="10.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\cartservice.csproj" />
</ItemGroup>
</Project>