Skip to main content

Local debugging guide — C#

Last updated on April 14, 2026

This guide covers everything specific to debugging an Extend Event Handler app written in C#. For general debugging concepts — environment setup, VS Code debug workflow, log reading, and common issues — see:

Based on the Extend App template

File paths, task names, and class names in this guide refer to the AccelByte Extend Event Handler C# template. If your project uses different names, the concepts still apply — adjust paths to match your layout.


Prerequisites

See the C# language setup guide for installation requirements. In addition, grpcurl is required for simulating incoming events during local development — see Triggering events for testing in the main guide for installation and usage.


Project structure

File / ClassWhat it does
Program.csEntry point — wires together the gRPC server, Prometheus metrics, OpenTelemetry tracing, IAM login, and health checks.
Services/UserLoggedInService.csYour business logic for userLoggedIn events — receives the event and calls Entitlement.GrantEntitlement.
Services/UserThirdPartyLoggedInService.csYour business logic for userThirdPartyLoggedIn events — same pattern as UserLoggedInService.cs.
Services/Entitlement.csShared helper that calls the AGS Fulfillment API to grant an item to a user.
Classes/DefaultAccelByteServiceProvider.csReads configuration, builds the AccelByteSDK instance, and performs the initial client-credentials login.
Classes/AppSettingConfigRepository.csReads environment variables (AB_BASE_URL, AB_CLIENT_ID, AB_CLIENT_SECRET, AB_NAMESPACE, ITEM_ID_TO_GRANT) into typed properties.
Classes/DebugLoggerServerInterceptor.csgRPC interceptor — logs the method name and headers for every incoming call.
Classes/ExceptionHandlingInterceptor.csgRPC interceptor — catches unhandled exceptions and returns appropriate gRPC status codes.
Protos/iam/account/v1/account.protoProto definition for AGS IAM account events.
appsettings.jsonStatic configuration — Kestrel endpoint bindings (ports 6565 and 8080), log levels.

Port numbers (configured in appsettings.json):

PortPurpose
6565gRPC server — receives events from Kafka Connect and simulated grpcurl calls
8080HTTP server — Prometheus metrics endpoint (/metrics)

Running the service locally

From the terminal

# Export all variables from your .env file
export $(grep -v '^#' .env | xargs)

dotnet run --project src/AccelByte.PluginArch.EventHandler.Demo.Server/AccelByte.PluginArch.EventHandler.Demo.Server.csproj

From VS Code

Use Terminal → Run Task → "Run: App". This task is defined in .vscode/tasks.json and reads the .env file for you automatically.

Confirming the service is up

You should see log output like:

info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://0.0.0.0:8080
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://0.0.0.0:6565
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.

Confirm gRPC reflection (and therefore the server) is working:

grpcurl -plaintext localhost:6565 list

Expected output — a list of registered services:

accelbyte.iam.account.v1.UserAuthenticationUserLoggedInService
accelbyte.iam.account.v1.UserAuthenticationUserThirdPartyLoggedInService
grpc.health.v1.Health
grpc.reflection.v1alpha.ServerReflection

Attaching the debugger

The repository ships with a ready-to-use launch configuration in .vscode/launch.json:

{
"name": "Debug: App",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "Build: App",
"program": "${workspaceFolder}/src/AccelByte.PluginArch.EventHandler.Demo.Server/bin/Debug/net8.0/AccelByte.PluginArch.EventHandler.Demo.Server.dll",
"args": [],
"cwd": "${workspaceFolder}/src/AccelByte.PluginArch.EventHandler.Demo.Server",
"envFile": "${workspaceFolder}/.env",
"console": "integratedTerminal",
"justMyCode": true
}

Steps:

Follow the attaching the debugger steps in the generic guide, then select "Debug: App" from the dropdown.

VS Code builds the project (via the Build: App pre-launch task) and attaches the .NET debugger (coreclr). You do not need to configure the debugger separately.

Tip — justMyCode: By default justMyCode: true skips stepping into third-party library code. If you need to step into the AccelByte SDK or gRPC internals, change it to false in .vscode/launch.json.

Other IDEs — dotnet with remote attach

Start the application and attach a compatible .NET debugger (for example, JetBrains Rider or Visual Studio):

export $(grep -v '^#' .env | xargs)
dotnet run --project src/AccelByte.PluginArch.EventHandler.Demo.Server/AccelByte.PluginArch.EventHandler.Demo.Server.csproj

Then use your IDE's Attach to Process feature and select the AccelByte.PluginArch.EventHandler.Demo.Server process.


Where to put breakpoints

Most of the interesting logic happens in the service handler files. Start there.

What you want to investigateFile and location
Any login event arrivingServices/UserLoggedInService.cs — top of OnMessage
Third-party login event arrivingServices/UserThirdPartyLoggedInService.cs — top of OnMessage
Entitlement grant logicServices/Entitlement.cs — inside GrantEntitlement
Service not starting at allProgram.cs — just before Environment.Exit(1) calls
Startup credential checkClasses/DefaultAccelByteServiceProvider.cs — constructor, after Sdk.LoginClient(true)
gRPC call tracingClasses/DebugLoggerServerInterceptor.csUnaryServerHandler

Inspecting the incoming event payload

When the debugger pauses inside OnMessage, the request parameter contains the full event. Expand it in the Variables panel to see all fields the event carries — UserId, Namespace, PlatformId (for third-party events), and others defined in Protos/iam/account/v1/account.proto.

Conditional breakpoint syntax

Right-click a breakpoint → Edit Breakpoint → enter a C# expression, for example:

request.UserId == "test-user-001"

For more conditional breakpoint syntax examples, see the C# language guide.


Triggering events for testing

For event simulation options and grpcurl commands, see Triggering events for testing in the main guide.

Describe a service method

When you need to know the exact field names for a proto method, use grpcurl describe:

grpcurl -plaintext localhost:6565 \
describe accelbyte.iam.account.v1.UserAuthenticationUserLoggedInService.OnMessage

This prints the full proto definition of the method — useful when you need to know exactly what fields to include in the -d payload.


Reading logs

The service uses ASP.NET Core's Microsoft.Extensions.Logging, which writes to stdout. A single log line looks like:

info: AccelByte.PluginArch.EventHandler.Demo.Server.Services.UserLoggedInService[0]
Received UserLoggedIn event: { "userId": "test-user-001", "namespace": "mygame" }

Log levels

The default log level is configured in appsettings.json:

"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}

To see more verbose output during local development, override the level by setting the DOTNET_ENVIRONMENT=Development environment variable (already set in the "Run: App" VS Code task) or by adding overrides to appsettings.Development.json:

{
"Logging": {
"LogLevel": {
"Default": "Debug"
}
}
}

gRPC call pattern

The DebugLoggerServerInterceptor logs every incoming gRPC call:

info: AccelByte.PluginArch.EventHandler.Demo.Server.DebugLoggerServerInterceptor[0]
REQUEST /accelbyte.iam.account.v1.UserAuthenticationUserLoggedInService/OnMessage
info: AccelByte.PluginArch.EventHandler.Demo.Server.DebugLoggerServerInterceptor[0]
RESPONSE /accelbyte.iam.account.v1.UserAuthenticationUserLoggedInService/OnMessage

If only the REQUEST line appears and no RESPONSE line follows, the handler encountered an error before returning. Look for fail: or error: lines above or below.

Filtering logs

Use grep to focus on the output you care about in the terminal:

# Show only error-level lines
dotnet run ... 2>&1 | grep -E '^fail:|^crit:'

# Show only OnMessage calls
dotnet run ... 2>&1 | grep "OnMessage"

# Show lines related to a specific user
dotnet run ... 2>&1 | grep "test-user-001"

C#-specific troubleshooting

ITEM_ID_TO_GRANT not set — service exits immediately

Symptom:

fail: AccelByte.PluginArch.EventHandler.Demo.Server.Program[0]
ITEM_ID_TO_GRANT environment variable is required.

Cause: ITEM_ID_TO_GRANT is missing from the .env file.

Fix: Add ITEM_ID_TO_GRANT=<your-item-id> to .env and restart. The item ID must exist in a published store in the namespace specified by AB_NAMESPACE.


Service exits — client credential login fails

Symptom:

fail: AccelByte.PluginArch.EventHandler.Demo.Server.DefaultAccelByteServiceProvider[0]
Error unable to login using clientId and clientSecret

or an exception thrown from the DefaultAccelByteServiceProvider constructor.

Cause: AB_CLIENT_ID or AB_CLIENT_SECRET is wrong, or AB_BASE_URL points to the wrong environment.

Fix:

  1. Double-check the credentials in your .env.

  2. Confirm AB_BASE_URL is reachable:

    curl "$AB_BASE_URL/iam/v3/public/config"
  3. Make sure your OAuth client has the CLIENT_CREDENTIALS grant type enabled in the Admin Portal.


Build fails before the debugger starts

Symptom: Pressing F5 shows a build error in the terminal and the debugger never attaches.

Cause: The Build: App pre-launch task failed.

Fix:

  1. Open the Terminal → Run Task → "Build: App" task directly to see the full compiler output.
  2. Fix any compilation errors, then press F5 again.

You can also build from the terminal:

dotnet build src/plugin-arch-event-handler-grpc-server.sln

Checking for port conflicts

If you see "address already in use":

ss -tlnp | grep -E '6565|8080'

Kill the stale process and start again.


grpcurl returns "Failed to dial"

Symptom:

Failed to dial target host "localhost:6565": ...

Cause: The gRPC server either has not started yet, crashed on startup, or is listening on a different port.

Fix:

  1. Check the service logs for startup errors (fail: or crit: lines).
  2. Confirm port 6565 is open: ss -tlnp | grep 6565.
  3. Rerun grpcurl -plaintext localhost:6565 list — if it succeeds, the server is up.

Handler returns Internal but no error log appears

Symptom: grpcurl reports Code: Internal but you do not see a corresponding error line in the logs.

Cause: The exception is caught inside the AccelByte SDK before reaching the application logger, or justMyCode: true is hiding the stack frame.

Fix:

  1. Set a breakpoint in Services/Entitlement.cs inside GrantEntitlement to inspect any exception thrown by FulfillItemOp.Execute.
  2. Set justMyCode: false in .vscode/launch.json to allow stepping into SDK internals.
  3. Increase log verbosity in appsettings.json (set "Default": "Debug").

Debugger never pauses at a breakpoint

Possible causes and fixes:

CauseFix
Service started with Run: App task instead of the debuggerUse the VS Code "Debug: App" launch config (F5)
Project was built in Release modeThe launch config targets bin/Debug/net8.0/; make sure you're not building with -c Release
Another instance is already running and handling the eventCheck for port conflicts: ss -tlnp | grep 6565 and stop the stale process
justMyCode: true skips the fileCheck the file is part of the main project, not a referenced library; try setting justMyCode: false
grpcurl is targeting the wrong portConfirm port 6565 with grpcurl -plaintext localhost:6565 list

Proto changes have no effect

Symptom: You edited a file in Protos/ but the behavior of the service did not change.

Cause: The generated C# stubs have not been regenerated.

Fix: Run the proto generation step for the project (check Makefile or any proto.sh script in the repository root), then rebuild and restart the service:

dotnet build src/plugin-arch-event-handler-grpc-server.sln

AI assistance

The app template ships with a Claude agent skill at .claude/skills/debugging-guide/SKILL.md. Copy the full skill file into your own repository and activate it in your AI assistant.

For the full skill content and prompting tips, see the AI assistance section in the main guide.


References