diff --git a/samples/function-app-service-bus/dotnet/src/GreetingFunctions.cs b/samples/function-app-service-bus/dotnet/src/GreetingFunctions.cs
index bbe47f89..3a3c86b3 100644
--- a/samples/function-app-service-bus/dotnet/src/GreetingFunctions.cs
+++ b/samples/function-app-service-bus/dotnet/src/GreetingFunctions.cs
@@ -1,554 +1,554 @@
-using System.Net;
-using System.Text.Json;
-using System.Text.Json.Serialization;
-using Microsoft.Azure.Functions.Worker;
-using Microsoft.Azure.Functions.Worker.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Configuration;
-using Azure.Identity;
-using Azure.Messaging.ServiceBus;
-using Azure.Messaging.ServiceBus.Administration;
-
-namespace LocalStack.Azure.Samples;
-
-///
-/// A simple Azure Function that processes Service Bus messages and responds with a greeting.
-///
-public class HelloWorld
-{
- // Instance field for logging - keeps proper Azure Functions execution context
- private readonly ILogger _logger;
-
- // Static configuration values - initialized once per application lifetime
- private static string? _connectionString;
- private static string? _clientId;
- private static string? _fullyQualifiedNamespace;
- private static bool _hasConnectionString;
- private static bool _hasClientId;
- private static bool _hasFullyQualifiedNamespace;
- private static string? _inputQueueName;
- private static string? _outputQueueName;
- private static bool _configurationValid = false;
- private static string[]? _names;
-
- // Greeting templates used by GetGreeting to produce varied responses
- private static readonly string[] _greetingTemplates = new[]
- {
- "Hello {0}, how are you?",
- "Hi {0}, great to see you!",
- "Hey {0}, hope you're having a wonderful day!",
- "Good day {0}, welcome aboard!",
- "Greetings {0}, nice to meet you!",
- "Howdy {0}, what's going on?",
- "Welcome {0}, glad you're here!",
- "Salutations {0}, how's everything going?"
- };
-
- private static readonly Random _random = new();
-
- // Circular buffers for message history across all functions
- private const int MaxHistory = 100;
- private static readonly object _historyLock = new();
- private static readonly CircularBuffer _requesterSent = new(MaxHistory);
- private static readonly CircularBuffer _handlerReceived = new(MaxHistory);
- private static readonly CircularBuffer _handlerSent = new(MaxHistory);
- private static readonly CircularBuffer _consumerReceived = new(MaxHistory);
-
- // Static initialization - runs once per application lifetime
- private static readonly Lazy _initialized = new Lazy(() => { Initialize(); return true; });
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger factory used to create loggers for this class.
- public HelloWorld(ILoggerFactory loggerFactory)
- {
- _logger = loggerFactory.CreateLogger();
- }
-
- ///
- /// One-time initialization of Azure Storage infrastructure (queues, containers, tables).
- /// This method runs exactly once per application lifetime and stores configuration values in static fields.
- ///
- /// A task representing the asynchronous initialization operation.
- private static void Initialize()
- {
- try
- {
- // Create a temporary configuration instance for initialization
- var configBuilder = new ConfigurationBuilder()
- .AddEnvironmentVariables()
- .AddJsonFile("local.settings.json", optional: true);
- var config = configBuilder.Build();
-
- // Create a temporary logger for initialization
- using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
- var logger = loggerFactory.CreateLogger();
-
- logger.LogInformation("[Initialize] Starting one-time initialization...");
-
- // Read and store configuration values in static fields with fallback defaults
- _connectionString = config["SERVICE_BUS_CONNECTION_STRING"];
- _clientId = config["AZURE_CLIENT_ID"];
- _fullyQualifiedNamespace = config["SERVICE_BUS_CONNECTION_STRING:fullyQualifiedNamespace"];
- _inputQueueName = config["INPUT_QUEUE_NAME"] ?? "input";
- _outputQueueName = config["OUTPUT_QUEUE_NAME"] ?? "output";
- _names = config["NAMES"]?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
-
- _hasConnectionString = !string.IsNullOrWhiteSpace(_connectionString);
- _hasClientId = !string.IsNullOrWhiteSpace(_clientId);
- _hasFullyQualifiedNamespace = !string.IsNullOrWhiteSpace(_fullyQualifiedNamespace);
-
- // Check if names ae configured. If not use, use default names
- if (_names == null || _names.Length == 0)
- {
- logger.LogWarning("[Initialize] NAMES configuration is missing or empty. Using default names.");
- _names = new[] { "Alice", "Paolo", "Leo", "Mia" };
- }
-
- // Validate configuration and set the flag
- _configurationValid = ValidateConfigurationValues(logger);
- }
- catch (Exception ex)
- {
- // Log error but don't throw - let functions continue to work even if initialization fails
- Console.WriteLine("[Initialize] Initialization failed: {0}", ex.Message);
- _configurationValid = false;
- }
- }
-
- ///
- /// Validates that all required configuration values are present and not empty.
- /// With default values in place, only the connection string is mandatory.
- ///
- /// Logger for reporting validation errors.
- /// True if all configuration values are valid, false otherwise.
- private static bool ValidateConfigurationValues(ILogger logger)
- {
- bool isValid = true;
-
- // Requirement: Must have (ID AND Namespace) OR (Connection String)
- if (!(_hasClientId && _hasFullyQualifiedNamespace) && !_hasConnectionString)
- {
- logger.LogError("[ValidateConfigurationValues] Incomplete configuration. You must provide BOTH Client ID and Namespace, OR a Connection String.");
- isValid = false;
- }
-
- // Additional Safety: If they provided a partial Identity, catch it!
- if (_hasClientId != _hasFullyQualifiedNamespace && !_hasConnectionString)
- {
- logger.LogError("[ValidateConfigurationValues] Partial Identity detected. Both Client ID and Namespace are required.");
- isValid = false;
- }
-
- // Log the configuration values being used (helpful for debugging)
- if (isValid)
- {
- logger.LogInformation("[ValidateConfigurationValues] Configuration loaded successfully:");
- logger.LogInformation(" - Input Queue: {inputQueue}", _inputQueueName);
- logger.LogInformation(" - Output Queue: {outputQueue}", _outputQueueName);
- logger.LogInformation(" - Names: {names}", string.Join(", ", _names != null ? _names : Array.Empty()));
- }
-
- return isValid;
- }
-
- ///
- /// Checks if configuration values have been successfully loaded and validated.
- /// This method provides a fast runtime check without re-reading configuration.
- /// With default values, this primarily checks if the connection string is available.
- ///
- /// True if configuration is valid and available, false otherwise.
- private static bool IsConfigurationValid()
- {
- // Valid if we have a connection string OR (client ID + fully qualified namespace)
- return _configurationValid && (_hasConnectionString || (_hasClientId && _hasFullyQualifiedNamespace));
- }
-
- ///
- /// Processes a Service Bus message by reading, validating, and responding to the input message.
- ///
- /// The received Service Bus message containing the request payload as JSON.
- /// Actions for managing the Service Bus message lifecycle (e.g., completion).
- ///
- /// A JSON-formatted response message containing a greeting and the current date, or null if the input is invalid.
- ///
- [Function("GreetingHandler")]
- [ServiceBusOutput("%OUTPUT_QUEUE_NAME%", Connection = "SERVICE_BUS_CONNECTION_STRING")]
- public async Task GreetingHandlerAsync(
- [ServiceBusTrigger("%INPUT_QUEUE_NAME%", Connection = "SERVICE_BUS_CONNECTION_STRING", AutoCompleteMessages = false)] ServiceBusReceivedMessage message,
- ServiceBusMessageActions messageActions)
- {
- // Log the incoming message details
- _logger.LogInformation("[GreetingHandler] Message ID: {id}", message.MessageId);
- _logger.LogInformation("[GreetingHandler] Message Body: {body}", message.Body);
- _logger.LogInformation("[GreetingHandler] Message Content-Type: {contentType}", message.ContentType);
-
- // Read the message body as a byte array
- byte[] bodyBytes = message.Body.ToArray();
-
- // Check that the bodyBytes is not null or empty
- if (bodyBytes == null || bodyBytes.Length == 0)
- {
- _logger.LogError("[GreetingHandler] Received message [{messageId}] body is empty or null.", message.MessageId);
- return null;
- }
- // Convert the byte array to a string
- string json = System.Text.Encoding.UTF8.GetString(bodyBytes);
-
- // Check that the JSON is not null or empty
- if (string.IsNullOrEmpty(json))
- {
- _logger.LogError("[GreetingHandler] Received message [{messageId}] body is empty or invalid.", message.MessageId);
- return null;
- }
-
- // Deserialize the JSON into a RequestMessage object
- RequestMessage? requestMessage = JsonSerializer.Deserialize(json);
-
- // Check that the request message is not null or empty
- if (requestMessage == null || string.IsNullOrWhiteSpace(requestMessage?.Name))
- {
- _logger.LogError("[GreetingHandler] Received request message [{messageId}] body is empty or invalid.", message.MessageId);
- return null;
- }
-
- _logger.LogInformation("[GreetingHandler] Processing request for name: {name}", requestMessage.Name);
-
- // Record received name in history
- lock (_historyLock)
- {
- _handlerReceived.Add(requestMessage.Name);
- }
-
- // Create the response message
- var greetingText = GetGreeting(requestMessage.Name);
- var outputObj = new ResponseMessage
- {
- Date = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
- Text = greetingText
- };
- var outputMessage = JsonSerializer.Serialize(outputObj);
-
- // Complete the message after processing
- await messageActions.CompleteMessageAsync(message);
-
- // Log the successful processing of the message
- _logger.LogInformation("[GreetingHandler] Processed message [{messageId}] successfully: {greetingText}", message.MessageId, greetingText);
-
- // Return the response message
- return outputMessage;
- }
-
- ///
- /// Timer-triggered function that sends a greeting request message to the input queue.
- ///
- /// Timer metadata containing schedule status and next occurrence information.
- [Function("GreetingRequester")]
- [FixedDelayRetry(5, "00:00:10")]
- public async Task GreetingRequesterAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOnStartup = true)] TimerInfo timerInfo)
- {
- // Log the start of the function execution
- _logger.LogInformation("[GreetingRequester] Timer trigger function started.");
-
- // Ensure one-time initialization has run
- _ = _initialized.Value;
-
- // Fast configuration validation using pre-loaded static values
- if (!IsConfigurationValid())
- {
- _logger.LogError("[GreetingRequester] Configuration is invalid or not loaded. Aborting function execution.");
- return;
- }
-
- if (_names == null || _names.Length == 0)
- {
- _logger.LogError("[GreetingRequester] Names are not configured. Aborting function execution.");
- return;
- }
-
- try
- {
- // Create Service Bus client
- _logger.LogInformation("[GreetingRequester] Creating Service Bus client for sending messages...");
- await using var client = _hasClientId && _hasFullyQualifiedNamespace
- ? new ServiceBusClient(_fullyQualifiedNamespace, new DefaultAzureCredential())
- : new ServiceBusClient(_connectionString);
-
- // Create message sender for the input queue
- _logger.LogInformation("[GreetingRequester] Creating sender for input queue '{inputQueue}'", _inputQueueName);
- await using var sender = client.CreateSender(_inputQueueName);
-
- // Create request message with randomly selected name
- var random = new Random();
- var selectedName = _names[random.Next(_names.Length)];
- var requestMessage = new RequestMessage { Name = selectedName };
- var messageBody = JsonSerializer.Serialize(requestMessage);
-
- // Create and send Service Bus message
- var serviceBusMessage = new ServiceBusMessage(messageBody)
- {
- ContentType = "application/json"
- };
-
- _logger.LogInformation("[GreetingRequester] Sending message to input queue '{inputQueue}'...", _inputQueueName);
- await sender.SendMessageAsync(serviceBusMessage);
- _logger.LogInformation("[GreetingRequester] Successfully sent message to input queue '{inputQueue}' with name: {Name}", _inputQueueName, selectedName);
-
- // Record sent name in history
- lock (_historyLock)
- {
- _requesterSent.Add(selectedName);
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "[GreetingRequester] Failed to send message to input queue '{inputQueue}'", _inputQueueName);
- return;
- }
-
- // Log the next scheduled timer occurrence
- _logger.LogInformation("[GreetingRequester] Function Ran. Next timer schedule = {nextSchedule}", timerInfo.ScheduleStatus?.Next);
- }
-
- ///
- /// Timer-triggered function that receives and processes greeting response messages from the output queue.
- ///
- /// Timer metadata containing schedule status and next occurrence information.
- [Function("GreetingConsumer")]
- [FixedDelayRetry(5, "00:00:10")]
- public async Task GreetingConsumerAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOnStartup = true)] TimerInfo timerInfo)
- {
- // Log the start of the function execution
- _logger.LogInformation("[GreetingConsumer] Timer trigger function started.");
-
- // Ensure one-time initialization has run
- _ = _initialized.Value;
-
- // Fast configuration validation using pre-loaded static values
- if (!IsConfigurationValid())
- {
- _logger.LogError("[GreetingConsumer] Configuration is invalid or not loaded. Aborting function execution.");
- return;
- }
-
- try
- {
- // Create Service Bus client for receiving messages from the output queue
- _logger.LogInformation("[GreetingConsumer] Creating Service Bus client for receiving messages...");
- await using var client = _hasClientId && _hasFullyQualifiedNamespace
- ? new ServiceBusClient(_fullyQualifiedNamespace, new DefaultAzureCredential())
- : new ServiceBusClient(_connectionString);
- var receiver = client.CreateReceiver(_outputQueueName);
-
- _logger.LogInformation("[GreetingConsumer] Starting to receive messages from output queue '{outputQueue}'", _outputQueueName);
-
- // Loop to receive messages (with timeout to prevent infinite waiting)
- var timeout = TimeSpan.FromSeconds(30);
- var startTime = DateTime.UtcNow;
-
- try
- {
- while (DateTime.UtcNow - startTime < timeout)
- {
- try
- {
- // Receive message with a short timeout
- var receivedMessage = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(5));
-
- if (receivedMessage == null)
- {
- _logger.LogInformation("[GreetingConsumer] No more messages available in output queue '{outputQueue}'", _outputQueueName);
- break;
- }
-
- // Convert message body to string
- var messageBody = receivedMessage.Body.ToString();
-
- try
- {
- // Attempt to deserialize to ResponseMessage
- var responseMessage = JsonSerializer.Deserialize(messageBody);
-
- if (responseMessage != null)
- {
- _logger.LogInformation("[GreetingConsumer] Successfully received and deserialized message from output queue. Date: {Date}, Text: {Text}",
- responseMessage.Date, responseMessage.Text);
-
- // Complete the message after successful processing
- await receiver.CompleteMessageAsync(receivedMessage);
-
- // Record received greeting in history
- lock (_historyLock)
- {
- _consumerReceived.Add(responseMessage.Text);
- }
- }
- else
- {
- _logger.LogWarning("[GreetingConsumer] Received message could not be deserialized to ResponseMessage (null result)");
- await receiver.DeadLetterMessageAsync(receivedMessage, "DeserializationFailed", "Message deserialized to null");
- }
- }
- catch (JsonException jsonEx)
- {
- _logger.LogError(jsonEx, "[GreetingConsumer] Failed to deserialize message from output queue. Message body: {messageBody}", messageBody);
- await receiver.DeadLetterMessageAsync(receivedMessage, "DeserializationFailed", jsonEx.Message);
- }
- }
- catch (Exception messageEx)
- {
- _logger.LogError(messageEx, "[GreetingConsumer] Error occurred while receiving message from output queue '{outputQueue}'", _outputQueueName);
- // Continue the loop to try receiving more messages
- }
- }
- }
- finally
- {
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
- try
- {
- await receiver.CloseAsync(cts.Token);
- }
- catch
- { /* timeout or error on close */ }
- try
- {
- await client.DisposeAsync();
- }
- catch
- { /* benign */
- }
- }
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "[GreetingConsumer] Failed to receive messages from output queue '{outputQueue}'", _outputQueueName);
- }
-
- // Log the next scheduled timer occurrence
- _logger.LogInformation("[GreetingConsumer] Function Ran. Next timer schedule = {nextSchedule}", timerInfo.ScheduleStatus?.Next);
- }
-
- ///
- /// Selects a random greeting template and formats it with the given name.
- /// The generated greeting is also stored in a circular buffer for later retrieval.
- ///
- /// The name to include in the greeting.
- /// A randomly chosen greeting string addressed to the specified name.
- private static string GetGreeting(string name)
- {
- var template = _greetingTemplates[_random.Next(_greetingTemplates.Length)];
- var greeting = string.Format(template, name);
-
- lock (_historyLock)
- {
- _handlerSent.Add(greeting);
- }
-
- return greeting;
- }
-
- ///
- /// HTTP-triggered function that returns the most recent greetings from the circular buffer.
- /// Greetings are returned in reverse chronological order (newest first).
- ///
- /// The incoming HTTP request.
- /// The number of greetings to return (default: 20, max: 100).
- /// An HTTP response containing a JSON array of recent greetings.
- [Function("GetGreetings")]
- public async Task GetGreetingsAsync(
- [HttpTrigger(AuthorizationLevel.Function, "get", Route = "greetings")] HttpRequestData request,
- int count = 20)
- {
- _logger.LogInformation("[GetGreetings] Retrieving last {count} entries.", count);
-
- // Clamp count to valid range
- if (count < 1) count = 1;
- if (count > MaxHistory) count = MaxHistory;
-
- object history;
- lock (_historyLock)
- {
- history = new
- {
- requester = new
- {
- sent = _requesterSent.ToArray(count)
- },
- handler = new
- {
- received = _handlerReceived.ToArray(count),
- sent = _handlerSent.ToArray(count)
- },
- consumer = new
- {
- received = _consumerReceived.ToArray(count)
- }
- };
- }
-
- var response = request.CreateResponse(HttpStatusCode.OK);
- response.Headers.Add("Content-Type", "application/json");
- await response.WriteStringAsync(JsonSerializer.Serialize(history));
- return response;
- }
-
- private sealed class CircularBuffer
- {
- private readonly string[] _items;
- private int _index;
- private int _count;
-
- public CircularBuffer(int capacity) => _items = new string[capacity];
-
- public void Add(string item)
- {
- _items[_index] = item;
- _index = (_index + 1) % _items.Length;
- if (_count < _items.Length) _count++;
- }
-
- public string[] ToArray(int count)
- {
- var available = Math.Min(count, _count);
- var result = new string[available];
- for (int i = 0; i < available; i++)
- {
- var idx = (_index - available + i + _items.Length) % _items.Length;
- result[i] = _items[idx];
- }
- return result;
- }
- }
-}
-
-///
-/// Represents the input payload for greeting requests.
-///
-public class RequestMessage
-{
- ///
- /// Gets or sets the name to greet.
- ///
- [JsonPropertyName("name")]
- public required string Name { get; set; }
-}
-
-///
-/// Represents the response payload for greeting requests.
-///
-public class ResponseMessage
-{
- ///
- /// Gets or sets the date of the response message.
- ///
- [JsonPropertyName("date")]
- public required string Date { get; set; }
-
- ///
- /// Gets or sets the text of the response message.
- ///
- [JsonPropertyName("text")]
- public required string Text { get; set; }
+using System.Net;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Configuration;
+using Azure.Identity;
+using Azure.Messaging.ServiceBus;
+using Azure.Messaging.ServiceBus.Administration;
+
+namespace LocalStack.Azure.Samples;
+
+///
+/// A simple Azure Function that processes Service Bus messages and responds with a greeting.
+///
+public class HelloWorld
+{
+ // Instance field for logging - keeps proper Azure Functions execution context
+ private readonly ILogger _logger;
+
+ // Static configuration values - initialized once per application lifetime
+ private static string? _connectionString;
+ private static string? _clientId;
+ private static string? _fullyQualifiedNamespace;
+ private static bool _hasConnectionString;
+ private static bool _hasClientId;
+ private static bool _hasFullyQualifiedNamespace;
+ private static string? _inputQueueName;
+ private static string? _outputQueueName;
+ private static bool _configurationValid = false;
+ private static string[]? _names;
+
+ // Greeting templates used by GetGreeting to produce varied responses
+ private static readonly string[] _greetingTemplates = new[]
+ {
+ "Hello {0}, how are you?",
+ "Hi {0}, great to see you!",
+ "Hey {0}, hope you're having a wonderful day!",
+ "Good day {0}, welcome aboard!",
+ "Greetings {0}, nice to meet you!",
+ "Howdy {0}, what's going on?",
+ "Welcome {0}, glad you're here!",
+ "Salutations {0}, how's everything going?"
+ };
+
+ private static readonly Random _random = new();
+
+ // Circular buffers for message history across all functions
+ private const int MaxHistory = 100;
+ private static readonly object _historyLock = new();
+ private static readonly CircularBuffer _requesterSent = new(MaxHistory);
+ private static readonly CircularBuffer _handlerReceived = new(MaxHistory);
+ private static readonly CircularBuffer _handlerSent = new(MaxHistory);
+ private static readonly CircularBuffer _consumerReceived = new(MaxHistory);
+
+ // Static initialization - runs once per application lifetime
+ private static readonly Lazy _initialized = new Lazy(() => { Initialize(); return true; });
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger factory used to create loggers for this class.
+ public HelloWorld(ILoggerFactory loggerFactory)
+ {
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ ///
+ /// One-time initialization of Azure Storage infrastructure (queues, containers, tables).
+ /// This method runs exactly once per application lifetime and stores configuration values in static fields.
+ ///
+ /// A task representing the asynchronous initialization operation.
+ private static void Initialize()
+ {
+ try
+ {
+ // Create a temporary configuration instance for initialization
+ var configBuilder = new ConfigurationBuilder()
+ .AddEnvironmentVariables()
+ .AddJsonFile("local.settings.json", optional: true);
+ var config = configBuilder.Build();
+
+ // Create a temporary logger for initialization
+ using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
+ var logger = loggerFactory.CreateLogger();
+
+ logger.LogInformation("[Initialize] Starting one-time initialization...");
+
+ // Read and store configuration values in static fields with fallback defaults
+ _connectionString = config["SERVICE_BUS_CONNECTION_STRING"];
+ _clientId = config["AZURE_CLIENT_ID"];
+ _fullyQualifiedNamespace = config["SERVICE_BUS_CONNECTION_STRING:fullyQualifiedNamespace"];
+ _inputQueueName = config["INPUT_QUEUE_NAME"] ?? "input";
+ _outputQueueName = config["OUTPUT_QUEUE_NAME"] ?? "output";
+ _names = config["NAMES"]?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ _hasConnectionString = !string.IsNullOrWhiteSpace(_connectionString);
+ _hasClientId = !string.IsNullOrWhiteSpace(_clientId);
+ _hasFullyQualifiedNamespace = !string.IsNullOrWhiteSpace(_fullyQualifiedNamespace);
+
+ // Check if names ae configured. If not use, use default names
+ if (_names == null || _names.Length == 0)
+ {
+ logger.LogWarning("[Initialize] NAMES configuration is missing or empty. Using default names.");
+ _names = new[] { "Alice", "Paolo", "Leo", "Mia" };
+ }
+
+ // Validate configuration and set the flag
+ _configurationValid = ValidateConfigurationValues(logger);
+ }
+ catch (Exception ex)
+ {
+ // Log error but don't throw - let functions continue to work even if initialization fails
+ Console.WriteLine("[Initialize] Initialization failed: {0}", ex.Message);
+ _configurationValid = false;
+ }
+ }
+
+ ///
+ /// Validates that all required configuration values are present and not empty.
+ /// With default values in place, only the connection string is mandatory.
+ ///
+ /// Logger for reporting validation errors.
+ /// True if all configuration values are valid, false otherwise.
+ private static bool ValidateConfigurationValues(ILogger logger)
+ {
+ bool isValid = true;
+
+ // Requirement: Must have (ID AND Namespace) OR (Connection String)
+ if (!(_hasClientId && _hasFullyQualifiedNamespace) && !_hasConnectionString)
+ {
+ logger.LogError("[ValidateConfigurationValues] Incomplete configuration. You must provide BOTH Client ID and Namespace, OR a Connection String.");
+ isValid = false;
+ }
+
+ // Additional Safety: If they provided a partial Identity, catch it!
+ if (_hasClientId != _hasFullyQualifiedNamespace && !_hasConnectionString)
+ {
+ logger.LogError("[ValidateConfigurationValues] Partial Identity detected. Both Client ID and Namespace are required.");
+ isValid = false;
+ }
+
+ // Log the configuration values being used (helpful for debugging)
+ if (isValid)
+ {
+ logger.LogInformation("[ValidateConfigurationValues] Configuration loaded successfully:");
+ logger.LogInformation(" - Input Queue: {inputQueue}", _inputQueueName);
+ logger.LogInformation(" - Output Queue: {outputQueue}", _outputQueueName);
+ logger.LogInformation(" - Names: {names}", string.Join(", ", _names != null ? _names : Array.Empty()));
+ }
+
+ return isValid;
+ }
+
+ ///
+ /// Checks if configuration values have been successfully loaded and validated.
+ /// This method provides a fast runtime check without re-reading configuration.
+ /// With default values, this primarily checks if the connection string is available.
+ ///
+ /// True if configuration is valid and available, false otherwise.
+ private static bool IsConfigurationValid()
+ {
+ // Valid if we have a connection string OR (client ID + fully qualified namespace)
+ return _configurationValid && (_hasConnectionString || (_hasClientId && _hasFullyQualifiedNamespace));
+ }
+
+ ///
+ /// Processes a Service Bus message by reading, validating, and responding to the input message.
+ ///
+ /// The received Service Bus message containing the request payload as JSON.
+ /// Actions for managing the Service Bus message lifecycle (e.g., completion).
+ ///
+ /// A JSON-formatted response message containing a greeting and the current date, or null if the input is invalid.
+ ///
+ [Function("GreetingHandler")]
+ [ServiceBusOutput("%OUTPUT_QUEUE_NAME%", Connection = "SERVICE_BUS_CONNECTION_STRING")]
+ public async Task GreetingHandlerAsync(
+ [ServiceBusTrigger("%INPUT_QUEUE_NAME%", Connection = "SERVICE_BUS_CONNECTION_STRING", AutoCompleteMessages = false)] ServiceBusReceivedMessage message,
+ ServiceBusMessageActions messageActions)
+ {
+ // Log the incoming message details
+ _logger.LogInformation("[GreetingHandler] Message ID: {id}", message.MessageId);
+ _logger.LogInformation("[GreetingHandler] Message Body: {body}", message.Body);
+ _logger.LogInformation("[GreetingHandler] Message Content-Type: {contentType}", message.ContentType);
+
+ // Read the message body as a byte array
+ byte[] bodyBytes = message.Body.ToArray();
+
+ // Check that the bodyBytes is not null or empty
+ if (bodyBytes == null || bodyBytes.Length == 0)
+ {
+ _logger.LogError("[GreetingHandler] Received message [{messageId}] body is empty or null.", message.MessageId);
+ return null;
+ }
+ // Convert the byte array to a string
+ string json = System.Text.Encoding.UTF8.GetString(bodyBytes);
+
+ // Check that the JSON is not null or empty
+ if (string.IsNullOrEmpty(json))
+ {
+ _logger.LogError("[GreetingHandler] Received message [{messageId}] body is empty or invalid.", message.MessageId);
+ return null;
+ }
+
+ // Deserialize the JSON into a RequestMessage object
+ RequestMessage? requestMessage = JsonSerializer.Deserialize(json);
+
+ // Check that the request message is not null or empty
+ if (requestMessage == null || string.IsNullOrWhiteSpace(requestMessage?.Name))
+ {
+ _logger.LogError("[GreetingHandler] Received request message [{messageId}] body is empty or invalid.", message.MessageId);
+ return null;
+ }
+
+ _logger.LogInformation("[GreetingHandler] Processing request for name: {name}", requestMessage.Name);
+
+ // Record received name in history
+ lock (_historyLock)
+ {
+ _handlerReceived.Add(requestMessage.Name);
+ }
+
+ // Create the response message
+ var greetingText = GetGreeting(requestMessage.Name);
+ var outputObj = new ResponseMessage
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
+ Text = greetingText
+ };
+ var outputMessage = JsonSerializer.Serialize(outputObj);
+
+ // Complete the message after processing
+ await messageActions.CompleteMessageAsync(message);
+
+ // Log the successful processing of the message
+ _logger.LogInformation("[GreetingHandler] Processed message [{messageId}] successfully: {greetingText}", message.MessageId, greetingText);
+
+ // Return the response message
+ return outputMessage;
+ }
+
+ ///
+ /// Timer-triggered function that sends a greeting request message to the input queue.
+ ///
+ /// Timer metadata containing schedule status and next occurrence information.
+ [Function("GreetingRequester")]
+ [FixedDelayRetry(5, "00:00:10")]
+ public async Task GreetingRequesterAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOnStartup = true)] TimerInfo timerInfo)
+ {
+ // Log the start of the function execution
+ _logger.LogInformation("[GreetingRequester] Timer trigger function started.");
+
+ // Ensure one-time initialization has run
+ _ = _initialized.Value;
+
+ // Fast configuration validation using pre-loaded static values
+ if (!IsConfigurationValid())
+ {
+ _logger.LogError("[GreetingRequester] Configuration is invalid or not loaded. Aborting function execution.");
+ return;
+ }
+
+ if (_names == null || _names.Length == 0)
+ {
+ _logger.LogError("[GreetingRequester] Names are not configured. Aborting function execution.");
+ return;
+ }
+
+ try
+ {
+ // Create Service Bus client
+ _logger.LogInformation("[GreetingRequester] Creating Service Bus client for sending messages...");
+ await using var client = _hasClientId && _hasFullyQualifiedNamespace
+ ? new ServiceBusClient(_fullyQualifiedNamespace, new DefaultAzureCredential())
+ : new ServiceBusClient(_connectionString);
+
+ // Create message sender for the input queue
+ _logger.LogInformation("[GreetingRequester] Creating sender for input queue '{inputQueue}'", _inputQueueName);
+ await using var sender = client.CreateSender(_inputQueueName);
+
+ // Create request message with randomly selected name
+ var random = new Random();
+ var selectedName = _names[random.Next(_names.Length)];
+ var requestMessage = new RequestMessage { Name = selectedName };
+ var messageBody = JsonSerializer.Serialize(requestMessage);
+
+ // Create and send Service Bus message
+ var serviceBusMessage = new ServiceBusMessage(messageBody)
+ {
+ ContentType = "application/json"
+ };
+
+ _logger.LogInformation("[GreetingRequester] Sending message to input queue '{inputQueue}'...", _inputQueueName);
+ await sender.SendMessageAsync(serviceBusMessage);
+ _logger.LogInformation("[GreetingRequester] Successfully sent message to input queue '{inputQueue}' with name: {Name}", _inputQueueName, selectedName);
+
+ // Record sent name in history
+ lock (_historyLock)
+ {
+ _requesterSent.Add(selectedName);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "[GreetingRequester] Failed to send message to input queue '{inputQueue}'", _inputQueueName);
+ return;
+ }
+
+ // Log the next scheduled timer occurrence
+ _logger.LogInformation("[GreetingRequester] Function Ran. Next timer schedule = {nextSchedule}", timerInfo.ScheduleStatus?.Next);
+ }
+
+ ///
+ /// Timer-triggered function that receives and processes greeting response messages from the output queue.
+ ///
+ /// Timer metadata containing schedule status and next occurrence information.
+ [Function("GreetingConsumer")]
+ [FixedDelayRetry(5, "00:00:10")]
+ public async Task GreetingConsumerAsync([TimerTrigger("%TIMER_SCHEDULE%", RunOnStartup = true)] TimerInfo timerInfo)
+ {
+ // Log the start of the function execution
+ _logger.LogInformation("[GreetingConsumer] Timer trigger function started.");
+
+ // Ensure one-time initialization has run
+ _ = _initialized.Value;
+
+ // Fast configuration validation using pre-loaded static values
+ if (!IsConfigurationValid())
+ {
+ _logger.LogError("[GreetingConsumer] Configuration is invalid or not loaded. Aborting function execution.");
+ return;
+ }
+
+ try
+ {
+ // Create Service Bus client for receiving messages from the output queue
+ _logger.LogInformation("[GreetingConsumer] Creating Service Bus client for receiving messages...");
+ await using var client = _hasClientId && _hasFullyQualifiedNamespace
+ ? new ServiceBusClient(_fullyQualifiedNamespace, new DefaultAzureCredential())
+ : new ServiceBusClient(_connectionString);
+ var receiver = client.CreateReceiver(_outputQueueName);
+
+ _logger.LogInformation("[GreetingConsumer] Starting to receive messages from output queue '{outputQueue}'", _outputQueueName);
+
+ // Loop to receive messages (with timeout to prevent infinite waiting)
+ var timeout = TimeSpan.FromSeconds(30);
+ var startTime = DateTime.UtcNow;
+
+ try
+ {
+ while (DateTime.UtcNow - startTime < timeout)
+ {
+ try
+ {
+ // Receive message with a short timeout
+ var receivedMessage = await receiver.ReceiveMessageAsync(TimeSpan.FromSeconds(5));
+
+ if (receivedMessage == null)
+ {
+ _logger.LogInformation("[GreetingConsumer] No more messages available in output queue '{outputQueue}'", _outputQueueName);
+ break;
+ }
+
+ // Convert message body to string
+ var messageBody = receivedMessage.Body.ToString();
+
+ try
+ {
+ // Attempt to deserialize to ResponseMessage
+ var responseMessage = JsonSerializer.Deserialize(messageBody);
+
+ if (responseMessage != null)
+ {
+ _logger.LogInformation("[GreetingConsumer] Successfully received and deserialized message from output queue. Date: {Date}, Text: {Text}",
+ responseMessage.Date, responseMessage.Text);
+
+ // Complete the message after successful processing
+ await receiver.CompleteMessageAsync(receivedMessage);
+
+ // Record received greeting in history
+ lock (_historyLock)
+ {
+ _consumerReceived.Add(responseMessage.Text);
+ }
+ }
+ else
+ {
+ _logger.LogWarning("[GreetingConsumer] Received message could not be deserialized to ResponseMessage (null result)");
+ await receiver.DeadLetterMessageAsync(receivedMessage, "DeserializationFailed", "Message deserialized to null");
+ }
+ }
+ catch (JsonException jsonEx)
+ {
+ _logger.LogError(jsonEx, "[GreetingConsumer] Failed to deserialize message from output queue. Message body: {messageBody}", messageBody);
+ await receiver.DeadLetterMessageAsync(receivedMessage, "DeserializationFailed", jsonEx.Message);
+ }
+ }
+ catch (Exception messageEx)
+ {
+ _logger.LogError(messageEx, "[GreetingConsumer] Error occurred while receiving message from output queue '{outputQueue}'", _outputQueueName);
+ // Continue the loop to try receiving more messages
+ }
+ }
+ }
+ finally
+ {
+ using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
+ try
+ {
+ await receiver.CloseAsync(cts.Token);
+ }
+ catch
+ { /* timeout or error on close */ }
+ try
+ {
+ await client.DisposeAsync();
+ }
+ catch
+ { /* benign */
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "[GreetingConsumer] Failed to receive messages from output queue '{outputQueue}'", _outputQueueName);
+ }
+
+ // Log the next scheduled timer occurrence
+ _logger.LogInformation("[GreetingConsumer] Function Ran. Next timer schedule = {nextSchedule}", timerInfo.ScheduleStatus?.Next);
+ }
+
+ ///
+ /// Selects a random greeting template and formats it with the given name.
+ /// The generated greeting is also stored in a circular buffer for later retrieval.
+ ///
+ /// The name to include in the greeting.
+ /// A randomly chosen greeting string addressed to the specified name.
+ private static string GetGreeting(string name)
+ {
+ var template = _greetingTemplates[_random.Next(_greetingTemplates.Length)];
+ var greeting = string.Format(template, name);
+
+ lock (_historyLock)
+ {
+ _handlerSent.Add(greeting);
+ }
+
+ return greeting;
+ }
+
+ ///
+ /// HTTP-triggered function that returns the most recent greetings from the circular buffer.
+ /// Greetings are returned in reverse chronological order (newest first).
+ ///
+ /// The incoming HTTP request.
+ /// The number of greetings to return (default: 20, max: 100).
+ /// An HTTP response containing a JSON array of recent greetings.
+ [Function("GetGreetings")]
+ public async Task GetGreetingsAsync(
+ [HttpTrigger(AuthorizationLevel.Function, "get", Route = "greetings")] HttpRequestData request,
+ int count = 20)
+ {
+ _logger.LogInformation("[GetGreetings] Retrieving last {count} entries.", count);
+
+ // Clamp count to valid range
+ if (count < 1) count = 1;
+ if (count > MaxHistory) count = MaxHistory;
+
+ object history;
+ lock (_historyLock)
+ {
+ history = new
+ {
+ requester = new
+ {
+ sent = _requesterSent.ToArray(count)
+ },
+ handler = new
+ {
+ received = _handlerReceived.ToArray(count),
+ sent = _handlerSent.ToArray(count)
+ },
+ consumer = new
+ {
+ received = _consumerReceived.ToArray(count)
+ }
+ };
+ }
+
+ var response = request.CreateResponse(HttpStatusCode.OK);
+ response.Headers.Add("Content-Type", "application/json");
+ await response.WriteStringAsync(JsonSerializer.Serialize(history));
+ return response;
+ }
+
+ private sealed class CircularBuffer
+ {
+ private readonly string[] _items;
+ private int _index;
+ private int _count;
+
+ public CircularBuffer(int capacity) => _items = new string[capacity];
+
+ public void Add(string item)
+ {
+ _items[_index] = item;
+ _index = (_index + 1) % _items.Length;
+ if (_count < _items.Length) _count++;
+ }
+
+ public string[] ToArray(int count)
+ {
+ var available = Math.Min(count, _count);
+ var result = new string[available];
+ for (int i = 0; i < available; i++)
+ {
+ var idx = (_index - available + i + _items.Length) % _items.Length;
+ result[i] = _items[idx];
+ }
+ return result;
+ }
+ }
+}
+
+///
+/// Represents the input payload for greeting requests.
+///
+public class RequestMessage
+{
+ ///
+ /// Gets or sets the name to greet.
+ ///
+ [JsonPropertyName("name")]
+ public required string Name { get; set; }
+}
+
+///
+/// Represents the response payload for greeting requests.
+///
+public class ResponseMessage
+{
+ ///
+ /// Gets or sets the date of the response message.
+ ///
+ [JsonPropertyName("date")]
+ public required string Date { get; set; }
+
+ ///
+ /// Gets or sets the text of the response message.
+ ///
+ [JsonPropertyName("text")]
+ public required string Text { get; set; }
}
\ No newline at end of file
diff --git a/samples/function-app-service-bus/dotnet/src/Program.cs b/samples/function-app-service-bus/dotnet/src/Program.cs
index c76837e4..51336f3d 100644
--- a/samples/function-app-service-bus/dotnet/src/Program.cs
+++ b/samples/function-app-service-bus/dotnet/src/Program.cs
@@ -1,7 +1,7 @@
-using Microsoft.Extensions.Hosting;
-
-var host = new HostBuilder()
- .ConfigureFunctionsWorkerDefaults()
- .Build();
-
-host.Run();
+using Microsoft.Extensions.Hosting;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults()
+ .Build();
+
+host.Run();
diff --git a/samples/function-app-service-bus/dotnet/src/sample.csproj b/samples/function-app-service-bus/dotnet/src/sample.csproj
index 8b21d364..c6917428 100644
--- a/samples/function-app-service-bus/dotnet/src/sample.csproj
+++ b/samples/function-app-service-bus/dotnet/src/sample.csproj
@@ -1,31 +1,31 @@
-
-
- net10.0
- v4
- Exe
- enable
- enable
-
-
-
-
-
-
-
-
-
-
-
-
-
- PreserveNewest
-
-
- PreserveNewest
- Never
-
-
-
-
-
+
+
+ net10.0
+ v4
+ Exe
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/function-app-storage-http/dotnet/src/sample/GameSessionManager.cs b/samples/function-app-storage-http/dotnet/src/sample/GameSessionManager.cs
index ca1d8ebc..9c500bb4 100644
--- a/samples/function-app-storage-http/dotnet/src/sample/GameSessionManager.cs
+++ b/samples/function-app-storage-http/dotnet/src/sample/GameSessionManager.cs
@@ -1,778 +1,778 @@
-using System.Net;
-using System.Text.Json;
-using Microsoft.Azure.Functions.Worker;
-using Microsoft.Azure.Functions.Worker.Http;
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Configuration;
-using Azure.Storage.Blobs;
-using Azure.Storage.Queues;
-using Azure.Storage.Queues.Models;
-using Azure.Data.Tables;
-using Azure;
-
-namespace LocalStack.Azure.Samples;
-
-///
-/// Manages game sessions and player interactions using Azure Functions with hybrid storage approach.
-/// This class demonstrates a complete gaming scoreboard system with blob storage for game files,
-/// queue processing for game events, table storage for winners, and internal dictionary for active game data.
-/// The system processes GameStatusRequest/GameStatusResponse messages and maintains game state in memory
-/// for improved performance while still utilizing Azure Storage services for persistence and messaging.
-///
-public class GameSessionManager
-{
- // Instance field for logging - keeps proper Azure Functions execution context
- private readonly ILogger _logger;
-
- // Static configuration values - initialized once per application lifetime
- private static string? _connectionString;
- private static string? _inputQueueName;
- private static string? _outputQueueName;
- private static string? _triggerQueueName;
- private static string? _inputContainerName;
- private static string? _outputContainerName;
- private static string? _inputTableName;
- private static string? _outputTableName;
- private static string[]? _playerNames;
- private static bool _configurationValid;
- private static int _gameId = 1;
-
- // Static dictionary to store game data in memory - game ID as key, list of player scores as value
- // This replaces Azure Table Storage for demonstration purposes and provides faster access
- private static readonly Dictionary> _gameData = new Dictionary>();
- private static readonly object _gameDataLock = new object();
-
- // Static initialization - runs once per application lifetime
- private static readonly Lazy _infrastructureInitialization = new Lazy(() => InitializeInfrastructureOnceAsync());
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The logger factory used to create loggers for this class.
- public GameSessionManager(ILoggerFactory loggerFactory)
- {
- _logger = loggerFactory.CreateLogger();
- }
-
- ///
- /// Ensures that Azure infrastructure (queues, containers, tables) is initialized exactly once
- /// during the application lifetime. This method is thread-safe and idempotent.
- ///
- /// A task that completes when infrastructure initialization is finished.
- private async Task EnsureInfrastructureInitializedAsync()
- {
- await _infrastructureInitialization.Value;
- }
-
- ///
- /// One-time initialization of Azure Storage infrastructure (queues, containers, tables).
- /// This method runs exactly once per application lifetime and stores configuration values in static fields.
- ///
- /// A task representing the asynchronous initialization operation.
- private static async Task InitializeInfrastructureOnceAsync()
- {
- try
- {
- // Create a temporary configuration instance for initialization
- var configBuilder = new ConfigurationBuilder()
- .AddEnvironmentVariables()
- .AddJsonFile("local.settings.json", optional: true);
- var config = configBuilder.Build();
-
- // Create a temporary logger for initialization
- using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
- var logger = loggerFactory.CreateLogger();
-
- logger.LogInformation("[InitializeInfrastructureOnceAsync] Starting one-time infrastructure initialization...");
-
- // Read and store configuration values in static fields with fallback defaults
- _connectionString = config["STORAGE_ACCOUNT_CONNECTION_STRING"];
- _inputQueueName = config["INPUT_QUEUE_NAME"] ?? "input";
- _outputQueueName = config["OUTPUT_QUEUE_NAME"] ?? "output";
- _triggerQueueName = config["TRIGGER_QUEUE_NAME"] ?? "trigger";
- _inputContainerName = config["INPUT_STORAGE_CONTAINER_NAME"] ?? "input";
- _outputContainerName = config["OUTPUT_STORAGE_CONTAINER_NAME"] ?? "output";
- _inputTableName = config["INPUT_TABLE_NAME"] ?? "scoreboards";
- _outputTableName = config["OUTPUT_TABLE_NAME"] ?? "winners";
- _playerNames = config["PLAYER_NAMES"]?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
-
- // Check if player names ae configured. If not use, use default names
- if (_playerNames == null || _playerNames.Length == 0)
- {
- logger.LogWarning("[InitializeInfrastructureOnceAsync] PLAYER_NAMES configuration is missing or empty. Using default names.");
- _playerNames = new[] { "Alice", "Anastasia", "Paolo", "Leo", "Mia" };
- }
-
- // Validate configuration and set the flag
- _configurationValid = ValidateConfigurationValues(logger);
-
- if (_configurationValid && _connectionString != null)
- {
- // Initialize all infrastructure components
- await InitializeQueuesAsync(_connectionString, logger, new[] { _inputQueueName, _outputQueueName, _triggerQueueName }.Where(q => q != null).ToArray()!);
- await InitializeContainersAsync(_connectionString, logger, new[] { _inputContainerName, _outputContainerName }.Where(c => c != null).ToArray()!);
- await InitializeTablesAsync(_connectionString, logger, new[] { _inputTableName, _outputTableName }.Where(t => t != null).ToArray()!);
-
- logger.LogInformation("[InitializeInfrastructureOnceAsync] Infrastructure initialization completed successfully.");
- }
- else
- {
- logger.LogError("[InitializeInfrastructureOnceAsync] Configuration validation failed. Infrastructure initialization aborted.");
- }
- }
- catch (Exception ex)
- {
- // Log error but don't throw - let functions continue to work even if initialization fails
- Console.WriteLine("[InitializeInfrastructureOnceAsync] Failed to initialize infrastructure: {0}", ex.Message);
- _configurationValid = false;
- }
- }
-
- ///
- /// Validates that all required configuration values are present and not empty.
- /// With default values in place, only the connection string is mandatory.
- ///
- /// Logger for reporting validation errors.
- /// True if all configuration values are valid, false otherwise.
- private static bool ValidateConfigurationValues(ILogger logger)
- {
- bool isValid = true;
-
- // Connection string is the only truly required value - everything else has defaults
- if (string.IsNullOrWhiteSpace(_connectionString))
- {
- logger.LogError("[ValidateConfigurationValues] STORAGE_ACCOUNT_CONNECTION_STRING configuration value is missing and is required.");
- isValid = false;
- }
-
- // Log the configuration values being used (helpful for debugging)
- if (isValid)
- {
- logger.LogInformation("[ValidateConfigurationValues] Configuration loaded successfully:");
- logger.LogInformation(" - Input Queue: {inputQueue}", _inputQueueName);
- logger.LogInformation(" - Output Queue: {outputQueue}", _outputQueueName);
- logger.LogInformation(" - Trigger Queue: {triggerQueue}", _triggerQueueName);
- logger.LogInformation(" - Input Container: {inputContainer}", _inputContainerName);
- logger.LogInformation(" - Output Container: {outputContainer}", _outputContainerName);
- logger.LogInformation(" - Input Table: {inputTable}", _inputTableName);
- logger.LogInformation(" - Output Table: {outputTable}", _outputTableName);
- }
-
- return isValid;
- }
-
- ///
- /// Checks if configuration values have been successfully loaded and validated.
- /// This method provides a fast runtime check without re-reading configuration.
- /// With default values, this primarily checks if the connection string is available.
- ///
- /// True if configuration is valid and available, false otherwise.
- private static bool IsConfigurationValid()
- {
- // Since we have defaults for all values except connection string,
- // we only need to check the configuration validation flag and connection string
- return _configurationValid && !string.IsNullOrWhiteSpace(_connectionString);
- }
-
- ///
- /// Static version of queue initialization for one-time setup.
- ///
- private static async Task InitializeQueuesAsync(string connectionString, ILogger logger, string[] queues)
- {
- try
- {
- foreach (var queueName in queues.Where(q => !string.IsNullOrWhiteSpace(q)))
- {
- var queueClient = new QueueClient(connectionString, queueName);
- await queueClient.CreateIfNotExistsAsync();
- logger.LogInformation("[InitializeQueuesAsync] Initialized queue: {queueName}", queueName);
- }
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "[InitializeQueuesAsync] Failed to initialize queues.");
- }
- }
-
- ///
- /// Static version of container initialization for one-time setup.
- ///
- private static async Task InitializeContainersAsync(string connectionString, ILogger logger, string[] containers)
- {
- try
- {
- var blobServiceClient = new BlobServiceClient(connectionString);
- foreach (var containerName in containers.Where(c => !string.IsNullOrWhiteSpace(c)))
- {
- var containerClient = blobServiceClient.GetBlobContainerClient(containerName);
- await containerClient.CreateIfNotExistsAsync();
- logger.LogInformation("[InitializeContainersAsync] Initialized container: {containerName}", containerName);
- }
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "[InitializeContainersAsync] Failed to initialize containers.");
- }
- }
-
- ///
- /// Static version of table initialization for one-time setup.
- ///
- private static async Task InitializeTablesAsync(string connectionString, ILogger logger, string[] tables)
- {
- try
- {
- foreach (var tableName in tables.Where(t => !string.IsNullOrWhiteSpace(t)))
- {
- var tableClient = new TableClient(connectionString, tableName);
- await tableClient.CreateIfNotExistsAsync();
- logger.LogInformation("[InitializeTablesAsync] Initialized table: {tableName}", tableName);
- }
- }
- catch (Exception ex)
- {
- logger.LogError(ex, "[InitializeTablesAsync] Failed to initialize tables.");
- }
- }
-
- ///
- /// Handles HTTP GET requests to retrieve player score for a specific game and player.
- ///
- /// The HTTP request data.
- /// The game ID to retrieve scores for, provided in the route.
- /// The player name to retrieve status for, provided in the route.
- /// An HTTP response with player score information or an error message.
- [Function("GetPlayerScore")]
- public async Task GetPlayerScoreAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "player/{gameId}/{name}/status")] HttpRequestData request, int gameId, string name)
- {
- HttpResponseData response;
-
- // Log the incoming request
- _logger.LogInformation("[GetPlayerScore] Received GET request with gameId = {gameId}, name = {name}.", gameId, name ?? "NULL");
-
- // Validate the name parameter
- if (name == null || string.IsNullOrWhiteSpace(name))
- {
- response = request.CreateResponse(HttpStatusCode.BadRequest);
- await response.WriteStringAsync("Invalid parameters: name parameter is required.");
- return response;
- }
-
- // Check if the game and player exist in the internal dictionary
- PlayerScore? playerScore = null;
- lock (_gameDataLock)
- {
- if (_gameData.TryGetValue(gameId, out var players))
- {
- playerScore = players.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
- }
- }
-
- if (playerScore == null)
- {
- response = request.CreateResponse(HttpStatusCode.NotFound);
- await response.WriteStringAsync($"Game {gameId} and player '{name}' tuple not found.");
- return response;
- }
-
- // Return the player score information
- response = request.CreateResponse(HttpStatusCode.OK);
-
- // Create the response message
- var outputObj = new PlayerScoreResponse
- {
- Date = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
- GameId = gameId,
- Name = playerScore.Name,
- Score = playerScore.Score
- };
- var outputMessage = JsonSerializer.Serialize(outputObj);
-
- // Write the response message to the HTTP response
- await response.WriteStringAsync(outputMessage);
-
- // Log the successful processing
- _logger.LogInformation("[GetPlayerScore] Processed request successfully with gameId = {gameId}, name = {name}, score = {score}.", gameId, name, playerScore.Score);
-
- // Return the HTTP response
- return response;
- }
-
- ///
- /// Handles HTTP POST and PUT requests to retrieve game status information.
- /// Accepts a GameStatusRequest in the request body and returns comprehensive game status
- /// including winner determination and all player scores for the specified game.
- ///
- /// The HTTP request data containing a JSON-serialized GameStatusRequest.
- /// An HTTP response with GameStatusResponse containing game status details, winner, and all players, or an error message if the game is not found or invalid.
- [Function("CreateGameStatus")]
- public async Task CreateGameStatusAsync([HttpTrigger(AuthorizationLevel.Function, "post", "put", Route = "game/session")] HttpRequestData request)
- {
- HttpResponseData response;
-
- // Log the incoming request method
- _logger.LogInformation("[CreateGameStatus] Received {method} request.", request.Method);
-
- // Read the request body as a string
- var requestBody = await request.ReadAsStringAsync();
-
- // Validate that the request body is not empty
- if (requestBody == null || string.IsNullOrWhiteSpace(requestBody))
- {
- response = request.CreateResponse(HttpStatusCode.BadRequest);
- await response.WriteStringAsync("[CreateGameStatus] Invalid request message: Request body is required.");
- return response;
- }
-
- GameStatusRequest? requestMessage;
- try
- {
- // Attempt to deserialize the request body into a GameStatusRequest object
- requestMessage = JsonSerializer.Deserialize(requestBody);
- }
- catch (JsonException)
- {
- // Handle invalid JSON format
- response = request.CreateResponse(HttpStatusCode.BadRequest);
- await response.WriteStringAsync("[CreateGameStatus] Invalid request message: Request body is not in the proper format.");
- return response;
- }
-
- // Validate that the GameId property is present and valid
- if (requestMessage == null || requestMessage.GameId <= 0)
- {
- response = request.CreateResponse(HttpStatusCode.BadRequest);
- await response.WriteStringAsync("[CreateGameStatus] Invalid request message: 'GameId' is required and must be greater than 0.");
- return response;
- }
-
- // Check if the game exists in the internal dictionary
- List? players = null;
- bool gameFound = false;
- lock (_gameDataLock)
- {
- gameFound = _gameData.TryGetValue(requestMessage.GameId, out players);
- }
-
- if (!gameFound || players == null)
- {
- _logger.LogWarning("[CreateGameStatus] Game {gameId} not found in internal data store.", requestMessage.GameId);
- response = request.CreateResponse(HttpStatusCode.NotFound);
- await response.WriteStringAsync($"Game {requestMessage.GameId} not found.");
- return response;
- }
-
- if (players.Count == 0)
- {
- _logger.LogWarning("[CreateGameStatus] Game {gameId} has no player data.", requestMessage.GameId);
- response = request.CreateResponse(HttpStatusCode.NotFound);
- await response.WriteStringAsync($"Game {requestMessage.GameId} has no player data.");
- return response;
- }
-
- // Find the winner (player with highest score)
- var winner = players.OrderByDescending(p => p.Score).First();
-
- // Return a response if the request message is valid
- response = request.CreateResponse(HttpStatusCode.OK);
-
- // Create the response message
- var outputObj = new GameStatusResponse
- {
- Date = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
- GameId = requestMessage.GameId,
- Winner = winner.Name,
- Players = new List(players) // Create a copy of the list
- };
- var outputMessage = JsonSerializer.Serialize(outputObj);
-
- // Write the response message to the HTTP response
- await response.WriteStringAsync(outputMessage);
-
- // Log the successful processing
- _logger.LogInformation("[CreateGameStatus] Processed request successfully with gameId = {gameId}, winner = {winner}.", requestMessage.GameId, winner.Name);
-
- // Return the HTTP response
- return response;
- }
-
- ///
- /// Processes uploaded game status files from blob storage and generates comprehensive game status responses.
- /// Deserializes GameStatusRequest from blob content, retrieves game data from internal dictionary,
- /// determines the winner, and returns a GameStatusResponse with complete game information.
- ///
- /// The blob content as byte array containing JSON-serialized GameStatusRequest.
- /// The name of the blob file being processed.
- /// A JSON-formatted GameStatusResponse string containing game status, winner, and all players, or null if the input is invalid or game not found.
- [Function("ProcessGameFile")]
- [BlobOutput("%OUTPUT_STORAGE_CONTAINER_NAME%/{name}")]
- public string? ProcessGameFile(
- [BlobTrigger("%INPUT_STORAGE_CONTAINER_NAME%/{name}", Connection = "STORAGE_ACCOUNT_CONNECTION_STRING")] byte[] blobBytes,
- string name)
- {
- // Check that the blobBytes is not null or empty
- if (blobBytes == null || blobBytes.Length == 0)
- {
- _logger.LogError("[ProcessGameFile] Received [{name}] blob is empty or null.", name);
- return null;
- }
-
- // Convert the byte array to a string
- string json = System.Text.Encoding.UTF8.GetString(blobBytes);
-
- // Check that the JSON is not null or empty
- if (string.IsNullOrEmpty(json))
- {
- _logger.LogError("[ProcessGameFile] Received [{name}] blob is empty or invalid.", name);
- return null;
- }
-
- // Deserialize the JSON into a GameStatusRequest object
- GameStatusRequest? gameStatusRequest = JsonSerializer.Deserialize(json);
-
- // Check that the request message is not null
- if (gameStatusRequest == null)
- {
- _logger.LogError("[ProcessGameFile] Received [{name}] blob contains invalid GameStatusRequest.", name);
- return null;
- }
-
- // Check if the game exists in the internal dictionary
- List? players = null;
- lock (_gameDataLock)
- {
- if (!_gameData.TryGetValue(gameStatusRequest.GameId, out players))
- {
- _logger.LogWarning("[ProcessGameFile] Game {gameId} not found in internal data store.", gameStatusRequest.GameId);
- return null;
- }
- }
-
- if (players == null || players.Count == 0)
- {
- _logger.LogWarning("[ProcessGameFile] Game {gameId} has no player data.", gameStatusRequest.GameId);
- return null;
- }
-
- // Find the winner (player with highest score)
- var winner = players.OrderByDescending(p => p.Score).First();
-
- // Create the response message
- var outputObj = new GameStatusResponse
- {
- Date = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
- GameId = gameStatusRequest.GameId,
- Winner = winner.Name,
- Players = new List(players) // Create a copy of the list
- };
- var outputMessage = JsonSerializer.Serialize(outputObj);
-
- // Log the successful processing of the blob
- _logger.LogInformation("[ProcessGameFile] Processed blob [{name}] successfully for game {gameId}.", name, gameStatusRequest.GameId);
-
- // Return the response message
- return outputMessage;
- }
-
- ///
- /// Handles game events from Azure Storage Queue and processes game status requests.
- /// Deserializes GameStatusRequest from queue messages, retrieves game data from internal dictionary,
- /// determines the winner, and returns a GameStatusResponse for further processing in the output queue.
- ///
- /// The incoming queue message containing JSON-serialized GameStatusRequest data.
- /// The function execution context provided by the Azure Functions runtime.
- ///
- /// A JSON-formatted GameStatusResponse string containing game status, winner, and all players for output queue processing, or null if the input is invalid or game not found.
- ///
- [Function("HandleGameEvent")]
- [QueueOutput("%OUTPUT_QUEUE_NAME%", Connection = "STORAGE_ACCOUNT_CONNECTION_STRING")]
- public string? HandleGameEvent([QueueTrigger("%INPUT_QUEUE_NAME%", Connection = "STORAGE_ACCOUNT_CONNECTION_STRING")] QueueMessage message, FunctionContext context)
- {
-
- // Check that the message and the body are not null or empty
- if (message == null || string.IsNullOrWhiteSpace(message.Body?.ToString()))
- {
- _logger.LogError("[HandleGameEvent] Received queue message is null or empty.");
- return null;
- }
-
- var json = message.Body.ToString() ?? string.Empty;
-
- // Check that the JSON is not null or empty
- if (string.IsNullOrEmpty(json))
- {
- _logger.LogError("[HandleGameEvent] Received [{messageId}] queue message is empty or invalid.", message.MessageId);
- return null;
- }
-
- // Deserialize the JSON into a GameStatusRequest object
- GameStatusRequest? requestMessage = JsonSerializer.Deserialize(json);
-
- // Check that the request message is not null
- if (requestMessage == null)
- {
- _logger.LogError("[HandleGameEvent] Received [{messageId}] queue message contains invalid GameStatusRequest.", message.MessageId);
- return null;
- }
-
- // Check if the game exists in the internal dictionary
- List? players = null;
- lock (_gameDataLock)
- {
- if (!_gameData.TryGetValue(requestMessage.GameId, out players))
- {
- _logger.LogWarning("[HandleGameEvent] Game {gameId} not found in internal data store.", requestMessage.GameId);
- // Return null for now, but could create an error response if needed
- return null;
- }
- }
-
- if (players == null || players.Count == 0)
- {
- _logger.LogWarning("[HandleGameEvent] Game {gameId} has no player data.", requestMessage.GameId);
- return null;
- }
-
- // Find the winner (player with highest score)
- var winner = players.OrderByDescending(p => p.Score).First();
-
- // Create the response message
- var outputObj = new GameStatusResponse
- {
- Date = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
- GameId = requestMessage.GameId,
- Winner = winner.Name,
- Players = new List(players) // Create a copy of the list
- };
- var outputMessage = JsonSerializer.Serialize(outputObj);
-
- // Log the successful processing of the queue message
- _logger.LogInformation("[HandleGameEvent] Processed queue message [{messageId}] successfully for game {gameId}.", message.MessageId, requestMessage.GameId);
-
- // Return the response message
- return outputMessage;
-
- }
-
- ///
- /// Processes game scoreboards to determine winners and manage game results.
- /// Retrieves scoreboard entries for a game, finds the highest score, and records the winner.
- ///
- /// The game ID from the queue message to filter scoreboard entries.
- /// The scoreboard entities matching the specified game ID.
- /// The winning ScoreboardEntity with the highest score, or null if no entities are found.
- [Function("ProcessScoreboard")]
- [TableOutput("%OUTPUT_TABLE_NAME%", Connection = "STORAGE_ACCOUNT_CONNECTION_STRING")]
- public ScoreboardEntity? ProcessScoreboard(
- [QueueTrigger("%TRIGGER_QUEUE_NAME%")] string gameId,
- [TableInput("%INPUT_TABLE_NAME%", "{queueTrigger}",
- Connection = "STORAGE_ACCOUNT_CONNECTION_STRING")] IEnumerable entities)
- {
- // Find the entity with the highest score
- var winner = entities.OrderByDescending(e => e.Score).FirstOrDefault();
- _logger.LogInformation("[ProcessScoreboard] Processed game ID {gameId}. Winner: {winnerName} with score {score}.", gameId, winner?.PlayerName ?? "No winner", winner?.Score.ToString() ?? "N/A");
-
- if (winner != null)
- {
- // Create a new entity for the output table with a new RowKey
- var winnerEntity = new ScoreboardEntity
- {
- PartitionKey = $"winner-game-{int.Parse(gameId):D3}",
- RowKey = Guid.NewGuid().ToString(),
- GameId = winner.GameId,
- PlayerName = winner.PlayerName,
- Score = winner.Score,
- Timestamp = DateTimeOffset.UtcNow,
- ETag = ETag.All
- };
-
- return winnerEntity;
- }
-
- return null;
- }
-
- ///
- /// Timer-triggered function that generates new game rounds with random player data and initiates the complete gaming workflow.
- /// Creates GameStatusRequest objects, uploads them as blobs, sends queue messages, creates internal dictionary entries
- /// with random player scores, and triggers the scoreboard processing pipeline. Runs every minute and on startup.
- ///
- /// Timer metadata containing schedule status and next occurrence information.
- /// A task that represents the asynchronous game round generation operation.
- [Function("CreateGame")]
- [FixedDelayRetry(5, "00:00:10")]
- public async Task CreateGameAsync([TimerTrigger("0 */1 * * * *", RunOnStartup = true)] TimerInfo timerInfo)
- {
- _logger.LogInformation("[CreateGameAsync] Triggered execution.");
-
- // Ensure infrastructure is initialized (runs only once per app lifetime)
- await EnsureInfrastructureInitializedAsync();
-
- // Fast configuration validation using pre-loaded static values
- if (!IsConfigurationValid())
- {
- _logger.LogError("[CreateGameAsync] Configuration is invalid or not loaded. Aborting function execution.");
- return;
- }
-
- if (_playerNames == null || _playerNames.Length == 0)
- {
- _logger.LogError("[CreateGameAsync] Player names are not configured. Aborting function execution.");
- return;
- }
-
- var random = new Random();
- var gameStatusRequest = new GameStatusRequest { GameId = _gameId };
-
- // Serialize the request message to JSON
- var message = JsonSerializer.Serialize(gameStatusRequest);
-
- // Log the generated message and configuration values
- _logger.LogInformation("[CreateGameAsync] Generated message: {message}", message);
-
- // Create a unique blob name with the required format
- var now = DateTime.UtcNow;
- var blobFileName = $"game-{_gameId:D3}-status-{now:yyyy-MM-dd-HH-mm-ss}.json";
-
- // Create scoreboard entries for all players using the updated method
- await CreateScoreboardEntriesAsync(_connectionString, _inputTableName);
-
- // Upload blob to the input container
- await UploadBlobAsync(_connectionString, _inputContainerName, blobFileName, message);
-
- // Send message to the input queue
- await SendQueueMessageAsync(_connectionString, _inputQueueName, message);
-
- // Send message to the trigger queue
- await SendQueueMessageAsync(_connectionString, _triggerQueueName, _gameId.ToString());
-
- // Increment game ID for next execution
- _gameId++;
-
- // Log the next scheduled timer occurrence
- _logger.LogInformation("[CreateGameAsync] Function Ran. Next timer schedule = {nextSchedule}", timerInfo.ScheduleStatus?.Next);
- }
-
- ///
- /// Uploads a message as a blob to the specified Azure Storage container.
- ///
- /// The storage account connection string.
- /// The name of the container to upload to.
- /// The name of the blob file to create.
- /// The message content to upload as blob data.
- /// A task that represents the asynchronous upload operation.
- private async Task UploadBlobAsync(string? connectionString, string? inputContainerName, string blobFileName, string message)
- {
- try
- {
- var blobServiceClient = new BlobServiceClient(connectionString);
- var blobContainerClient = blobServiceClient.GetBlobContainerClient(inputContainerName);
-
- await blobContainerClient.CreateIfNotExistsAsync();
-
- var blobClient = blobContainerClient.GetBlobClient(blobFileName);
-
- using (var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(message)))
- {
- await blobClient.UploadAsync(stream, overwrite: true);
- }
- _logger.LogInformation("[UploadBlobAsync] Uploaded blob: {blobFileName} to container: {containerName}", blobFileName, inputContainerName);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "[UploadBlobAsync] Failed to upload blob: {blobFileName} to container: {containerName}", blobFileName, inputContainerName);
- }
- }
-
- ///
- /// Sends a message to the specified Azure Storage queue.
- ///
- /// The storage account connection string.
- /// The name of the queue to send the message to.
- /// The message content to send (will be Base64 encoded).
- /// A task that represents the asynchronous send operation.
- private async Task SendQueueMessageAsync(string? connectionString, string? queueName, string message)
- {
- try
- {
- var queueClient = new QueueClient(connectionString, queueName);
-
- await queueClient.CreateIfNotExistsAsync();
-
- await queueClient.SendMessageAsync(Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(message)));
- _logger.LogInformation("[SendQueueMessageAsync] Sent message to queue: {queueName}", queueName);
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "[SendQueueMessageAsync] Failed to send message to queue: {queueName}", queueName);
- }
- }
-
- ///
- /// Creates scoreboard entries for each player with random scores and stores them in the internal dictionary.
- /// This method replaces Azure Table Storage operations by maintaining game data in memory using a thread-safe
- /// Dictionary structure. Each game is stored with its unique game ID and contains all player scores.
- ///
- /// The storage account connection string.
- /// The name of the table to store the scoreboard entries in.
- /// A task that represents the asynchronous operation of creating and storing player scores.
- private async Task CreateScoreboardEntriesAsync(string? connectionString, string? tableName)
- {
- try
- {
- var random = new Random();
- var playerScores = new List();
- var tableClient = new TableClient(connectionString, tableName);
- await tableClient.CreateIfNotExistsAsync();
- var partitionKey = _gameId.ToString();
-
- if (_playerNames == null || _playerNames.Length == 0)
- {
- _logger.LogWarning("[CreateScoreboardEntriesAsync] No player names configured. Skipping scoreboard entry creation.");
- return;
- }
-
- foreach (var name in _playerNames)
- {
- var score = Math.Max(0, random.Next(0, 101)); // Random number between 0 and 100, ensure >= 0
- var playerScore = new PlayerScore
- {
- Name = name,
- Score = score
- };
-
- playerScores.Add(playerScore);
-
- var entity = new ScoreboardEntity
- {
- PartitionKey = partitionKey,
- RowKey = Guid.NewGuid().ToString(),
- GameId = _gameId,
- PlayerName = name,
- Score = score,
- Timestamp = DateTimeOffset.UtcNow,
- ETag = ETag.All
- };
-
- await tableClient.AddEntityAsync(entity);
-
- _logger.LogInformation("[CreateScoreboardEntriesAsync] Added scoreboard entry for {playerName} with score {score} in game {gameId}", name, score, _gameId);
- }
-
- // Store the game data in the internal dictionary (thread-safe)
- lock (_gameDataLock)
- {
- _gameData[_gameId] = playerScores;
- }
-
- _logger.LogInformation("[CreateScoreboardEntriesAsync] Created {playerCount} scoreboard entries for game {gameId}.", _playerNames.Length, _gameId);
-
- // Simulate async work to maintain the async signature
- await Task.CompletedTask;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "[CreateScoreboardEntriesAsync] Failed to create scoreboard entries for game {gameId}", _gameId);
- }
- }
+using System.Net;
+using System.Text.Json;
+using Microsoft.Azure.Functions.Worker;
+using Microsoft.Azure.Functions.Worker.Http;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Configuration;
+using Azure.Storage.Blobs;
+using Azure.Storage.Queues;
+using Azure.Storage.Queues.Models;
+using Azure.Data.Tables;
+using Azure;
+
+namespace LocalStack.Azure.Samples;
+
+///
+/// Manages game sessions and player interactions using Azure Functions with hybrid storage approach.
+/// This class demonstrates a complete gaming scoreboard system with blob storage for game files,
+/// queue processing for game events, table storage for winners, and internal dictionary for active game data.
+/// The system processes GameStatusRequest/GameStatusResponse messages and maintains game state in memory
+/// for improved performance while still utilizing Azure Storage services for persistence and messaging.
+///
+public class GameSessionManager
+{
+ // Instance field for logging - keeps proper Azure Functions execution context
+ private readonly ILogger _logger;
+
+ // Static configuration values - initialized once per application lifetime
+ private static string? _connectionString;
+ private static string? _inputQueueName;
+ private static string? _outputQueueName;
+ private static string? _triggerQueueName;
+ private static string? _inputContainerName;
+ private static string? _outputContainerName;
+ private static string? _inputTableName;
+ private static string? _outputTableName;
+ private static string[]? _playerNames;
+ private static bool _configurationValid;
+ private static int _gameId = 1;
+
+ // Static dictionary to store game data in memory - game ID as key, list of player scores as value
+ // This replaces Azure Table Storage for demonstration purposes and provides faster access
+ private static readonly Dictionary> _gameData = new Dictionary>();
+ private static readonly object _gameDataLock = new object();
+
+ // Static initialization - runs once per application lifetime
+ private static readonly Lazy _infrastructureInitialization = new Lazy(() => InitializeInfrastructureOnceAsync());
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The logger factory used to create loggers for this class.
+ public GameSessionManager(ILoggerFactory loggerFactory)
+ {
+ _logger = loggerFactory.CreateLogger();
+ }
+
+ ///
+ /// Ensures that Azure infrastructure (queues, containers, tables) is initialized exactly once
+ /// during the application lifetime. This method is thread-safe and idempotent.
+ ///
+ /// A task that completes when infrastructure initialization is finished.
+ private async Task EnsureInfrastructureInitializedAsync()
+ {
+ await _infrastructureInitialization.Value;
+ }
+
+ ///
+ /// One-time initialization of Azure Storage infrastructure (queues, containers, tables).
+ /// This method runs exactly once per application lifetime and stores configuration values in static fields.
+ ///
+ /// A task representing the asynchronous initialization operation.
+ private static async Task InitializeInfrastructureOnceAsync()
+ {
+ try
+ {
+ // Create a temporary configuration instance for initialization
+ var configBuilder = new ConfigurationBuilder()
+ .AddEnvironmentVariables()
+ .AddJsonFile("local.settings.json", optional: true);
+ var config = configBuilder.Build();
+
+ // Create a temporary logger for initialization
+ using var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole());
+ var logger = loggerFactory.CreateLogger();
+
+ logger.LogInformation("[InitializeInfrastructureOnceAsync] Starting one-time infrastructure initialization...");
+
+ // Read and store configuration values in static fields with fallback defaults
+ _connectionString = config["STORAGE_ACCOUNT_CONNECTION_STRING"];
+ _inputQueueName = config["INPUT_QUEUE_NAME"] ?? "input";
+ _outputQueueName = config["OUTPUT_QUEUE_NAME"] ?? "output";
+ _triggerQueueName = config["TRIGGER_QUEUE_NAME"] ?? "trigger";
+ _inputContainerName = config["INPUT_STORAGE_CONTAINER_NAME"] ?? "input";
+ _outputContainerName = config["OUTPUT_STORAGE_CONTAINER_NAME"] ?? "output";
+ _inputTableName = config["INPUT_TABLE_NAME"] ?? "scoreboards";
+ _outputTableName = config["OUTPUT_TABLE_NAME"] ?? "winners";
+ _playerNames = config["PLAYER_NAMES"]?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+
+ // Check if player names ae configured. If not use, use default names
+ if (_playerNames == null || _playerNames.Length == 0)
+ {
+ logger.LogWarning("[InitializeInfrastructureOnceAsync] PLAYER_NAMES configuration is missing or empty. Using default names.");
+ _playerNames = new[] { "Alice", "Anastasia", "Paolo", "Leo", "Mia" };
+ }
+
+ // Validate configuration and set the flag
+ _configurationValid = ValidateConfigurationValues(logger);
+
+ if (_configurationValid && _connectionString != null)
+ {
+ // Initialize all infrastructure components
+ await InitializeQueuesAsync(_connectionString, logger, new[] { _inputQueueName, _outputQueueName, _triggerQueueName }.Where(q => q != null).ToArray()!);
+ await InitializeContainersAsync(_connectionString, logger, new[] { _inputContainerName, _outputContainerName }.Where(c => c != null).ToArray()!);
+ await InitializeTablesAsync(_connectionString, logger, new[] { _inputTableName, _outputTableName }.Where(t => t != null).ToArray()!);
+
+ logger.LogInformation("[InitializeInfrastructureOnceAsync] Infrastructure initialization completed successfully.");
+ }
+ else
+ {
+ logger.LogError("[InitializeInfrastructureOnceAsync] Configuration validation failed. Infrastructure initialization aborted.");
+ }
+ }
+ catch (Exception ex)
+ {
+ // Log error but don't throw - let functions continue to work even if initialization fails
+ Console.WriteLine("[InitializeInfrastructureOnceAsync] Failed to initialize infrastructure: {0}", ex.Message);
+ _configurationValid = false;
+ }
+ }
+
+ ///
+ /// Validates that all required configuration values are present and not empty.
+ /// With default values in place, only the connection string is mandatory.
+ ///
+ /// Logger for reporting validation errors.
+ /// True if all configuration values are valid, false otherwise.
+ private static bool ValidateConfigurationValues(ILogger logger)
+ {
+ bool isValid = true;
+
+ // Connection string is the only truly required value - everything else has defaults
+ if (string.IsNullOrWhiteSpace(_connectionString))
+ {
+ logger.LogError("[ValidateConfigurationValues] STORAGE_ACCOUNT_CONNECTION_STRING configuration value is missing and is required.");
+ isValid = false;
+ }
+
+ // Log the configuration values being used (helpful for debugging)
+ if (isValid)
+ {
+ logger.LogInformation("[ValidateConfigurationValues] Configuration loaded successfully:");
+ logger.LogInformation(" - Input Queue: {inputQueue}", _inputQueueName);
+ logger.LogInformation(" - Output Queue: {outputQueue}", _outputQueueName);
+ logger.LogInformation(" - Trigger Queue: {triggerQueue}", _triggerQueueName);
+ logger.LogInformation(" - Input Container: {inputContainer}", _inputContainerName);
+ logger.LogInformation(" - Output Container: {outputContainer}", _outputContainerName);
+ logger.LogInformation(" - Input Table: {inputTable}", _inputTableName);
+ logger.LogInformation(" - Output Table: {outputTable}", _outputTableName);
+ }
+
+ return isValid;
+ }
+
+ ///
+ /// Checks if configuration values have been successfully loaded and validated.
+ /// This method provides a fast runtime check without re-reading configuration.
+ /// With default values, this primarily checks if the connection string is available.
+ ///
+ /// True if configuration is valid and available, false otherwise.
+ private static bool IsConfigurationValid()
+ {
+ // Since we have defaults for all values except connection string,
+ // we only need to check the configuration validation flag and connection string
+ return _configurationValid && !string.IsNullOrWhiteSpace(_connectionString);
+ }
+
+ ///
+ /// Static version of queue initialization for one-time setup.
+ ///
+ private static async Task InitializeQueuesAsync(string connectionString, ILogger logger, string[] queues)
+ {
+ try
+ {
+ foreach (var queueName in queues.Where(q => !string.IsNullOrWhiteSpace(q)))
+ {
+ var queueClient = new QueueClient(connectionString, queueName);
+ await queueClient.CreateIfNotExistsAsync();
+ logger.LogInformation("[InitializeQueuesAsync] Initialized queue: {queueName}", queueName);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "[InitializeQueuesAsync] Failed to initialize queues.");
+ }
+ }
+
+ ///
+ /// Static version of container initialization for one-time setup.
+ ///
+ private static async Task InitializeContainersAsync(string connectionString, ILogger logger, string[] containers)
+ {
+ try
+ {
+ var blobServiceClient = new BlobServiceClient(connectionString);
+ foreach (var containerName in containers.Where(c => !string.IsNullOrWhiteSpace(c)))
+ {
+ var containerClient = blobServiceClient.GetBlobContainerClient(containerName);
+ await containerClient.CreateIfNotExistsAsync();
+ logger.LogInformation("[InitializeContainersAsync] Initialized container: {containerName}", containerName);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "[InitializeContainersAsync] Failed to initialize containers.");
+ }
+ }
+
+ ///
+ /// Static version of table initialization for one-time setup.
+ ///
+ private static async Task InitializeTablesAsync(string connectionString, ILogger logger, string[] tables)
+ {
+ try
+ {
+ foreach (var tableName in tables.Where(t => !string.IsNullOrWhiteSpace(t)))
+ {
+ var tableClient = new TableClient(connectionString, tableName);
+ await tableClient.CreateIfNotExistsAsync();
+ logger.LogInformation("[InitializeTablesAsync] Initialized table: {tableName}", tableName);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "[InitializeTablesAsync] Failed to initialize tables.");
+ }
+ }
+
+ ///
+ /// Handles HTTP GET requests to retrieve player score for a specific game and player.
+ ///
+ /// The HTTP request data.
+ /// The game ID to retrieve scores for, provided in the route.
+ /// The player name to retrieve status for, provided in the route.
+ /// An HTTP response with player score information or an error message.
+ [Function("GetPlayerScore")]
+ public async Task GetPlayerScoreAsync([HttpTrigger(AuthorizationLevel.Function, "get", Route = "player/{gameId}/{name}/status")] HttpRequestData request, int gameId, string name)
+ {
+ HttpResponseData response;
+
+ // Log the incoming request
+ _logger.LogInformation("[GetPlayerScore] Received GET request with gameId = {gameId}, name = {name}.", gameId, name ?? "NULL");
+
+ // Validate the name parameter
+ if (name == null || string.IsNullOrWhiteSpace(name))
+ {
+ response = request.CreateResponse(HttpStatusCode.BadRequest);
+ await response.WriteStringAsync("Invalid parameters: name parameter is required.");
+ return response;
+ }
+
+ // Check if the game and player exist in the internal dictionary
+ PlayerScore? playerScore = null;
+ lock (_gameDataLock)
+ {
+ if (_gameData.TryGetValue(gameId, out var players))
+ {
+ playerScore = players.FirstOrDefault(p => p.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+
+ if (playerScore == null)
+ {
+ response = request.CreateResponse(HttpStatusCode.NotFound);
+ await response.WriteStringAsync($"Game {gameId} and player '{name}' tuple not found.");
+ return response;
+ }
+
+ // Return the player score information
+ response = request.CreateResponse(HttpStatusCode.OK);
+
+ // Create the response message
+ var outputObj = new PlayerScoreResponse
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
+ GameId = gameId,
+ Name = playerScore.Name,
+ Score = playerScore.Score
+ };
+ var outputMessage = JsonSerializer.Serialize(outputObj);
+
+ // Write the response message to the HTTP response
+ await response.WriteStringAsync(outputMessage);
+
+ // Log the successful processing
+ _logger.LogInformation("[GetPlayerScore] Processed request successfully with gameId = {gameId}, name = {name}, score = {score}.", gameId, name, playerScore.Score);
+
+ // Return the HTTP response
+ return response;
+ }
+
+ ///
+ /// Handles HTTP POST and PUT requests to retrieve game status information.
+ /// Accepts a GameStatusRequest in the request body and returns comprehensive game status
+ /// including winner determination and all player scores for the specified game.
+ ///
+ /// The HTTP request data containing a JSON-serialized GameStatusRequest.
+ /// An HTTP response with GameStatusResponse containing game status details, winner, and all players, or an error message if the game is not found or invalid.
+ [Function("CreateGameStatus")]
+ public async Task CreateGameStatusAsync([HttpTrigger(AuthorizationLevel.Function, "post", "put", Route = "game/session")] HttpRequestData request)
+ {
+ HttpResponseData response;
+
+ // Log the incoming request method
+ _logger.LogInformation("[CreateGameStatus] Received {method} request.", request.Method);
+
+ // Read the request body as a string
+ var requestBody = await request.ReadAsStringAsync();
+
+ // Validate that the request body is not empty
+ if (requestBody == null || string.IsNullOrWhiteSpace(requestBody))
+ {
+ response = request.CreateResponse(HttpStatusCode.BadRequest);
+ await response.WriteStringAsync("[CreateGameStatus] Invalid request message: Request body is required.");
+ return response;
+ }
+
+ GameStatusRequest? requestMessage;
+ try
+ {
+ // Attempt to deserialize the request body into a GameStatusRequest object
+ requestMessage = JsonSerializer.Deserialize(requestBody);
+ }
+ catch (JsonException)
+ {
+ // Handle invalid JSON format
+ response = request.CreateResponse(HttpStatusCode.BadRequest);
+ await response.WriteStringAsync("[CreateGameStatus] Invalid request message: Request body is not in the proper format.");
+ return response;
+ }
+
+ // Validate that the GameId property is present and valid
+ if (requestMessage == null || requestMessage.GameId <= 0)
+ {
+ response = request.CreateResponse(HttpStatusCode.BadRequest);
+ await response.WriteStringAsync("[CreateGameStatus] Invalid request message: 'GameId' is required and must be greater than 0.");
+ return response;
+ }
+
+ // Check if the game exists in the internal dictionary
+ List? players = null;
+ bool gameFound = false;
+ lock (_gameDataLock)
+ {
+ gameFound = _gameData.TryGetValue(requestMessage.GameId, out players);
+ }
+
+ if (!gameFound || players == null)
+ {
+ _logger.LogWarning("[CreateGameStatus] Game {gameId} not found in internal data store.", requestMessage.GameId);
+ response = request.CreateResponse(HttpStatusCode.NotFound);
+ await response.WriteStringAsync($"Game {requestMessage.GameId} not found.");
+ return response;
+ }
+
+ if (players.Count == 0)
+ {
+ _logger.LogWarning("[CreateGameStatus] Game {gameId} has no player data.", requestMessage.GameId);
+ response = request.CreateResponse(HttpStatusCode.NotFound);
+ await response.WriteStringAsync($"Game {requestMessage.GameId} has no player data.");
+ return response;
+ }
+
+ // Find the winner (player with highest score)
+ var winner = players.OrderByDescending(p => p.Score).First();
+
+ // Return a response if the request message is valid
+ response = request.CreateResponse(HttpStatusCode.OK);
+
+ // Create the response message
+ var outputObj = new GameStatusResponse
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
+ GameId = requestMessage.GameId,
+ Winner = winner.Name,
+ Players = new List(players) // Create a copy of the list
+ };
+ var outputMessage = JsonSerializer.Serialize(outputObj);
+
+ // Write the response message to the HTTP response
+ await response.WriteStringAsync(outputMessage);
+
+ // Log the successful processing
+ _logger.LogInformation("[CreateGameStatus] Processed request successfully with gameId = {gameId}, winner = {winner}.", requestMessage.GameId, winner.Name);
+
+ // Return the HTTP response
+ return response;
+ }
+
+ ///
+ /// Processes uploaded game status files from blob storage and generates comprehensive game status responses.
+ /// Deserializes GameStatusRequest from blob content, retrieves game data from internal dictionary,
+ /// determines the winner, and returns a GameStatusResponse with complete game information.
+ ///
+ /// The blob content as byte array containing JSON-serialized GameStatusRequest.
+ /// The name of the blob file being processed.
+ /// A JSON-formatted GameStatusResponse string containing game status, winner, and all players, or null if the input is invalid or game not found.
+ [Function("ProcessGameFile")]
+ [BlobOutput("%OUTPUT_STORAGE_CONTAINER_NAME%/{name}")]
+ public string? ProcessGameFile(
+ [BlobTrigger("%INPUT_STORAGE_CONTAINER_NAME%/{name}", Connection = "STORAGE_ACCOUNT_CONNECTION_STRING")] byte[] blobBytes,
+ string name)
+ {
+ // Check that the blobBytes is not null or empty
+ if (blobBytes == null || blobBytes.Length == 0)
+ {
+ _logger.LogError("[ProcessGameFile] Received [{name}] blob is empty or null.", name);
+ return null;
+ }
+
+ // Convert the byte array to a string
+ string json = System.Text.Encoding.UTF8.GetString(blobBytes);
+
+ // Check that the JSON is not null or empty
+ if (string.IsNullOrEmpty(json))
+ {
+ _logger.LogError("[ProcessGameFile] Received [{name}] blob is empty or invalid.", name);
+ return null;
+ }
+
+ // Deserialize the JSON into a GameStatusRequest object
+ GameStatusRequest? gameStatusRequest = JsonSerializer.Deserialize(json);
+
+ // Check that the request message is not null
+ if (gameStatusRequest == null)
+ {
+ _logger.LogError("[ProcessGameFile] Received [{name}] blob contains invalid GameStatusRequest.", name);
+ return null;
+ }
+
+ // Check if the game exists in the internal dictionary
+ List? players = null;
+ lock (_gameDataLock)
+ {
+ if (!_gameData.TryGetValue(gameStatusRequest.GameId, out players))
+ {
+ _logger.LogWarning("[ProcessGameFile] Game {gameId} not found in internal data store.", gameStatusRequest.GameId);
+ return null;
+ }
+ }
+
+ if (players == null || players.Count == 0)
+ {
+ _logger.LogWarning("[ProcessGameFile] Game {gameId} has no player data.", gameStatusRequest.GameId);
+ return null;
+ }
+
+ // Find the winner (player with highest score)
+ var winner = players.OrderByDescending(p => p.Score).First();
+
+ // Create the response message
+ var outputObj = new GameStatusResponse
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
+ GameId = gameStatusRequest.GameId,
+ Winner = winner.Name,
+ Players = new List(players) // Create a copy of the list
+ };
+ var outputMessage = JsonSerializer.Serialize(outputObj);
+
+ // Log the successful processing of the blob
+ _logger.LogInformation("[ProcessGameFile] Processed blob [{name}] successfully for game {gameId}.", name, gameStatusRequest.GameId);
+
+ // Return the response message
+ return outputMessage;
+ }
+
+ ///
+ /// Handles game events from Azure Storage Queue and processes game status requests.
+ /// Deserializes GameStatusRequest from queue messages, retrieves game data from internal dictionary,
+ /// determines the winner, and returns a GameStatusResponse for further processing in the output queue.
+ ///
+ /// The incoming queue message containing JSON-serialized GameStatusRequest data.
+ /// The function execution context provided by the Azure Functions runtime.
+ ///
+ /// A JSON-formatted GameStatusResponse string containing game status, winner, and all players for output queue processing, or null if the input is invalid or game not found.
+ ///
+ [Function("HandleGameEvent")]
+ [QueueOutput("%OUTPUT_QUEUE_NAME%", Connection = "STORAGE_ACCOUNT_CONNECTION_STRING")]
+ public string? HandleGameEvent([QueueTrigger("%INPUT_QUEUE_NAME%", Connection = "STORAGE_ACCOUNT_CONNECTION_STRING")] QueueMessage message, FunctionContext context)
+ {
+
+ // Check that the message and the body are not null or empty
+ if (message == null || string.IsNullOrWhiteSpace(message.Body?.ToString()))
+ {
+ _logger.LogError("[HandleGameEvent] Received queue message is null or empty.");
+ return null;
+ }
+
+ var json = message.Body.ToString() ?? string.Empty;
+
+ // Check that the JSON is not null or empty
+ if (string.IsNullOrEmpty(json))
+ {
+ _logger.LogError("[HandleGameEvent] Received [{messageId}] queue message is empty or invalid.", message.MessageId);
+ return null;
+ }
+
+ // Deserialize the JSON into a GameStatusRequest object
+ GameStatusRequest? requestMessage = JsonSerializer.Deserialize(json);
+
+ // Check that the request message is not null
+ if (requestMessage == null)
+ {
+ _logger.LogError("[HandleGameEvent] Received [{messageId}] queue message contains invalid GameStatusRequest.", message.MessageId);
+ return null;
+ }
+
+ // Check if the game exists in the internal dictionary
+ List? players = null;
+ lock (_gameDataLock)
+ {
+ if (!_gameData.TryGetValue(requestMessage.GameId, out players))
+ {
+ _logger.LogWarning("[HandleGameEvent] Game {gameId} not found in internal data store.", requestMessage.GameId);
+ // Return null for now, but could create an error response if needed
+ return null;
+ }
+ }
+
+ if (players == null || players.Count == 0)
+ {
+ _logger.LogWarning("[HandleGameEvent] Game {gameId} has no player data.", requestMessage.GameId);
+ return null;
+ }
+
+ // Find the winner (player with highest score)
+ var winner = players.OrderByDescending(p => p.Score).First();
+
+ // Create the response message
+ var outputObj = new GameStatusResponse
+ {
+ Date = DateTime.Now.ToString("yyyy-MM-ddTHH:mm:ss"),
+ GameId = requestMessage.GameId,
+ Winner = winner.Name,
+ Players = new List(players) // Create a copy of the list
+ };
+ var outputMessage = JsonSerializer.Serialize(outputObj);
+
+ // Log the successful processing of the queue message
+ _logger.LogInformation("[HandleGameEvent] Processed queue message [{messageId}] successfully for game {gameId}.", message.MessageId, requestMessage.GameId);
+
+ // Return the response message
+ return outputMessage;
+
+ }
+
+ ///
+ /// Processes game scoreboards to determine winners and manage game results.
+ /// Retrieves scoreboard entries for a game, finds the highest score, and records the winner.
+ ///
+ /// The game ID from the queue message to filter scoreboard entries.
+ /// The scoreboard entities matching the specified game ID.
+ /// The winning ScoreboardEntity with the highest score, or null if no entities are found.
+ [Function("ProcessScoreboard")]
+ [TableOutput("%OUTPUT_TABLE_NAME%", Connection = "STORAGE_ACCOUNT_CONNECTION_STRING")]
+ public ScoreboardEntity? ProcessScoreboard(
+ [QueueTrigger("%TRIGGER_QUEUE_NAME%")] string gameId,
+ [TableInput("%INPUT_TABLE_NAME%", "{queueTrigger}",
+ Connection = "STORAGE_ACCOUNT_CONNECTION_STRING")] IEnumerable entities)
+ {
+ // Find the entity with the highest score
+ var winner = entities.OrderByDescending(e => e.Score).FirstOrDefault();
+ _logger.LogInformation("[ProcessScoreboard] Processed game ID {gameId}. Winner: {winnerName} with score {score}.", gameId, winner?.PlayerName ?? "No winner", winner?.Score.ToString() ?? "N/A");
+
+ if (winner != null)
+ {
+ // Create a new entity for the output table with a new RowKey
+ var winnerEntity = new ScoreboardEntity
+ {
+ PartitionKey = $"winner-game-{int.Parse(gameId):D3}",
+ RowKey = Guid.NewGuid().ToString(),
+ GameId = winner.GameId,
+ PlayerName = winner.PlayerName,
+ Score = winner.Score,
+ Timestamp = DateTimeOffset.UtcNow,
+ ETag = ETag.All
+ };
+
+ return winnerEntity;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Timer-triggered function that generates new game rounds with random player data and initiates the complete gaming workflow.
+ /// Creates GameStatusRequest objects, uploads them as blobs, sends queue messages, creates internal dictionary entries
+ /// with random player scores, and triggers the scoreboard processing pipeline. Runs every minute and on startup.
+ ///
+ /// Timer metadata containing schedule status and next occurrence information.
+ /// A task that represents the asynchronous game round generation operation.
+ [Function("CreateGame")]
+ [FixedDelayRetry(5, "00:00:10")]
+ public async Task CreateGameAsync([TimerTrigger("0 */1 * * * *", RunOnStartup = true)] TimerInfo timerInfo)
+ {
+ _logger.LogInformation("[CreateGameAsync] Triggered execution.");
+
+ // Ensure infrastructure is initialized (runs only once per app lifetime)
+ await EnsureInfrastructureInitializedAsync();
+
+ // Fast configuration validation using pre-loaded static values
+ if (!IsConfigurationValid())
+ {
+ _logger.LogError("[CreateGameAsync] Configuration is invalid or not loaded. Aborting function execution.");
+ return;
+ }
+
+ if (_playerNames == null || _playerNames.Length == 0)
+ {
+ _logger.LogError("[CreateGameAsync] Player names are not configured. Aborting function execution.");
+ return;
+ }
+
+ var random = new Random();
+ var gameStatusRequest = new GameStatusRequest { GameId = _gameId };
+
+ // Serialize the request message to JSON
+ var message = JsonSerializer.Serialize(gameStatusRequest);
+
+ // Log the generated message and configuration values
+ _logger.LogInformation("[CreateGameAsync] Generated message: {message}", message);
+
+ // Create a unique blob name with the required format
+ var now = DateTime.UtcNow;
+ var blobFileName = $"game-{_gameId:D3}-status-{now:yyyy-MM-dd-HH-mm-ss}.json";
+
+ // Create scoreboard entries for all players using the updated method
+ await CreateScoreboardEntriesAsync(_connectionString, _inputTableName);
+
+ // Upload blob to the input container
+ await UploadBlobAsync(_connectionString, _inputContainerName, blobFileName, message);
+
+ // Send message to the input queue
+ await SendQueueMessageAsync(_connectionString, _inputQueueName, message);
+
+ // Send message to the trigger queue
+ await SendQueueMessageAsync(_connectionString, _triggerQueueName, _gameId.ToString());
+
+ // Increment game ID for next execution
+ _gameId++;
+
+ // Log the next scheduled timer occurrence
+ _logger.LogInformation("[CreateGameAsync] Function Ran. Next timer schedule = {nextSchedule}", timerInfo.ScheduleStatus?.Next);
+ }
+
+ ///
+ /// Uploads a message as a blob to the specified Azure Storage container.
+ ///
+ /// The storage account connection string.
+ /// The name of the container to upload to.
+ /// The name of the blob file to create.
+ /// The message content to upload as blob data.
+ /// A task that represents the asynchronous upload operation.
+ private async Task UploadBlobAsync(string? connectionString, string? inputContainerName, string blobFileName, string message)
+ {
+ try
+ {
+ var blobServiceClient = new BlobServiceClient(connectionString);
+ var blobContainerClient = blobServiceClient.GetBlobContainerClient(inputContainerName);
+
+ await blobContainerClient.CreateIfNotExistsAsync();
+
+ var blobClient = blobContainerClient.GetBlobClient(blobFileName);
+
+ using (var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(message)))
+ {
+ await blobClient.UploadAsync(stream, overwrite: true);
+ }
+ _logger.LogInformation("[UploadBlobAsync] Uploaded blob: {blobFileName} to container: {containerName}", blobFileName, inputContainerName);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "[UploadBlobAsync] Failed to upload blob: {blobFileName} to container: {containerName}", blobFileName, inputContainerName);
+ }
+ }
+
+ ///
+ /// Sends a message to the specified Azure Storage queue.
+ ///
+ /// The storage account connection string.
+ /// The name of the queue to send the message to.
+ /// The message content to send (will be Base64 encoded).
+ /// A task that represents the asynchronous send operation.
+ private async Task SendQueueMessageAsync(string? connectionString, string? queueName, string message)
+ {
+ try
+ {
+ var queueClient = new QueueClient(connectionString, queueName);
+
+ await queueClient.CreateIfNotExistsAsync();
+
+ await queueClient.SendMessageAsync(Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(message)));
+ _logger.LogInformation("[SendQueueMessageAsync] Sent message to queue: {queueName}", queueName);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "[SendQueueMessageAsync] Failed to send message to queue: {queueName}", queueName);
+ }
+ }
+
+ ///
+ /// Creates scoreboard entries for each player with random scores and stores them in the internal dictionary.
+ /// This method replaces Azure Table Storage operations by maintaining game data in memory using a thread-safe
+ /// Dictionary structure. Each game is stored with its unique game ID and contains all player scores.
+ ///
+ /// The storage account connection string.
+ /// The name of the table to store the scoreboard entries in.
+ /// A task that represents the asynchronous operation of creating and storing player scores.
+ private async Task CreateScoreboardEntriesAsync(string? connectionString, string? tableName)
+ {
+ try
+ {
+ var random = new Random();
+ var playerScores = new List();
+ var tableClient = new TableClient(connectionString, tableName);
+ await tableClient.CreateIfNotExistsAsync();
+ var partitionKey = _gameId.ToString();
+
+ if (_playerNames == null || _playerNames.Length == 0)
+ {
+ _logger.LogWarning("[CreateScoreboardEntriesAsync] No player names configured. Skipping scoreboard entry creation.");
+ return;
+ }
+
+ foreach (var name in _playerNames)
+ {
+ var score = Math.Max(0, random.Next(0, 101)); // Random number between 0 and 100, ensure >= 0
+ var playerScore = new PlayerScore
+ {
+ Name = name,
+ Score = score
+ };
+
+ playerScores.Add(playerScore);
+
+ var entity = new ScoreboardEntity
+ {
+ PartitionKey = partitionKey,
+ RowKey = Guid.NewGuid().ToString(),
+ GameId = _gameId,
+ PlayerName = name,
+ Score = score,
+ Timestamp = DateTimeOffset.UtcNow,
+ ETag = ETag.All
+ };
+
+ await tableClient.AddEntityAsync(entity);
+
+ _logger.LogInformation("[CreateScoreboardEntriesAsync] Added scoreboard entry for {playerName} with score {score} in game {gameId}", name, score, _gameId);
+ }
+
+ // Store the game data in the internal dictionary (thread-safe)
+ lock (_gameDataLock)
+ {
+ _gameData[_gameId] = playerScores;
+ }
+
+ _logger.LogInformation("[CreateScoreboardEntriesAsync] Created {playerCount} scoreboard entries for game {gameId}.", _playerNames.Length, _gameId);
+
+ // Simulate async work to maintain the async signature
+ await Task.CompletedTask;
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "[CreateScoreboardEntriesAsync] Failed to create scoreboard entries for game {gameId}", _gameId);
+ }
+ }
}
\ No newline at end of file
diff --git a/samples/function-app-storage-http/dotnet/src/sample/Program.cs b/samples/function-app-storage-http/dotnet/src/sample/Program.cs
index c76837e4..51336f3d 100644
--- a/samples/function-app-storage-http/dotnet/src/sample/Program.cs
+++ b/samples/function-app-storage-http/dotnet/src/sample/Program.cs
@@ -1,7 +1,7 @@
-using Microsoft.Extensions.Hosting;
-
-var host = new HostBuilder()
- .ConfigureFunctionsWorkerDefaults()
- .Build();
-
-host.Run();
+using Microsoft.Extensions.Hosting;
+
+var host = new HostBuilder()
+ .ConfigureFunctionsWorkerDefaults()
+ .Build();
+
+host.Run();
diff --git a/samples/function-app-storage-http/dotnet/src/sample/sample.csproj b/samples/function-app-storage-http/dotnet/src/sample/sample.csproj
index d7c6345f..97195a59 100644
--- a/samples/function-app-storage-http/dotnet/src/sample/sample.csproj
+++ b/samples/function-app-storage-http/dotnet/src/sample/sample.csproj
@@ -1,33 +1,33 @@
-
-
- net10.0
- v4
- Exe
- enable
- enable
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- PreserveNewest
-
-
- PreserveNewest
- Never
-
-
-
-
-
+
+
+ net10.0
+ v4
+ Exe
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+ Never
+
+
+
+
+
\ No newline at end of file