From 8d8e2e40dff9511b42faac670e297dfedda34693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Kie=C5=82kowicz?= Date: Fri, 24 Nov 2023 11:09:22 +0100 Subject: [PATCH] [cartservice[ Update to .NET8 --- src/cartservice/CartServiceImpl.cs | 160 +++++----- src/cartservice/Dockerfile | 4 +- src/cartservice/HealthImpl.cs | 26 +- src/cartservice/Program.cs | 277 +++++++++--------- src/cartservice/cartservice.csproj | 18 +- src/cartservice/cartstore/Bogo.cs | 61 ++-- src/cartservice/cartstore/ConfigHelper.cs | 27 +- src/cartservice/cartstore/DatabaseCache.cs | 41 ++- src/cartservice/cartstore/LocalCartStore.cs | 81 +++-- src/cartservice/cartstore/RedisCartStore.cs | 229 +++++++-------- src/cartservice/cartstore/UserId.cs | 165 ++++++----- src/cartservice/interfaces/ICartStore.cs | 17 +- src/cartservice/tests/CartServiceTests.cs | 183 ++++++------ .../tests/cartservice.tests.csproj | 14 +- 14 files changed, 643 insertions(+), 660 deletions(-) diff --git a/src/cartservice/CartServiceImpl.cs b/src/cartservice/CartServiceImpl.cs index c3580047ac8..ce0a7e4e016 100644 --- a/src/cartservice/CartServiceImpl.cs +++ b/src/cartservice/CartServiceImpl.cs @@ -21,106 +21,104 @@ using static Hipstershop.CartService; using System.Diagnostics; -namespace cartservice -{ +namespace cartservice; - // Cart wrapper to deal with grpc communication - internal class CartServiceImpl : CartServiceBase - { - private ICartStore cartStore; - private static readonly Empty Empty = new(); +// Cart wrapper to deal with grpc communication +internal class CartServiceImpl : CartServiceBase +{ + private ICartStore cartStore; + private static readonly Empty Empty = new(); - private static readonly Func> Getter = - (md, key) => + private static readonly Func> Getter = + (md, key) => + { + List result = new List(); + foreach (var item in md.GetAll(key.ToLower())) { - List result = new List(); - foreach (var item in md.GetAll(key.ToLower())) - { - result.Add(item.Value); - } - return result; - }; + result.Add(item.Value); + } + return result; + }; - public CartServiceImpl(ICartStore cartStore) - { - this.cartStore = cartStore; - } + public CartServiceImpl(ICartStore cartStore) + { + this.cartStore = cartStore; + } - // Simplified implementation for B3multi propagator - // It is needed as we do not have support for Grpc server in OpenTelemetry .NET Instrumentation - public ActivityContext TraceContextFromGrpcContext(ServerCallContext context) + // Simplified implementation for B3multi propagator + // It is needed as we do not have support for Grpc server in OpenTelemetry .NET Instrumentation + public ActivityContext TraceContextFromGrpcContext(ServerCallContext context) + { + try { - try + string traceId = null; + string spanId = null; + string sampled = null; + foreach (var header in context.RequestHeaders) { - string traceId = null; - string spanId = null; - string sampled = null; - foreach (var header in context.RequestHeaders) + if (header.IsBinary) { - if (header.IsBinary) - { - continue; - } - - switch (header.Key.ToLowerInvariant()) - { - case "x-b3-traceid": - traceId = header.Value; - if (traceId.Length == 16) - { - traceId = "0000000000000000" + traceId; - } - break; - case "x-b3-spanid": - spanId = header.Value; - break; - case "x-b3-sampled": - sampled = header.Value; - break; - } + continue; } - return !string.IsNullOrEmpty(traceId) && !string.IsNullOrEmpty(spanId) && !string.IsNullOrEmpty(sampled) - ? new ActivityContext(ActivityTraceId.CreateFromString(traceId), - ActivitySpanId.CreateFromString(spanId), - sampled == "1" ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None, isRemote: true) - : new ActivityContext(); - } - catch - { - return new ActivityContext(); + switch (header.Key.ToLowerInvariant()) + { + case "x-b3-traceid": + traceId = header.Value; + if (traceId.Length == 16) + { + traceId = "0000000000000000" + traceId; + } + break; + case "x-b3-spanid": + spanId = header.Value; + break; + case "x-b3-sampled": + sampled = header.Value; + break; + } } - } - public override async Task AddItem(AddItemRequest request, ServerCallContext context) + return !string.IsNullOrEmpty(traceId) && !string.IsNullOrEmpty(spanId) && !string.IsNullOrEmpty(sampled) + ? new ActivityContext(ActivityTraceId.CreateFromString(traceId), + ActivitySpanId.CreateFromString(spanId), + sampled == "1" ? ActivityTraceFlags.Recorded : ActivityTraceFlags.None, isRemote: true) + : new ActivityContext(); + } + catch { - using var activity = ActivitySourceUtil.ActivitySource.StartActivity("AddItem", ActivityKind.Server, TraceContextFromGrpcContext(context)); - activity?.SetTag("component", "rpc"); - activity?.SetTag("grpc.method", "/hipstershop.CartService/AddItem"); - - await cartStore.AddItemAsync(request.UserId, request.Item.ProductId, request.Item.Quantity); - return Empty; + return new ActivityContext(); } + } - public override async Task EmptyCart(EmptyCartRequest request, ServerCallContext context) - { - using var activity = ActivitySourceUtil.ActivitySource.StartActivity("EmptyCart", ActivityKind.Server, TraceContextFromGrpcContext(context)); - activity?.SetTag("component", "rpc"); - activity?.SetTag("grpc.method", "/hipstershop.CartService/EmptyCart"); + public override async Task AddItem(AddItemRequest request, ServerCallContext context) + { + using var activity = ActivitySourceUtil.ActivitySource.StartActivity("AddItem", ActivityKind.Server, TraceContextFromGrpcContext(context)); + activity?.SetTag("component", "rpc"); + activity?.SetTag("grpc.method", "/hipstershop.CartService/AddItem"); - await cartStore.EmptyCartAsync(request.UserId); - return Empty; - } + await cartStore.AddItemAsync(request.UserId, request.Item.ProductId, request.Item.Quantity); + return Empty; + } - public override Task GetCart(GetCartRequest request, ServerCallContext context) + public override async Task EmptyCart(EmptyCartRequest request, ServerCallContext context) + { + using var activity = ActivitySourceUtil.ActivitySource.StartActivity("EmptyCart", ActivityKind.Server, TraceContextFromGrpcContext(context)); + activity?.SetTag("component", "rpc"); + activity?.SetTag("grpc.method", "/hipstershop.CartService/EmptyCart"); + + await cartStore.EmptyCartAsync(request.UserId); + return Empty; + } + + public override Task GetCart(GetCartRequest request, ServerCallContext context) + { + using var activity = ActivitySourceUtil.ActivitySource.StartActivity("GetCart", ActivityKind.Server, TraceContextFromGrpcContext(context)); { - using var activity = ActivitySourceUtil.ActivitySource.StartActivity("GetCart", ActivityKind.Server, TraceContextFromGrpcContext(context)); - { - activity?.SetTag("component", "rpc"); - activity?.SetTag("grpc.method", "/hipstershop.CartService/GetCart"); + activity?.SetTag("component", "rpc"); + activity?.SetTag("grpc.method", "/hipstershop.CartService/GetCart"); - return cartStore.GetCartAsync(request.UserId); - } + return cartStore.GetCartAsync(request.UserId); } } } \ No newline at end of file diff --git a/src/cartservice/Dockerfile b/src/cartservice/Dockerfile index 3526fc253b2..b7ccf448c9e 100644 --- a/src/cartservice/Dockerfile +++ b/src/cartservice/Dockerfile @@ -1,4 +1,4 @@ -ARG NET_VERSION=7.0 +ARG NET_VERSION=8.0 FROM mcr.microsoft.com/dotnet/sdk:${NET_VERSION} as builder WORKDIR /app @@ -14,7 +14,7 @@ FROM mcr.microsoft.com/dotnet/aspnet:${NET_VERSION} RUN apt-get update RUN apt-get -y dist-upgrade -ARG GRPC_HEALTH_PROBE_VERSION=v0.4.11 +ARG GRPC_HEALTH_PROBE_VERSION=v0.4.22 ADD https://github.com/grpc-ecosystem/grpc-health-probe/releases/download/${GRPC_HEALTH_PROBE_VERSION}/grpc_health_probe-linux-amd64 /bin/grpc_health_probe RUN chmod +x /bin/grpc_health_probe diff --git a/src/cartservice/HealthImpl.cs b/src/cartservice/HealthImpl.cs index 9cdcbfaceff..b900d68260d 100644 --- a/src/cartservice/HealthImpl.cs +++ b/src/cartservice/HealthImpl.cs @@ -1,22 +1,20 @@ -using System; using System.Threading.Tasks; using cartservice.interfaces; using Grpc.Core; using Grpc.Health.V1; -using StackExchange.Redis; using static Grpc.Health.V1.Health; -namespace cartservice { - internal class HealthImpl : HealthBase { - private ICartStore dependency { get; } - public HealthImpl (ICartStore dependency) { - this.dependency = dependency; - } +namespace cartservice; - public override Task Check(HealthCheckRequest request, ServerCallContext context){ - return Task.FromResult(new HealthCheckResponse { - Status = dependency.Ping() ? HealthCheckResponse.Types.ServingStatus.Serving : HealthCheckResponse.Types.ServingStatus.NotServing - }); - } +internal class HealthImpl : HealthBase { + private ICartStore dependency { get; } + public HealthImpl (ICartStore dependency) { + this.dependency = dependency; } -} + + public override Task Check(HealthCheckRequest request, ServerCallContext context){ + return Task.FromResult(new HealthCheckResponse { + Status = dependency.Ping() ? HealthCheckResponse.Types.ServingStatus.Serving : HealthCheckResponse.Types.ServingStatus.NotServing + }); + } +} \ No newline at end of file diff --git a/src/cartservice/Program.cs b/src/cartservice/Program.cs index 8b051b79f5e..8c9adf170b2 100644 --- a/src/cartservice/Program.cs +++ b/src/cartservice/Program.cs @@ -21,10 +21,10 @@ using Grpc.Core; using StackExchange.Redis; -namespace cartservice +namespace cartservice; + +class Program { - class Program - { const string CART_SERVICE_ADDRESS = "LISTEN_ADDR"; const string REDIS_ADDRESS = "REDIS_ADDR"; const string CART_SERVICE_PORT = "PORT"; @@ -33,170 +33,169 @@ class Program [Verb("start", HelpText = "Starts the server listening on provided port")] class ServerOptions { - [Option('h', "hostname", HelpText = "The ip on which the server is running. If not provided, LISTEN_ADDR environment variable value will be used. If not defined, localhost is used")] - public string Host { get; set; } + [Option('h', "hostname", HelpText = "The ip on which the server is running. If not provided, LISTEN_ADDR environment variable value will be used. If not defined, localhost is used")] + public string Host { get; set; } - [Option('p', "port", HelpText = "The port on for running the server")] - public int Port { get; set; } + [Option('p', "port", HelpText = "The port on for running the server")] + public int Port { get; set; } - [Option('r', "redis", HelpText = "The ip of redis cache")] - public string Redis { get; set; } + [Option('r', "redis", HelpText = "The ip of redis cache")] + public string Redis { get; set; } } static object StartServer(string host, int port, ICartStore cartStore) { - // Run the server in a separate thread and make the main thread busy waiting. - // The busy wait is because when we run in a container, we can't use techniques such as waiting on user input (Console.Readline()) - Task serverTask = Task.Run(async () => - { - try - { - await cartStore.InitializeAsync(); - - Console.WriteLine($"Trying to start a grpc server at {host}:{port}"); - Server server = new Server - { - Services = - { - // Cart Service Endpoint - Hipstershop.CartService.BindService(new CartServiceImpl(cartStore)), - - // Health Endpoint - Grpc.Health.V1.Health.BindService(new HealthImpl(cartStore)) - }, - Ports = { new ServerPort(host, port, ServerCredentials.Insecure) } - }; - - Console.WriteLine($"Cart server is listening at {host}:{port}"); - server.Start(); - - Console.WriteLine("Initialization completed"); - - // Keep the server up and running - while (true) - { - Thread.Sleep(TimeSpan.FromMinutes(10)); - } - } - catch (Exception ex) + // Run the server in a separate thread and make the main thread busy waiting. + // The busy wait is because when we run in a container, we can't use techniques such as waiting on user input (Console.Readline()) + Task serverTask = Task.Run(async () => { - Console.WriteLine(ex); - } - }); + try + { + await cartStore.InitializeAsync(); - return Task.WaitAny(new[] { serverTask }); - } - - static void Main(string[] args) - { - if (args.Length == 0) - { - Console.WriteLine("Invalid number of arguments supplied"); - Environment.Exit(-1); - } - - switch (args[0]) - { - case "start": - Parser.Default.ParseArguments(args).MapResult( - (ServerOptions options) => - { - var redis = NewRedisConnection(options.Redis); - Console.WriteLine($"Started as process with id {System.Diagnostics.Process.GetCurrentProcess().Id}"); - - // Set hostname/ip address - string hostname = options.Host; - if (string.IsNullOrEmpty(hostname)) + Console.WriteLine($"Trying to start a grpc server at {host}:{port}"); + Server server = new Server { - Console.WriteLine($"Reading host address from {CART_SERVICE_ADDRESS} environment variable"); - hostname = Environment.GetEnvironmentVariable(CART_SERVICE_ADDRESS); - if (string.IsNullOrEmpty(hostname)) - { - Console.WriteLine($"Environment variable {CART_SERVICE_ADDRESS} was not set. Setting the host to 0.0.0.0"); - hostname = "0.0.0.0"; - } - } + Services = + { + // Cart Service Endpoint + Hipstershop.CartService.BindService(new CartServiceImpl(cartStore)), + + // Health Endpoint + Grpc.Health.V1.Health.BindService(new HealthImpl(cartStore)) + }, + Ports = { new ServerPort(host, port, ServerCredentials.Insecure) } + }; + + Console.WriteLine($"Cart server is listening at {host}:{port}"); + server.Start(); + + Console.WriteLine("Initialization completed"); - // Set the port - int port = options.Port; - if (options.Port <= 0) + // Keep the server up and running + while (true) { - Console.WriteLine($"Reading cart service port from {CART_SERVICE_PORT} environment variable"); - string portStr = Environment.GetEnvironmentVariable(CART_SERVICE_PORT); - if (string.IsNullOrEmpty(portStr)) - { - Console.WriteLine($"{CART_SERVICE_PORT} environment variable was not set. Setting the port to 8080"); - port = 8080; - } - else - { - port = int.Parse(portStr); - } + Thread.Sleep(TimeSpan.FromMinutes(10)); } + } + catch (Exception ex) + { + Console.WriteLine(ex); + } + }); + + return Task.WaitAny(new[] { serverTask }); + } - // Set redis cache host (hostname+port) - ICartStore cartStore; - - // Redis was specified via command line or environment variable - // If you want to start cart store using local cache in process, you can replace the following line with this: - // cartStore = new LocalCartStore(); - cartStore = new RedisCartStore(redis); - - return StartServer(hostname, port, cartStore); - }, - errs => 1); - break; - default: - Console.WriteLine("Invalid command"); - break; - } + static void Main(string[] args) + { + if (args.Length == 0) + { + Console.WriteLine("Invalid number of arguments supplied"); + Environment.Exit(-1); + } + + switch (args[0]) + { + case "start": + Parser.Default.ParseArguments(args).MapResult( + (ServerOptions options) => + { + var redis = NewRedisConnection(options.Redis); + Console.WriteLine($"Started as process with id {System.Diagnostics.Process.GetCurrentProcess().Id}"); + + // Set hostname/ip address + string hostname = options.Host; + if (string.IsNullOrEmpty(hostname)) + { + Console.WriteLine($"Reading host address from {CART_SERVICE_ADDRESS} environment variable"); + hostname = Environment.GetEnvironmentVariable(CART_SERVICE_ADDRESS); + if (string.IsNullOrEmpty(hostname)) + { + Console.WriteLine($"Environment variable {CART_SERVICE_ADDRESS} was not set. Setting the host to 0.0.0.0"); + hostname = "0.0.0.0"; + } + } + + // Set the port + int port = options.Port; + if (options.Port <= 0) + { + Console.WriteLine($"Reading cart service port from {CART_SERVICE_PORT} environment variable"); + string portStr = Environment.GetEnvironmentVariable(CART_SERVICE_PORT); + if (string.IsNullOrEmpty(portStr)) + { + Console.WriteLine($"{CART_SERVICE_PORT} environment variable was not set. Setting the port to 8080"); + port = 8080; + } + else + { + port = int.Parse(portStr); + } + } + + // Set redis cache host (hostname+port) + ICartStore cartStore; + + // Redis was specified via command line or environment variable + // If you want to start cart store using local cache in process, you can replace the following line with this: + // cartStore = new LocalCartStore(); + cartStore = new RedisCartStore(redis); + + return StartServer(hostname, port, cartStore); + }, + errs => 1); + break; + default: + Console.WriteLine("Invalid command"); + break; + } } private static ConnectionMultiplexer NewRedisConnection(string address) { - address = ReadRedisAddress(address); - var connectionString = $"{address},ssl=false,allowAdmin=true,connectRetry=5"; - var redisConnectionOptions = ConfigurationOptions.Parse(connectionString); + address = ReadRedisAddress(address); + var connectionString = $"{address},ssl=false,allowAdmin=true,connectRetry=5"; + var redisConnectionOptions = ConfigurationOptions.Parse(connectionString); - // Try to reconnect if first retry failed (up to 5 times with exponential backoff) - redisConnectionOptions.ConnectRetry = REDIS_RETRY_NUM; - redisConnectionOptions.ReconnectRetryPolicy = new ExponentialRetry(100); + // Try to reconnect if first retry failed (up to 5 times with exponential backoff) + redisConnectionOptions.ConnectRetry = REDIS_RETRY_NUM; + redisConnectionOptions.ReconnectRetryPolicy = new ExponentialRetry(100); - redisConnectionOptions.KeepAlive = 180; + redisConnectionOptions.KeepAlive = 180; - Console.WriteLine("Connecting to Redis: " + connectionString); - var redis = ConnectionMultiplexer.Connect(redisConnectionOptions); + Console.WriteLine("Connecting to Redis: " + connectionString); + var redis = ConnectionMultiplexer.Connect(redisConnectionOptions); - // redis.InternalError += (o, e) => { Console.WriteLine(e.Exception); }; + // redis.InternalError += (o, e) => { Console.WriteLine(e.Exception); }; - if (redis == null) - { - Console.WriteLine("Wasn't able to connect to redis"); + if (redis == null) + { + Console.WriteLine("Wasn't able to connect to redis"); - // We weren't able to connect to redis despite 5 retries with exponential backoff - throw new ApplicationException("Wasn't able to connect to redis"); - } + // We weren't able to connect to redis despite 5 retries with exponential backoff + throw new ApplicationException("Wasn't able to connect to redis"); + } - Console.WriteLine("Successfully connected to Redis"); - return redis; + Console.WriteLine("Successfully connected to Redis"); + return redis; } private static string ReadRedisAddress(string address) { - if (!string.IsNullOrEmpty(address)) - { - return address; - } - - Console.WriteLine($"Reading redis cache address from environment variable {REDIS_ADDRESS}"); - string redis = Environment.GetEnvironmentVariable(REDIS_ADDRESS); - if (!string.IsNullOrEmpty(redis)) - { - return redis; - } + if (!string.IsNullOrEmpty(address)) + { + return address; + } + + Console.WriteLine($"Reading redis cache address from environment variable {REDIS_ADDRESS}"); + string redis = Environment.GetEnvironmentVariable(REDIS_ADDRESS); + if (!string.IsNullOrEmpty(redis)) + { + return redis; + } - return null; + return null; } - } -} +} \ No newline at end of file diff --git a/src/cartservice/cartservice.csproj b/src/cartservice/cartservice.csproj index 5b1f1ff8ebe..cc533738a68 100644 --- a/src/cartservice/cartservice.csproj +++ b/src/cartservice/cartservice.csproj @@ -2,24 +2,24 @@ Exe - net7.0 - 11.0 + net8.0 + 12.0 false - - + + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - + + + diff --git a/src/cartservice/cartstore/Bogo.cs b/src/cartservice/cartstore/Bogo.cs index 10ea46f6c63..fb58c4b1536 100644 --- a/src/cartservice/cartstore/Bogo.cs +++ b/src/cartservice/cartstore/Bogo.cs @@ -1,50 +1,49 @@ using System; using System.Collections.Generic; -namespace cartservice.cartstore +namespace cartservice.cartstore; + +public static class Bogo { - public static class Bogo - { public static void Sort(List list) { - do - { - Shuffle(list); - } while (!IsSorted(list)); + do + { + Shuffle(list); + } while (!IsSorted(list)); } private static void Shuffle(List list) { - var r = Random.Shared; - for (int i = 0; i < list.Count; i++) - { - var k = r.Next(i, list.Count); - var tmp = list[k]; - list[k] = list[i]; - list[i] = tmp; - } + var r = Random.Shared; + for (int i = 0; i < list.Count; i++) + { + var k = r.Next(i, list.Count); + var tmp = list[k]; + list[k] = list[i]; + list[i] = tmp; + } } private static bool IsSorted(List list) { - if (list is null || list.Count <= 1) - { - return true; - } - - var prevChar = list[0]; - for (int i = 1; i < list.Count; i++) - { - var currChar = list[i]; - if (prevChar > currChar) + if (list is null || list.Count <= 1) { - return false; + return true; } - prevChar = currChar; - } + var prevChar = list[0]; + for (int i = 1; i < list.Count; i++) + { + var currChar = list[i]; + if (prevChar > currChar) + { + return false; + } - return true; + prevChar = currChar; + } + + return true; } - } -} +} \ No newline at end of file diff --git a/src/cartservice/cartstore/ConfigHelper.cs b/src/cartservice/cartstore/ConfigHelper.cs index 105ccc360de..0cf7ade9289 100644 --- a/src/cartservice/cartstore/ConfigHelper.cs +++ b/src/cartservice/cartstore/ConfigHelper.cs @@ -1,21 +1,20 @@ using System; -namespace cartservice.cartstore +namespace cartservice.cartstore; + +internal static class ConfigHelper { - internal static class ConfigHelper - { public static bool GetBoolEnvVar(string envVarName, bool defaultValue) { - var varValue = Environment.GetEnvironmentVariable(envVarName) ?? string.Empty; - var result = varValue.ToLowerInvariant() switch - { - "true" or "1" or "yes" => true, - "false" or "0" or "no" => false, - _ => defaultValue - }; + var varValue = Environment.GetEnvironmentVariable(envVarName) ?? string.Empty; + var result = varValue.ToLowerInvariant() switch + { + "true" or "1" or "yes" => true, + "false" or "0" or "no" => false, + _ => defaultValue + }; - Console.WriteLine($"{nameof(ConfigHelper)}: env var {envVarName} = {result}"); - return result; + Console.WriteLine($"{nameof(ConfigHelper)}: env var {envVarName} = {result}"); + return result; } - } -} +} \ No newline at end of file diff --git a/src/cartservice/cartstore/DatabaseCache.cs b/src/cartservice/cartstore/DatabaseCache.cs index f73948dfa2b..21306a6106b 100644 --- a/src/cartservice/cartstore/DatabaseCache.cs +++ b/src/cartservice/cartstore/DatabaseCache.cs @@ -3,10 +3,10 @@ using System.Threading.Tasks; using StackExchange.Redis; -namespace cartservice.cartstore +namespace cartservice.cartstore; + +public class DatabaseCache { - public class DatabaseCache - { private static readonly bool OptimizeBlocking = ConfigHelper.GetBoolEnvVar("OPTIMIZE_BLOCKING", defaultValue: true); private readonly ConnectionMultiplexer _conn; @@ -14,32 +14,31 @@ public class DatabaseCache public DatabaseCache(ConnectionMultiplexer connection) { - _conn = connection; + _conn = connection; - var maxConcurrentDBRetrieval = OptimizeBlocking ? Environment.ProcessorCount : 1; - _pool = new Semaphore(0, maxConcurrentDBRetrieval); - _pool.Release(maxConcurrentDBRetrieval); + var maxConcurrentDBRetrieval = OptimizeBlocking ? Environment.ProcessorCount : 1; + _pool = new Semaphore(0, maxConcurrentDBRetrieval); + _pool.Release(maxConcurrentDBRetrieval); } public IDatabase ByPassBlocking() => _conn.GetDatabase(); public IDatabase Get() { - _pool.WaitOne(); - if (OptimizeBlocking) - { - _pool.Release(1); - } - else - { - Task.Run(async () => + _pool.WaitOne(); + if (OptimizeBlocking) { - await Task.Delay(Random.Shared.Next(250, 750)); _pool.Release(1); - }); - } + } + else + { + Task.Run(async () => + { + await Task.Delay(Random.Shared.Next(250, 750)); + _pool.Release(1); + }); + } - return _conn.GetDatabase(); + return _conn.GetDatabase(); } - } -} +} \ No newline at end of file diff --git a/src/cartservice/cartstore/LocalCartStore.cs b/src/cartservice/cartstore/LocalCartStore.cs index 7fd8a5ed21c..7abd1b4672e 100644 --- a/src/cartservice/cartstore/LocalCartStore.cs +++ b/src/cartservice/cartstore/LocalCartStore.cs @@ -19,30 +19,30 @@ using cartservice.interfaces; using Hipstershop; -namespace cartservice.cartstore +namespace cartservice.cartstore; + +internal class LocalCartStore : ICartStore { - internal class LocalCartStore : ICartStore - { - // Maps between user and their cart - private ConcurrentDictionary userCartItems = new ConcurrentDictionary(); - private readonly Hipstershop.Cart emptyCart = new Hipstershop.Cart(); + // Maps between user and their cart + private ConcurrentDictionary userCartItems = new ConcurrentDictionary(); + private readonly Hipstershop.Cart emptyCart = new Hipstershop.Cart(); - public Task InitializeAsync() - { - Console.WriteLine("Local Cart Store was initialized"); + public Task InitializeAsync() + { + Console.WriteLine("Local Cart Store was initialized"); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task AddItemAsync(string userId, string productId, int quantity) + public Task AddItemAsync(string userId, string productId, int quantity) + { + Console.WriteLine($"AddItemAsync called with userId={userId}, productId={productId}, quantity={quantity}"); + var newCart = new Hipstershop.Cart { - Console.WriteLine($"AddItemAsync called with userId={userId}, productId={productId}, quantity={quantity}"); - var newCart = new Hipstershop.Cart - { - UserId = userId, - Items = { new Hipstershop.CartItem { ProductId = productId, Quantity = quantity } } - }; - userCartItems.AddOrUpdate(userId, newCart, + UserId = userId, + Items = { new Hipstershop.CartItem { ProductId = productId, Quantity = quantity } } + }; + userCartItems.AddOrUpdate(userId, newCart, (k, exVal) => { // If the item exists, we update its quantity @@ -59,33 +59,32 @@ public Task AddItemAsync(string userId, string productId, int quantity) return exVal; }); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task EmptyCartAsync(string userId) - { - Console.WriteLine($"EmptyCartAsync called with userId={userId}"); - userCartItems[userId] = new Hipstershop.Cart(); + public Task EmptyCartAsync(string userId) + { + Console.WriteLine($"EmptyCartAsync called with userId={userId}"); + userCartItems[userId] = new Hipstershop.Cart(); - return Task.CompletedTask; - } + return Task.CompletedTask; + } - public Task GetCartAsync(string userId) + public Task GetCartAsync(string userId) + { + Console.WriteLine($"GetCartAsync called with userId={userId}"); + Hipstershop.Cart cart = null; + if (!userCartItems.TryGetValue(userId, out cart)) { - Console.WriteLine($"GetCartAsync called with userId={userId}"); - Hipstershop.Cart cart = null; - if (!userCartItems.TryGetValue(userId, out cart)) - { - Console.WriteLine($"No carts for user {userId}"); - return Task.FromResult(emptyCart); - } - - return Task.FromResult(cart); + Console.WriteLine($"No carts for user {userId}"); + return Task.FromResult(emptyCart); } - public bool Ping() - { - return true; - } + return Task.FromResult(cart); + } + + public bool Ping() + { + return true; } } \ No newline at end of file diff --git a/src/cartservice/cartstore/RedisCartStore.cs b/src/cartservice/cartstore/RedisCartStore.cs index 701ed272650..f4922bc0599 100644 --- a/src/cartservice/cartstore/RedisCartStore.cs +++ b/src/cartservice/cartstore/RedisCartStore.cs @@ -21,10 +21,10 @@ using Grpc.Core; using StackExchange.Redis; -namespace cartservice.cartstore +namespace cartservice.cartstore; + +public class RedisCartStore : ICartStore { - public class RedisCartStore : ICartStore - { private const string CART_FIELD_NAME = "cart"; private readonly byte[] emptyCartBytes; @@ -40,156 +40,155 @@ public class RedisCartStore : ICartStore public RedisCartStore(ConnectionMultiplexer connection) { - // Serialize empty cart into byte array. - var cart = new Hipstershop.Cart(); - emptyCartBytes = cart.ToByteArray(); + // Serialize empty cart into byte array. + var cart = new Hipstershop.Cart(); + emptyCartBytes = cart.ToByteArray(); - _random = Random.Shared; - _dbCache = new DatabaseCache(connection); + _random = Random.Shared; + _dbCache = new DatabaseCache(connection); } public Task InitializeAsync() { - return Task.CompletedTask; + return Task.CompletedTask; } public async Task AddItemAsync(string userId, string productId, int quantity) { - Console.WriteLine($"AddItemAsync called with userId={userId}, productId={productId}, quantity={quantity}"); - if (!UserId.IsValid(userId)) - { - throw new ArgumentException(nameof(userId)); - } - - try - { - var db = _dbCache.Get(); - - // Access the cart from the cache - var value = await db.HashGetAsync(userId, CART_FIELD_NAME); + Console.WriteLine($"AddItemAsync called with userId={userId}, productId={productId}, quantity={quantity}"); + if (!UserId.IsValid(userId)) + { + throw new ArgumentException(nameof(userId)); + } - Hipstershop.Cart cart; - if (value.IsNull) + try { - cart = new Hipstershop.Cart(); - cart.UserId = userId; - cart.Items.Add(new Hipstershop.CartItem { ProductId = productId, Quantity = quantity }); + var db = _dbCache.Get(); + + // Access the cart from the cache + var value = await db.HashGetAsync(userId, CART_FIELD_NAME); + + Hipstershop.Cart cart; + if (value.IsNull) + { + 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 db.HashSetAsync(userId, new[] { new HashEntry(CART_FIELD_NAME, cart.ToByteArray()) }); + + // Attempt to access "external database" some percentage of the time + await ConditionallyMockExternalResourceAccess("Cart.DbQuery.GetCart"); } - else + catch (Exception ex) { - 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; - } + throw new RpcException(new Grpc.Core.Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); } - - await db.HashSetAsync(userId, new[] { new HashEntry(CART_FIELD_NAME, cart.ToByteArray()) }); - - // Attempt to access "external database" some percentage of the time - await ConditionallyMockExternalResourceAccess("Cart.DbQuery.GetCart"); - } - catch (Exception ex) - { - throw new RpcException(new Grpc.Core.Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); - } } public async Task EmptyCartAsync(string userId) { - Console.WriteLine($"EmptyCartAsync called with userId={userId}"); - if (!UserId.IsValid(userId)) - { - throw new ArgumentException(nameof(userId)); - } - - try - { - var db = _dbCache.Get(); - - // Update the cache with empty cart for given user - await db.HashSetAsync(userId, new[] { new HashEntry(CART_FIELD_NAME, emptyCartBytes) }); - } - catch (Exception ex) - { - throw new RpcException(new Grpc.Core.Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); - } + Console.WriteLine($"EmptyCartAsync called with userId={userId}"); + if (!UserId.IsValid(userId)) + { + throw new ArgumentException(nameof(userId)); + } + + try + { + var db = _dbCache.Get(); + + // Update the cache with empty cart for given user + await db.HashSetAsync(userId, new[] { new HashEntry(CART_FIELD_NAME, emptyCartBytes) }); + } + catch (Exception ex) + { + throw new RpcException(new Grpc.Core.Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); + } } public async Task GetCartAsync(string userId) { - Console.WriteLine($"GetCartAsync called with userId={userId}"); - if (!UserId.IsValid(userId)) - { - throw new ArgumentException(nameof(userId)); - } + Console.WriteLine($"GetCartAsync called with userId={userId}"); + if (!UserId.IsValid(userId)) + { + throw new ArgumentException(nameof(userId)); + } - try - { - var db = _dbCache.Get(); + try + { + var db = _dbCache.Get(); - // Access the cart from the cache - var value = await db.HashGetAsync(userId, CART_FIELD_NAME); + // Access the cart from the cache + var value = await db.HashGetAsync(userId, CART_FIELD_NAME); - if (!value.IsNull) - { - // Attempt to access "external database" some percentage of the time. This happens after - // our redis call to represent some kind fo "cache miss" or secondary call that is not - // in the redis cache. - await ConditionallyMockExternalResourceAccess("Cart.DbQuery.GetCart"); + if (!value.IsNull) + { + // Attempt to access "external database" some percentage of the time. This happens after + // our redis call to represent some kind fo "cache miss" or secondary call that is not + // in the redis cache. + await ConditionallyMockExternalResourceAccess("Cart.DbQuery.GetCart"); - return Hipstershop.Cart.Parser.ParseFrom(value); - } + 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 Grpc.Core.Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); - } + // 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 Grpc.Core.Status(StatusCode.FailedPrecondition, $"Can't access cart storage. {ex}")); + } } public bool Ping() { - try - { - var db = _dbCache.ByPassBlocking(); - var res = db.Ping(); - return res != TimeSpan.Zero; - } - catch (Exception) - { - return false; - } + try + { + var db = _dbCache.ByPassBlocking(); + var res = db.Ping(); + return res != TimeSpan.Zero; + } + catch (Exception) + { + return false; + } } private async Task ConditionallyMockExternalResourceAccess(string operation) { - if (_random.NextDouble() >= EXTERNAL_DB_ACCESS_RATE) - { - return false; - } + if (_random.NextDouble() >= EXTERNAL_DB_ACCESS_RATE) + { + return false; + } - using var activity = ActivitySourceUtil.ActivitySource.StartActivity(operation, ActivityKind.Client); + using var activity = ActivitySourceUtil.ActivitySource.StartActivity(operation, ActivityKind.Client); - activity?.SetTag("db.system", "postgres"); - activity?.SetTag("db.type", "postgres"); - activity?.SetTag("peer.service", EXTERNAL_DB_NAME + ":98321"); + activity?.SetTag("db.system", "postgres"); + activity?.SetTag("db.type", "postgres"); + activity?.SetTag("peer.service", EXTERNAL_DB_NAME + ":98321"); - if (_random.NextDouble() < EXTERNAL_DB_ERROR_RATE) - { - activity?.SetStatus(ActivityStatusCode.Error); - } + if (_random.NextDouble() < EXTERNAL_DB_ERROR_RATE) + { + activity?.SetStatus(ActivityStatusCode.Error); + } - await Task.Delay(_random.Next(0, EXTERNAL_DB_MAX_DURATION_MILLIS)); + await Task.Delay(_random.Next(0, EXTERNAL_DB_MAX_DURATION_MILLIS)); - return true; + return true; } - } -} +} \ No newline at end of file diff --git a/src/cartservice/cartstore/UserId.cs b/src/cartservice/cartstore/UserId.cs index 487feac41ee..95906c61546 100644 --- a/src/cartservice/cartstore/UserId.cs +++ b/src/cartservice/cartstore/UserId.cs @@ -2,10 +2,10 @@ using System.Collections.Concurrent; using System.Collections.Generic; -namespace cartservice.cartstore +namespace cartservice.cartstore; + +public static class UserId { - public static class UserId - { private const int MaxCacheSize = 500; private readonly static bool FixExcessiveAllocation = ConfigHelper.GetBoolEnvVar("FIX_EXCESSIVE_ALLOCATION", defaultValue: true); @@ -19,116 +19,115 @@ public static class UserId public static bool IsValid(string userId) { - bool? result; - if (TryCachedValidation(userId, out result)) - { - return result ?? false; - } - - if (OptimizeCPU) - { - result = Guid.TryParse(userId, out _); - CacheValidation(userId, result.Value); - return result ?? false; - } - - // - // Silly user id validation - // - - var idChars = new List(userId.Length); - var tmpChars = new List(userId.Length); - foreach (var c in userId) - { - if (c == '-') + bool? result; + if (TryCachedValidation(userId, out result)) { - continue; + return result ?? false; } - tmpChars.Add(c); - const int idealSizeForBogoSort = 10; // Not too fast, not too slow. - if (tmpChars.Count == idealSizeForBogoSort) + if (OptimizeCPU) { - Bogo.Sort(tmpChars); - idChars.AddRange(tmpChars); - tmpChars.Clear(); + result = Guid.TryParse(userId, out _); + CacheValidation(userId, result.Value); + return result ?? false; } - } - Bogo.Sort(tmpChars); - idChars.AddRange(tmpChars); - tmpChars.Clear(); + // + // Silly user id validation + // - result = true; - foreach (var c in idChars) - { - if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) + var idChars = new List(userId.Length); + var tmpChars = new List(userId.Length); + foreach (var c in userId) { - continue; + if (c == '-') + { + continue; + } + + tmpChars.Add(c); + const int idealSizeForBogoSort = 10; // Not too fast, not too slow. + if (tmpChars.Count == idealSizeForBogoSort) + { + Bogo.Sort(tmpChars); + idChars.AddRange(tmpChars); + tmpChars.Clear(); + } } - result = false; - break; - } + Bogo.Sort(tmpChars); + idChars.AddRange(tmpChars); + tmpChars.Clear(); + + result = true; + foreach (var c in idChars) + { + if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) + { + continue; + } + + result = false; + break; + } - CacheValidation(userId, result ?? false); - return result ?? false; + CacheValidation(userId, result ?? false); + return result ?? false; } private static bool TryCachedValidation(string userId, out bool? result) { - var localUserId = ProcessUserId(userId); - return ValidationCache.TryGetValue(localUserId, out result); + var localUserId = ProcessUserId(userId); + return ValidationCache.TryGetValue(localUserId, out result); } private static void CacheValidation(string userId, bool result) { - lock(ValidationCacheLock) - { - if (ValidationCache.TryAdd(userId, result)) + lock(ValidationCacheLock) { - UserIdQueue.Enqueue(userId); + if (ValidationCache.TryAdd(userId, result)) + { + UserIdQueue.Enqueue(userId); + } } - } - - string expirationCandidate; - if (FixSlowLeak && ValidationCache.Count >= MaxCacheSize && UserIdQueue.TryDequeue(out expirationCandidate)) - { - ValidationCache.Remove(expirationCandidate, out var _); - return; - } - - if ((ulong)ValidationCache.Count - prevValidationCacheCount > MaxCacheSize) - { - var numOfItemsToEvictFromCache = (MaxCacheSize/100)*2; - for (int i = 0; i < numOfItemsToEvictFromCache; i++) + + string expirationCandidate; + if (FixSlowLeak && ValidationCache.Count >= MaxCacheSize && UserIdQueue.TryDequeue(out expirationCandidate)) { - if (UserIdQueue.TryDequeue(out expirationCandidate)) - { ValidationCache.Remove(expirationCandidate, out var _); - } + return; } - prevValidationCacheCount = (ulong)ValidationCache.Count + MaxCacheSize; - } + if ((ulong)ValidationCache.Count - prevValidationCacheCount > MaxCacheSize) + { + var numOfItemsToEvictFromCache = (MaxCacheSize/100)*2; + for (int i = 0; i < numOfItemsToEvictFromCache; i++) + { + if (UserIdQueue.TryDequeue(out expirationCandidate)) + { + ValidationCache.Remove(expirationCandidate, out var _); + } + } + + prevValidationCacheCount = (ulong)ValidationCache.Count + MaxCacheSize; + } } private static string ProcessUserId(string userId) { - var processedUserId = string.Empty; - if (FixExcessiveAllocation) - { - processedUserId = userId; - } - else - { - foreach (var c in userId) + var processedUserId = string.Empty; + if (FixExcessiveAllocation) + { + processedUserId = userId; + } + else { - processedUserId += c; + foreach (var c in userId) + { + processedUserId += c; + } } - } - return processedUserId; + return processedUserId; } - } -} +} \ No newline at end of file diff --git a/src/cartservice/interfaces/ICartStore.cs b/src/cartservice/interfaces/ICartStore.cs index 8c5e225828e..76a25d9b06c 100644 --- a/src/cartservice/interfaces/ICartStore.cs +++ b/src/cartservice/interfaces/ICartStore.cs @@ -14,17 +14,16 @@ using System.Threading.Tasks; -namespace cartservice.interfaces +namespace cartservice.interfaces; + +internal interface ICartStore { - internal interface ICartStore - { - Task InitializeAsync(); + Task InitializeAsync(); - Task AddItemAsync(string userId, string productId, int quantity); - Task EmptyCartAsync(string userId); + Task AddItemAsync(string userId, string productId, int quantity); + Task EmptyCartAsync(string userId); - Task GetCartAsync(string userId); + Task GetCartAsync(string userId); - bool Ping(); - } + bool Ping(); } \ No newline at end of file diff --git a/src/cartservice/tests/CartServiceTests.cs b/src/cartservice/tests/CartServiceTests.cs index ed297bde01e..e5f462625a2 100644 --- a/src/cartservice/tests/CartServiceTests.cs +++ b/src/cartservice/tests/CartServiceTests.cs @@ -19,118 +19,117 @@ using Xunit; using static Hipstershop.CartService; -namespace cartservice +namespace cartservice; + +public class E2ETests { - public class E2ETests - { - private static string serverHostName = "localhost"; - private static int port = 7070; + private static string serverHostName = "localhost"; + private static int port = 7070; - [Fact] - public async Task GetItem_NoAddItemBefore_EmptyCartReturned() - { - string userId = Guid.NewGuid().ToString(); + [Fact] + public async Task GetItem_NoAddItemBefore_EmptyCartReturned() + { + string userId = Guid.NewGuid().ToString(); - // Construct server's Uri - string targetUri = $"{serverHostName}:{port}"; + // Construct server's Uri + string targetUri = $"{serverHostName}:{port}"; - // Create a GRPC communication channel between the client and the server - var channel = new Channel(targetUri, ChannelCredentials.Insecure); + // Create a GRPC communication channel between the client and the server + var channel = new Channel(targetUri, ChannelCredentials.Insecure); - var client = new CartServiceClient(channel); + var client = new CartServiceClient(channel); - var request = new GetCartRequest - { - UserId = userId, - }; + var request = new GetCartRequest + { + UserId = userId, + }; - var cart = await client.GetCartAsync(request); - Assert.NotNull(cart); + var cart = await client.GetCartAsync(request); + Assert.NotNull(cart); - // All grpc objects implement IEquitable, so we can compare equality with by-value semantics - Assert.Equal(new Cart(), 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() - { - string userId = Guid.NewGuid().ToString(); + [Fact] + public async Task AddItem_ItemExists_Updated() + { + string userId = Guid.NewGuid().ToString(); - // Construct server's Uri - string targetUri = $"{serverHostName}:{port}"; + // Construct server's Uri + string targetUri = $"{serverHostName}:{port}"; - // Create a GRPC communication channel between the client and the server - var channel = new Channel(targetUri, ChannelCredentials.Insecure); + // Create a GRPC communication channel between the client and the server + var channel = new Channel(targetUri, ChannelCredentials.Insecure); - var client = new CartServiceClient(channel); - var request = new AddItemRequest + var client = new CartServiceClient(channel); + var request = new AddItemRequest + { + UserId = userId, + Item = new CartItem { - 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); + 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() + var getCartRequest = new GetCartRequest { - string userId = Guid.NewGuid().ToString(); + 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 }); + } - // Construct server's Uri - string targetUri = $"{serverHostName}:{port}"; + [Fact] + public async Task AddItem_New_Inserted() + { + string userId = Guid.NewGuid().ToString(); - // Create a GRPC communication channel between the client and the server - var channel = new Channel(targetUri, ChannelCredentials.Insecure); + // Construct server's Uri + string targetUri = $"{serverHostName}:{port}"; + + // Create a GRPC communication channel between the client and the server + var channel = new Channel(targetUri, ChannelCredentials.Insecure); - // Create a proxy object to work with the server - var client = new CartServiceClient(channel); + // Create a proxy object to work with the server + var client = new CartServiceClient(channel); - var request = new AddItemRequest + var request = new AddItemRequest + { + UserId = userId, + Item = new CartItem { - UserId = userId, - Item = new CartItem - { - ProductId = "1", - Quantity = 1 - } - }; + ProductId = "1", + Quantity = 1 + } + }; - await client.AddItemAsync(request); + 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); - } + 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); } -} +} \ No newline at end of file diff --git a/src/cartservice/tests/cartservice.tests.csproj b/src/cartservice/tests/cartservice.tests.csproj index d7fa2a06314..d920f8457b4 100644 --- a/src/cartservice/tests/cartservice.tests.csproj +++ b/src/cartservice/tests/cartservice.tests.csproj @@ -1,22 +1,18 @@ - net6.0 + net8.0 false - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive - - -