From 06436d30fe4165d3bd1b22e0f588f1cbc85defae Mon Sep 17 00:00:00 2001 From: WooSH Date: Sun, 14 Sep 2025 14:49:56 +0900 Subject: [PATCH 1/4] =?UTF-8?q?perf:=20=EC=B1=84=ED=8C=85=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=ED=8C=A8?= =?UTF-8?q?=ED=84=B4=20=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=98=88=EC=99=B8?= =?UTF-8?q?=20=EC=95=88=EC=A0=84=EC=84=B1=20=EA=B0=95=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task.Run 내부에 예외 처리 추가로 관찰되지 않은 태스크 예외 방지 ConfigureAwait(false) 적용으로 컨텍스트 스위칭 최적화 --- .../Services/Chat/ChatService.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/ProjectVG.Application/Services/Chat/ChatService.cs b/ProjectVG.Application/Services/Chat/ChatService.cs index bcfd2ef..57047b5 100644 --- a/ProjectVG.Application/Services/Chat/ChatService.cs +++ b/ProjectVG.Application/Services/Chat/ChatService.cs @@ -73,7 +73,7 @@ public async Task EnqueueChatRequestAsync(ChatRequestCommand LogChatRequestCommand(command); _ = Task.Run(async () => { - await ProcessChatRequestInternalAsync(preprocessContext); + await ProcessChatRequestInternalAsync(preprocessContext).ConfigureAwait(false); }); return ChatRequestResult.Accepted(command.Id.ToString(), command.UserId, command.CharacterId); @@ -109,20 +109,18 @@ private async Task ProcessChatRequestInternalAsync(ChatProcessContext context) { using var scope = _scopeFactory.CreateScope(); try { - await _llmProcessor.ProcessAsync(context); - await _ttsProcessor.ProcessAsync(context); + await _llmProcessor.ProcessAsync(context).ConfigureAwait(false); + await _ttsProcessor.ProcessAsync(context).ConfigureAwait(false); - // ChatSuccessHandler와 ChatResultProcessor를 같은 스코프에서 실행 - var successHandler = scope.ServiceProvider.GetRequiredService(); var resultProcessor = scope.ServiceProvider.GetRequiredService(); - - await successHandler.HandleAsync(context); - await resultProcessor.PersistResultsAsync(context); + + await successHandler.HandleAsync(context).ConfigureAwait(false); + await resultProcessor.PersistResultsAsync(context).ConfigureAwait(false); } catch (Exception) { var failureHandler = scope.ServiceProvider.GetRequiredService(); - await failureHandler.HandleAsync(context); + await failureHandler.HandleAsync(context).ConfigureAwait(false); } finally { LogChatProcessContext(context); From 8a7984ba892c0e0c5f22684e73799ed49b75c796 Mon Sep 17 00:00:00 2001 From: WooSH Date: Sun, 14 Sep 2025 14:54:15 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20ChatService=20=EB=B6=88?= =?UTF-8?q?=ED=95=84=EC=9A=94=ED=95=9C=20=EB=A1=9C=EA=B9=85=20=EB=A9=94?= =?UTF-8?q?=EC=84=9C=EB=93=9C=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 중복된 예외 처리 제거 및 코드 정리 LogChatRequestCommand, LogChatProcessContext 메서드 삭제 메트릭스 서비스에서 이미 로깅 제공으로 중복 방지 --- ProjectVG.Application/Services/Chat/ChatService.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/ProjectVG.Application/Services/Chat/ChatService.cs b/ProjectVG.Application/Services/Chat/ChatService.cs index 57047b5..3888a5c 100644 --- a/ProjectVG.Application/Services/Chat/ChatService.cs +++ b/ProjectVG.Application/Services/Chat/ChatService.cs @@ -70,8 +70,6 @@ public async Task EnqueueChatRequestAsync(ChatRequestCommand var preprocessContext = await PrepareChatRequestAsync(command); - LogChatRequestCommand(command); - _ = Task.Run(async () => { await ProcessChatRequestInternalAsync(preprocessContext).ConfigureAwait(false); }); @@ -123,20 +121,10 @@ private async Task ProcessChatRequestInternalAsync(ChatProcessContext context) await failureHandler.HandleAsync(context).ConfigureAwait(false); } finally { - LogChatProcessContext(context); _metricsService.EndChatMetrics(); _metricsService.LogChatMetrics(); } } - private void LogChatRequestCommand(ChatRequestCommand command) - { - _logger.LogInformation("Starting chat process: {CommandInfo}", command.ToDebugString()); - } - - private void LogChatProcessContext(ChatProcessContext context) - { - _logger.LogInformation("Chat process completed: {ContextInfo}", context.ToDebugString()); - } } } From d99f8009aa8f3ea2fd3770bd4fd5a002f173927c Mon Sep 17 00:00:00 2001 From: WooSH Date: Sun, 14 Sep 2025 17:52:50 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20ASP.NET=20Core=20=EB=B9=84?= =?UTF-8?q?=EB=8F=99=EA=B8=B0=20=EB=A9=94=EC=84=9C=EB=93=9C=EC=97=90?= =?UTF-8?q?=EC=84=9C=20ConfigureAwait(false)=20=EC=A0=9C=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 불필요한 ConfigureAwait(false) 호출 제거로 코드 가독성 개선 - WebSocketMiddleware.cs - AuthService.cs - ChatService.cs - ChatRequestValidator.cs - TokenService.cs - SqlServerUserRepository.cs ASP.NET Core에서는 SynchronizationContext가 없어 ConfigureAwait(false)의 이점이 제한적 코드 일관성 및 가독성 향상을 위해 제거 --- .../Middleware/WebSocketMiddleware.cs | 11 ++++----- .../Services/Auth/AuthService.cs | 20 ++++++++-------- .../Services/Chat/ChatService.cs | 12 +++++----- .../Chat/Validators/ChatRequestValidator.cs | 11 ++++----- ProjectVG.Infrastructure/Auth/TokenService.cs | 12 +++++----- .../User/SqlServerUserRepository.cs | 24 +++++++++---------- 6 files changed, 44 insertions(+), 46 deletions(-) diff --git a/ProjectVG.Api/Middleware/WebSocketMiddleware.cs b/ProjectVG.Api/Middleware/WebSocketMiddleware.cs index b2d95e3..33d6a51 100644 --- a/ProjectVG.Api/Middleware/WebSocketMiddleware.cs +++ b/ProjectVG.Api/Middleware/WebSocketMiddleware.cs @@ -123,7 +123,7 @@ await socket.SendAsync( new ArraySegment(welcomeMessage), WebSocketMessageType.Text, true, - cancellationTokenSource.Token).ConfigureAwait(false); + cancellationTokenSource.Token); while (socket.State == WebSocketState.Open && !cancellationTokenSource.Token.IsCancellationRequested) { @@ -131,8 +131,7 @@ await socket.SendAsync( using var ms = new MemoryStream(); do { - result = await socket.ReceiveAsync(new ArraySegment(buffer), cancellationTokenSource.Token) - .ConfigureAwait(false); + result = await socket.ReceiveAsync(new ArraySegment(buffer), cancellationTokenSource.Token); if (result.MessageType == WebSocketMessageType.Close) { _logger.LogInformation("연결 종료 요청: {UserId}", userId); @@ -160,7 +159,7 @@ await socket.SendAsync( new ArraySegment(pongMessage), WebSocketMessageType.Text, true, - cancellationTokenSource.Token).ConfigureAwait(false); + cancellationTokenSource.Token); } } } @@ -178,14 +177,14 @@ await socket.SendAsync( _logger.LogInformation("WebSocket 연결 해제: {UserId}", userId); try { - await _webSocketService.DisconnectAsync(userId).ConfigureAwait(false); + await _webSocketService.DisconnectAsync(userId); _connectionRegistry.Unregister(userId); if (socket.State == WebSocketState.Open || socket.State == WebSocketState.CloseReceived) { await socket.CloseAsync( WebSocketCloseStatus.NormalClosure, "Connection closed", - CancellationToken.None).ConfigureAwait(false); + CancellationToken.None); } } catch (Exception ex) { diff --git a/ProjectVG.Application/Services/Auth/AuthService.cs b/ProjectVG.Application/Services/Auth/AuthService.cs index a81900e..63fc38d 100644 --- a/ProjectVG.Application/Services/Auth/AuthService.cs +++ b/ProjectVG.Application/Services/Auth/AuthService.cs @@ -30,7 +30,7 @@ public async Task GuestLoginAsync(string guestId) throw new ValidationException(ErrorCode.GUEST_ID_INVALID); } - var user = await _userService.TryGetByProviderAsync("guest", guestId).ConfigureAwait(false); + var user = await _userService.TryGetByProviderAsync("guest", guestId); if (user == null) { string uuid = GenerateGuestUuid(guestId); @@ -41,23 +41,23 @@ public async Task GuestLoginAsync(string guestId) Provider: "guest" ); - user = await _userService.CreateUserAsync(createCommand).ConfigureAwait(false); + user = await _userService.CreateUserAsync(createCommand); _logger.LogInformation("새 게스트 사용자 생성됨: UserId={UserId}, GuestId={GuestId}", user.Id, guestId); } - return await FinalizeLoginAsync(user, "guest").ConfigureAwait(false); + return await FinalizeLoginAsync(user, "guest"); } private async Task FinalizeLoginAsync(UserDto user, string provider) { // 초기 크레딧 지급 - var tokenGranted = await _tokenManagementService.GrantInitialCreditsAsync(user.Id).ConfigureAwait(false); + var tokenGranted = await _tokenManagementService.GrantInitialCreditsAsync(user.Id); if (tokenGranted) { _logger.LogInformation("사용자 {UserId}에게 최초 크레딧 지급 완료", user.Id); } // 최종 JWT 토큰 발급 - var tokens = await _tokenService.GenerateTokensAsync(user.Id).ConfigureAwait(false); + var tokens = await _tokenService.GenerateTokensAsync(user.Id); return new AuthResult { Tokens = tokens, @@ -71,13 +71,13 @@ public async Task RefreshAccessTokenAsync(string? refreshToken) throw new ValidationException(ErrorCode.TOKEN_MISSING); } - var tokens = await _tokenService.RefreshAccessTokenAsync(refreshToken).ConfigureAwait(false); + var tokens = await _tokenService.RefreshAccessTokenAsync(refreshToken); if (tokens == null) { throw new ValidationException(ErrorCode.TOKEN_REFRESH_FAILED); } - var userId = await _tokenService.GetUserIdFromTokenAsync(refreshToken).ConfigureAwait(false); - var user = userId.HasValue ? await _userService.TryGetByIdAsync(userId.Value).ConfigureAwait(false) : null; + var userId = await _tokenService.GetUserIdFromTokenAsync(refreshToken); + var user = userId.HasValue ? await _userService.TryGetByIdAsync(userId.Value) : null; return new AuthResult { Tokens = tokens, @@ -91,9 +91,9 @@ public async Task LogoutAsync(string? refreshToken) throw new ValidationException(ErrorCode.TOKEN_MISSING); } - var revoked = await _tokenService.RevokeRefreshTokenAsync(refreshToken).ConfigureAwait(false); + var revoked = await _tokenService.RevokeRefreshTokenAsync(refreshToken); if (revoked) { - var userId = await _tokenService.GetUserIdFromTokenAsync(refreshToken).ConfigureAwait(false); + var userId = await _tokenService.GetUserIdFromTokenAsync(refreshToken); } return revoked; } diff --git a/ProjectVG.Application/Services/Chat/ChatService.cs b/ProjectVG.Application/Services/Chat/ChatService.cs index 3888a5c..5b4cb24 100644 --- a/ProjectVG.Application/Services/Chat/ChatService.cs +++ b/ProjectVG.Application/Services/Chat/ChatService.cs @@ -71,7 +71,7 @@ public async Task EnqueueChatRequestAsync(ChatRequestCommand var preprocessContext = await PrepareChatRequestAsync(command); _ = Task.Run(async () => { - await ProcessChatRequestInternalAsync(preprocessContext).ConfigureAwait(false); + await ProcessChatRequestInternalAsync(preprocessContext); }); return ChatRequestResult.Accepted(command.Id.ToString(), command.UserId, command.CharacterId); @@ -107,18 +107,18 @@ private async Task ProcessChatRequestInternalAsync(ChatProcessContext context) { using var scope = _scopeFactory.CreateScope(); try { - await _llmProcessor.ProcessAsync(context).ConfigureAwait(false); - await _ttsProcessor.ProcessAsync(context).ConfigureAwait(false); + await _llmProcessor.ProcessAsync(context); + await _ttsProcessor.ProcessAsync(context); var successHandler = scope.ServiceProvider.GetRequiredService(); var resultProcessor = scope.ServiceProvider.GetRequiredService(); - await successHandler.HandleAsync(context).ConfigureAwait(false); - await resultProcessor.PersistResultsAsync(context).ConfigureAwait(false); + await successHandler.HandleAsync(context); + await resultProcessor.PersistResultsAsync(context); } catch (Exception) { var failureHandler = scope.ServiceProvider.GetRequiredService(); - await failureHandler.HandleAsync(context).ConfigureAwait(false); + await failureHandler.HandleAsync(context); } finally { _metricsService.EndChatMetrics(); diff --git a/ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs b/ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs index 0c086b5..61fa5d4 100644 --- a/ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs +++ b/ProjectVG.Application/Services/Chat/Validators/ChatRequestValidator.cs @@ -35,22 +35,22 @@ public ChatRequestValidator( public async Task ValidateAsync(ChatRequestCommand command) { // 세션 검증 - 사용자 활성 세션 확인 - await ValidateUserSessionAsync(command.UserId).ConfigureAwait(false); + await ValidateUserSessionAsync(command.UserId); - var userExists = await _userService.ExistsByIdAsync(command.UserId).ConfigureAwait(false); + var userExists = await _userService.ExistsByIdAsync(command.UserId); if (!userExists) { _logger.LogWarning("사용자 ID 검증 실패: {UserId}", command.UserId); throw new NotFoundException(ErrorCode.USER_NOT_FOUND, command.UserId); } - var characterExists = await _characterService.CharacterExistsAsync(command.CharacterId).ConfigureAwait(false); + var characterExists = await _characterService.CharacterExistsAsync(command.CharacterId); if (!characterExists) { _logger.LogWarning("캐릭터 ID 검증 실패: {CharacterId}", command.CharacterId); throw new NotFoundException(ErrorCode.CHARACTER_NOT_FOUND, command.CharacterId); } // 토큰 잔액 검증 - 예상 비용으로 미리 확인 - var balance = await _tokenManagementService.GetCreditBalanceAsync(command.UserId).ConfigureAwait(false); + var balance = await _tokenManagementService.GetCreditBalanceAsync(command.UserId); var currentBalance = balance.CurrentBalance; if (currentBalance <= 0) { @@ -76,8 +76,7 @@ private async Task ValidateUserSessionAsync(Guid userId) try { // 사용자 ID를 기반으로 세션 조회 var userSessions = (await _sessionStorage - .GetSessionsByUserIdAsync(userId.ToString()) - .ConfigureAwait(false)) + .GetSessionsByUserIdAsync(userId.ToString())) .ToList(); if (userSessions.Count == 0) { diff --git a/ProjectVG.Infrastructure/Auth/TokenService.cs b/ProjectVG.Infrastructure/Auth/TokenService.cs index fa43192..1f88a78 100644 --- a/ProjectVG.Infrastructure/Auth/TokenService.cs +++ b/ProjectVG.Infrastructure/Auth/TokenService.cs @@ -24,7 +24,7 @@ public async Task GenerateTokensAsync(Guid userId) var accessTokenExpiresAt = DateTime.UtcNow.AddMinutes(15); var refreshTokenExpiresAt = DateTime.UtcNow.AddMinutes(1440); - var stored = await _refreshTokenStorage.StoreRefreshTokenAsync(refreshToken, userId, refreshTokenExpiresAt).ConfigureAwait(false); + var stored = await _refreshTokenStorage.StoreRefreshTokenAsync(refreshToken, userId, refreshTokenExpiresAt); if (!stored) { _logger.LogError("Failed to store refresh token for user {UserId}", userId); @@ -56,14 +56,14 @@ public async Task GenerateTokensAsync(Guid userId) return null; } - var isValid = await _refreshTokenStorage.IsRefreshTokenValidAsync(refreshToken).ConfigureAwait(false); + var isValid = await _refreshTokenStorage.IsRefreshTokenValidAsync(refreshToken); if (!isValid) { _logger.LogWarning("Refresh token not found in storage"); return null; } - var userId = await _refreshTokenStorage.GetUserIdFromRefreshTokenAsync(refreshToken).ConfigureAwait(false); + var userId = await _refreshTokenStorage.GetUserIdFromRefreshTokenAsync(refreshToken); if (!userId.HasValue) { _logger.LogWarning("User ID not found for refresh token"); @@ -75,7 +75,7 @@ public async Task GenerateTokensAsync(Guid userId) var accessTokenExpiresAt = DateTime.UtcNow.AddMinutes(15); // 기존 Refresh Token의 만료 시간 조회 - var refreshTokenExpiresAt = await _refreshTokenStorage.GetRefreshTokenExpiresAtAsync(refreshToken).ConfigureAwait(false); + var refreshTokenExpiresAt = await _refreshTokenStorage.GetRefreshTokenExpiresAtAsync(refreshToken); if (!refreshTokenExpiresAt.HasValue) { _logger.LogWarning("Refresh token expiration time not found"); @@ -93,7 +93,7 @@ public async Task GenerateTokensAsync(Guid userId) public async Task RevokeRefreshTokenAsync(string refreshToken) { - return await _refreshTokenStorage.RemoveRefreshTokenAsync(refreshToken).ConfigureAwait(false); + return await _refreshTokenStorage.RemoveRefreshTokenAsync(refreshToken); } public async Task ValidateRefreshTokenAsync(string refreshToken) @@ -110,7 +110,7 @@ public async Task ValidateRefreshTokenAsync(string refreshToken) return false; } - return await _refreshTokenStorage.IsRefreshTokenValidAsync(refreshToken).ConfigureAwait(false); + return await _refreshTokenStorage.IsRefreshTokenValidAsync(refreshToken); } public Task ValidateAccessTokenAsync(string accessToken) diff --git a/ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs b/ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs index 3eec249..fabbf15 100644 --- a/ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs +++ b/ProjectVG.Infrastructure/Persistence/Repositories/User/SqlServerUserRepository.cs @@ -24,37 +24,37 @@ public async Task> GetAllAsync() return await _context.Users .Where(u => u.Status == AccountStatus.Active) .OrderBy(u => u.Username) - .ToListAsync().ConfigureAwait(false); + .ToListAsync(); } public async Task GetByIdAsync(Guid id) { - return await _context.Users.FirstOrDefaultAsync(u => u.Id == id && u.Status != AccountStatus.Deleted).ConfigureAwait(false); + return await _context.Users.FirstOrDefaultAsync(u => u.Id == id && u.Status != AccountStatus.Deleted); } public async Task GetByUsernameAsync(string username) { - return await _context.Users.FirstOrDefaultAsync(u => u.Username == username && u.Status != AccountStatus.Deleted).ConfigureAwait(false); + return await _context.Users.FirstOrDefaultAsync(u => u.Username == username && u.Status != AccountStatus.Deleted); } public async Task GetByEmailAsync(string email) { - return await _context.Users.FirstOrDefaultAsync(u => u.Email == email && u.Status != AccountStatus.Deleted).ConfigureAwait(false); + return await _context.Users.FirstOrDefaultAsync(u => u.Email == email && u.Status != AccountStatus.Deleted); } public async Task GetByProviderIdAsync(string providerId) { - return await _context.Users.FirstOrDefaultAsync(u => u.ProviderId == providerId && u.Status != AccountStatus.Deleted).ConfigureAwait(false); + return await _context.Users.FirstOrDefaultAsync(u => u.ProviderId == providerId && u.Status != AccountStatus.Deleted); } public async Task GetByProviderAsync(string provider, string providerId) { - return await _context.Users.FirstOrDefaultAsync(u => u.Provider == provider && u.ProviderId == providerId && u.Status != AccountStatus.Deleted).ConfigureAwait(false); + return await _context.Users.FirstOrDefaultAsync(u => u.Provider == provider && u.ProviderId == providerId && u.Status != AccountStatus.Deleted); } public async Task GetByUIDAsync(string uid) { - return await _context.Users.FirstOrDefaultAsync(u => u.UID == uid && u.Status != AccountStatus.Deleted).ConfigureAwait(false); + return await _context.Users.FirstOrDefaultAsync(u => u.UID == uid && u.Status != AccountStatus.Deleted); } public async Task CreateAsync(User user) @@ -65,14 +65,14 @@ public async Task CreateAsync(User user) user.Status = AccountStatus.Active; _context.Users.Add(user); - await _context.SaveChangesAsync().ConfigureAwait(false); + await _context.SaveChangesAsync(); return user; } public async Task UpdateAsync(User user) { - var existingUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == user.Id && u.Status != AccountStatus.Deleted).ConfigureAwait(false); + var existingUser = await _context.Users.FirstOrDefaultAsync(u => u.Id == user.Id && u.Status != AccountStatus.Deleted); if (existingUser == null) { throw new NotFoundException(ErrorCode.USER_NOT_FOUND, "User", user.Id); } @@ -85,14 +85,14 @@ public async Task UpdateAsync(User user) existingUser.Status = user.Status; existingUser.Update(); - await _context.SaveChangesAsync().ConfigureAwait(false); + await _context.SaveChangesAsync(); return existingUser; } public async Task DeleteAsync(Guid id) { - var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == id && u.Status != AccountStatus.Deleted).ConfigureAwait(false); + var user = await _context.Users.FirstOrDefaultAsync(u => u.Id == id && u.Status != AccountStatus.Deleted); if (user == null) { throw new NotFoundException(ErrorCode.USER_NOT_FOUND, "User", id); @@ -100,7 +100,7 @@ public async Task DeleteAsync(Guid id) user.Status = AccountStatus.Deleted; user.Update(); - await _context.SaveChangesAsync().ConfigureAwait(false); + await _context.SaveChangesAsync(); } } } From a2ac848bdeeffdd852c25e58eb1738c64270f045 Mon Sep 17 00:00:00 2001 From: WooSH Date: Sun, 14 Sep 2025 23:31:49 +0900 Subject: [PATCH 4/4] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ProjectVG.Api/Services/TestClientLauncher.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ProjectVG.Api/Services/TestClientLauncher.cs b/ProjectVG.Api/Services/TestClientLauncher.cs index aaaaecb..32d9506 100644 --- a/ProjectVG.Api/Services/TestClientLauncher.cs +++ b/ProjectVG.Api/Services/TestClientLauncher.cs @@ -4,7 +4,7 @@ public class TestClientLauncher { public void Launch() { - Task.Delay(1000).ContinueWith(_ => { + _ = Task.Delay(1000).ContinueWith(_ => { try { var htmlPath = Path.Combine(Directory.GetCurrentDirectory(), "..", "test-clients", "test-client.html");