Overview
With Azure Functions, you can run small pieces of code without managing infrastructure. With the introduction of C# 13, you get access to more expressive syntax and advanced language features to build clean, testable, and efficient serverless applications.
The following are best practices for building robust, production-ready Azure Functions using C# 13:
- Clean Folder structure
- Dependency injection
- Configuration management
- Logging
- Error handling
- Unit testing
Project Setup
This project is built using the latest .NET 8 and C# 13 features, providing a modern, minimal, and efficient codebase. For scalable serverless execution, it leverages Azure Functions v4 and can be integrated with Azure Storage or Cosmos DB. In order to ensure a clean and testable architecture, this project uses xUnit as the unit testing framework, and Moq as the mocking dependency framework. The Azure CLI and Visual Studio Code offer a lightweight and productive workflow for development and deployment.
Tools & Stack
- .NET 8 / C# 13
- Azure Functions v4
- Azure Storage / Cosmos DB (optional)
- xUnit & Moq
- Azure CLI / Visual Studio Code
Folder Structure
Maintainability and scalability are ensured by the clean, modular structure of the project. In the root folder /AzureFunctionApp are all the main components of Azure Function. In the Functions folder, all function entry points are defined, while the business logic is abstracted into the Services folder. In the Interfaces folder, contracts and abstractions are defined. The Configuration folder contains configuration-related classes, including strongly typed settings, while the Tests folder contains unit tests. The host.json file contains global configuration for the Azure Functions runtime, and local.settings.json contains local development settings and secrets. In addition to aligning with best practices for serverless and test-driven development, this structure promotes the separation of concerns.

Function Example: HTTP Trigger to Process Orders
ProcessOrderFunction.cs
using System.Net;
using System.Text.Json;
using AzureFunctionApp.Interfaces;
using AzureFunctionApp.Models;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace AzureFunctionApp.Functions;
public class ProcessOrderFunction(IOrderService orderService, ILogger<ProcessOrderFunction> logger)
{
private readonly IOrderService _orderService = orderService;
private readonly ILogger<ProcessOrderFunction> _logger = logger;
[Function("ProcessOrder")]
public async Task<HttpResponseData> RunAsync(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "orders")] HttpRequestData req)
{
try
{
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
var order = JsonSerializer.Deserialize<Order>(requestBody);
if (order is null)
{
_logger.LogWarning("Received null or invalid order payload.");
return await CreateResponseAsync(req, HttpStatusCode.BadRequest, "Invalid order payload.");
}
await _orderService.ProcessOrderAsync(order);
_logger.LogInformation("Order {OrderId} processed successfully.", order.Id);
return await CreateResponseAsync(req, HttpStatusCode.OK, "Order processed successfully.");
}
catch (JsonException jsonEx)
{
_logger.LogError(jsonEx, "JSON parsing error.");
return await CreateResponseAsync(req, HttpStatusCode.BadRequest, "Invalid JSON format.");
}
catch (Exception ex)
{
_logger.LogError(ex, "Unhandled exception while processing order.");
return await CreateResponseAsync(req, HttpStatusCode.InternalServerError, "Internal server error.");
}
}
private static async Task<HttpResponseData> CreateResponseAsync(HttpRequestData req, HttpStatusCode statusCode, string message)
{
var response = req.CreateResponse(statusCode);
await response.WriteStringAsync(message);
return response;
}
}
Order.cs
namespace AzureFunctionApp.Models;
public record Order(Guid Id, string ProductName, int Quantity);
IOrderService.cs
using AzureFunctionApp.Models;
namespace AzureFunctionApp.Interfaces;
public interface IOrderService
{
Task ProcessOrderAsync(Order order);
}
OrderService.cs
using AzureFunctionApp.Interfaces;
using AzureFunctionApp.Models;
using Microsoft.Extensions.Logging;
namespace AzureFunctionApp.Services;
public class OrderService(ILogger<OrderService> logger) : IOrderService
{
private readonly ILogger<OrderService> _logger = logger;
public async Task ProcessOrderAsync(Order order)
{
_logger.LogInformation("Processing order for {ProductName} with quantity {Quantity}", order.ProductName, order.Quantity);
// Simulate processing time
await Task.Delay(100);
_logger.LogInformation("Order {OrderId} processed.", order.Id);
}
}
AppSettings.cs
namespace AzureFunctionApp.Configuration;
public class AppSettings
{
public string? Environment { get; init; }
public string? StorageConnectionString { get; init; }
}
local.settings.json
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
}
}
C# 13 Features Used
- Primary constructors (when available in .NET 8)
- List patterns (for filtering)
- Raw string literals (in configurations or templates)
- Interpolated string handlers for optimized logging
Unit Testing with xUnit and Moq
OrderFunctionTests.cs
using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using AzureFunctionApp.Functions;
using AzureFunctionApp.Interfaces;
using AzureFunctionApp.Models;
using AzureFunctionApp.Tests.Helpers;
using Microsoft.Extensions.Logging;
using Moq;
namespace AzureFunctionApp.Tests;
public class ProcessOrderFunctionTests
{
[Fact]
public async Task RunAsync_ShouldReturnOk_WhenOrderIsValid()
{
// Arrange
var order = new Order(Guid.NewGuid(), "Laptop", 2);
var orderJson = JsonSerializer.Serialize(order);
var request = new TestHttpRequestData(orderJson);
var mockOrderService = new Mock<IOrderService>();
mockOrderService.Setup(s => s.ProcessOrderAsync(It.IsAny<Order>())).Returns(Task.CompletedTask);
var logger = Mock.Of<ILogger<ProcessOrderFunction>>();
var function = new ProcessOrderFunction(mockOrderService.Object, logger);
// Act
var response = await function.RunAsync(request);
// Assert
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var bodyString = ((TestHttpResponseData)response).ReadBodyAsString();
Assert.Contains("Order processed successfully", bodyString);
}
}

Join the conversation! Your thoughts help the community grow.