
Learn Azure Functions with C# in this step-by-step tutorial. Build, test, and deploy serverless functions with practical code examples.
Azure Functions C# Tutorial — Build and Deploy Serverless Applications Step by Step
Azure Functions C# is one of the fastest ways to build scalable, event-driven applications without managing infrastructure. Whether you need to process HTTP requests, respond to database changes, or run scheduled tasks, Azure Functions lets you write focused C# code that runs only when triggered — and you pay only for the execution time you use.
In this tutorial, you will learn how to create, test, and deploy an Azure Function using C# from scratch. By the end, you will have a working serverless API running in the cloud, and a solid understanding of how Azure Functions fit into modern .NET application architecture.
What Are Azure Functions and Why Use C# for Serverless?
Azure Functions is Microsoft's serverless compute platform. Instead of provisioning virtual machines or configuring web servers, you write small, single-purpose functions that Azure executes in response to events called triggers.
Here is why C# developers choose Azure Functions over traditional hosting:
- Zero infrastructure management — No IIS configuration, no OS patching, no load balancer setup.
- Automatic scaling — Azure spins up new instances as demand increases and scales down to zero when idle.
- Cost efficiency — The Consumption plan gives you 1 million free executions per month. You pay per execution after that.
- First-class .NET support — Azure Functions runs on the .NET runtime with full support for dependency injection, Entity Framework, and the entire NuGet ecosystem.
- Multiple triggers — HTTP requests, timers, queue messages, blob storage events, Cosmos DB changes, and more.
Azure Functions supports two hosting models for .NET: the isolated worker model (recommended for new projects) and the older in-process model. This tutorial uses the isolated worker model with .NET 8, which is the current best practice.
Prerequisites
Before you begin, make sure you have the following installed:
- .NET 8 SDK or later — download here
- Azure Functions Core Tools v4 — for local development and testing
- Visual Studio 2022 (with Azure workload) or VS Code with the Azure Functions extension
- An Azure account — free tier is sufficient for this tutorial
Install Azure Functions Core Tools using npm:
// Run this in your terminal (not C#, but required for setup)
// npm install -g azure-functions-core-tools@4 --unsafe-perm true
Step 1 — Create a New Azure Functions C# Project
Open your terminal and run the following commands to scaffold a new Azure Functions project using the .NET CLI:
// Terminal commands:
// dotnet new func --name ProductApi --worker-runtime dotnet-isolated
// cd ProductApi
This creates a project with the isolated worker model. The project structure looks like this:
- Program.cs — The application entry point where you configure services and middleware.
- host.json — Runtime configuration for your function app.
- local.settings.json — Local environment variables and connection strings (never commit this to source control).
Open Program.cs and you will see the host builder pattern that .NET developers are already familiar with:
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
})
.Build();
host.Run();
This is identical to how you configure a standard ASP.NET Core application. You can register services, add middleware, and configure logging exactly as you would in any .NET project.
Step 2 — Build an HTTP Trigger Azure Function
The Azure Functions HTTP trigger is the most common starting point. It turns your function into a REST API endpoint. Create a new file called ProductFunction.cs:
using System.Net;
using Microsoft.Azure.Functions.Worker;
using Microsoft.Azure.Functions.Worker.Http;
using Microsoft.Extensions.Logging;
namespace ProductApi;
public class ProductFunction
{
private readonly ILogger<ProductFunction> _logger;
public ProductFunction(ILogger<ProductFunction> logger)
{
_logger = logger;
}
[Function("GetProducts")]
public async Task<HttpResponseData> GetProducts(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "products")]
HttpRequestData req)
{
_logger.LogInformation("Fetching all products");
var products = new List<Product>
{
new(1, "Mechanical Keyboard", 89.99m, "Electronics"),
new(2, "USB-C Hub", 45.50m, "Accessories"),
new(3, "Monitor Stand", 34.99m, "Office")
};
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(products);
return response;
}
[Function("GetProductById")]
public async Task<HttpResponseData> GetProductById(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "products/{id:int}")]
HttpRequestData req,
int id)
{
_logger.LogInformation("Fetching product with ID: {Id}", id);
var product = id switch
{
1 => new Product(1, "Mechanical Keyboard", 89.99m, "Electronics"),
2 => new Product(2, "USB-C Hub", 45.50m, "Accessories"),
3 => new Product(3, "Monitor Stand", 34.99m, "Office"),
_ => null
};
if (product is null)
{
var notFound = req.CreateResponse(HttpStatusCode.NotFound);
await notFound.WriteAsJsonAsync(new { message = $"Product {id} not found" });
return notFound;
}
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(product);
return response;
}
}
public record Product(int Id, string Name, decimal Price, string Category);
Notice several important details in this code:
- Constructor injection works exactly like ASP.NET Core. The
ILoggeris injected automatically. - The
[Function]attribute gives each function a unique name used for routing and monitoring. - The
[HttpTrigger]attribute defines the HTTP method, authorization level, and route template. - Route parameters like
{id:int}work the same way as in ASP.NET Core controllers. - The
recordtype keeps the data model clean and immutable.
Step 3 — Add Dependency Injection and a Service Layer
Real-world serverless C# applications need proper architecture. Putting business logic directly in your function is the number one mistake beginners make. Here is how to structure your code with dependency injection:
// IProductService.cs
public interface IProductService
{
Task<IEnumerable<Product>> GetAllAsync();
Task<Product?> GetByIdAsync(int id);
Task<Product> CreateAsync(CreateProductRequest request);
}
// ProductService.cs
public class ProductService : IProductService
{
private static readonly List<Product> _products = new()
{
new(1, "Mechanical Keyboard", 89.99m, "Electronics"),
new(2, "USB-C Hub", 45.50m, "Accessories"),
new(3, "Monitor Stand", 34.99m, "Office")
};
private static int _nextId = 4;
public Task<IEnumerable<Product>> GetAllAsync()
{
return Task.FromResult<IEnumerable<Product>>(_products);
}
public Task<Product?> GetByIdAsync(int id)
{
var product = _products.FirstOrDefault(p => p.Id == id);
return Task.FromResult(product);
}
public Task<Product> CreateAsync(CreateProductRequest request)
{
var product = new Product(_nextId++, request.Name, request.Price, request.Category);
_products.Add(product);
return Task.FromResult(product);
}
}
public record CreateProductRequest(string Name, decimal Price, string Category);
Register the service in Program.cs:
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices(services =>
{
services.AddApplicationInsightsTelemetryWorkerService();
services.ConfigureFunctionsApplicationInsights();
services.AddSingleton<IProductService, ProductService>();
})
.Build();
host.Run();
Now update your function to use the service:
public class ProductFunction
{
private readonly ILogger<ProductFunction> _logger;
private readonly IProductService _productService;
public ProductFunction(ILogger<ProductFunction> logger, IProductService productService)
{
_logger = logger;
_productService = productService;
}
[Function("GetProducts")]
public async Task<HttpResponseData> GetProducts(
[HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "products")]
HttpRequestData req)
{
var products = await _productService.GetAllAsync();
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteAsJsonAsync(products);
return response;
}
[Function("CreateProduct")]
public async Task<HttpResponseData> CreateProduct(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "products")]
HttpRequestData req)
{
var request = await req.ReadFromJsonAsync<CreateProductRequest>();
if (request is null)
{
return req.CreateResponse(HttpStatusCode.BadRequest);
}
var product = await _productService.CreateAsync(request);
var response = req.CreateResponse(HttpStatusCode.Created);
await response.WriteAsJsonAsync(product);
return response;
}
}
Notice that the POST endpoint uses AuthorizationLevel.Function instead of Anonymous. This requires an API key to call, which is a good security practice for write operations.
Step 4 — Add a Timer Trigger Function
Azure Functions excel at scheduled background tasks. Here is a timer trigger that runs every hour — perfect for data cleanup, report generation, or cache invalidation:
public class CleanupFunction
{
private readonly ILogger<CleanupFunction> _logger;
public CleanupFunction(ILogger<CleanupFunction> logger)
{
_logger = logger;
}
[Function("DailyCleanup")]
public Task Run(
[TimerTrigger("0 0 * * * *")] TimerInfo timerInfo)
{
_logger.LogInformation("Cleanup function executed at: {Time}", DateTime.UtcNow);
if (timerInfo.IsPastDue)
{
_logger.LogWarning("Timer is running late — a past due execution was detected");
}
// Perform cleanup logic here
_logger.LogInformation("Next scheduled run: {Next}", timerInfo.ScheduleStatus?.Next);
return Task.CompletedTask;
}
}
The CRON expression "0 0 * * * *" means "at second 0 of minute 0 of every hour." Azure Functions uses six-field CRON expressions (with seconds), unlike the standard five-field format used by Linux cron.
Step 5 — Test Your Azure Functions Locally
Run the project locally using the Azure Functions Core Tools:
// Terminal command:
// func start
You will see output showing your function endpoints:
// Functions:
// GetProducts: [GET] http://localhost:7071/api/products
// GetProductById: [GET] http://localhost:7071/api/products/{id}
// CreateProduct: [POST] http://localhost:7071/api/products
// DailyCleanup: Timer trigger
Test the GET endpoint by opening http://localhost:7071/api/products in your browser or using curl. You should see the JSON array of products.
Step 6 — Deploy Azure Functions to the Cloud
There are multiple ways to deploy Azure Functions. Here is the fastest approach using the Azure CLI:
// Step 1: Login to Azure
// az login
// Step 2: Create a resource group
// az group create --name ProductApiRG --location eastus
// Step 3: Create a storage account (required for Azure Functions)
// az storage account create --name productapistorage --location eastus \
// --resource-group ProductApiRG --sku Standard_LRS
// Step 4: Create the function app
// az functionapp create --resource-group ProductApiRG \
// --consumption-plan-location eastus \
// --runtime dotnet-isolated --runtime-version 8 \
// --functions-version 4 \
// --name ProductApiFunc \
// --storage-account productapistorage
// Step 5: Publish your code
// func azure functionapp publish ProductApiFunc
After deployment, Azure gives you a URL like https://productapifunc.azurewebsites.net/api/products. Your serverless API is now live and auto-scaling.
Deploy Using Visual Studio
If you prefer a GUI workflow: right-click the project in Solution Explorer, select Publish, choose Azure as the target, select Azure Function App (Windows), and follow the wizard. Visual Studio handles resource creation and deployment in one step.
Common Pitfalls and Best Practices
After building dozens of C# serverless applications, here are the mistakes I see most often and how to avoid them:
1. Cold Start Performance
On the Consumption plan, functions that have been idle get deallocated. The first request after idle time triggers a "cold start" that can take several seconds. Mitigations:
- Use the Premium plan with pre-warmed instances for latency-sensitive APIs.
- Keep your deployment package small — remove unused NuGet packages.
- Avoid heavy initialization in constructors. Use lazy loading for expensive resources.
2. Connection Management
Never create HttpClient instances inside your function method. This exhausts socket connections under load. Instead, use IHttpClientFactory:
// In Program.cs
services.AddHttpClient("ExternalApi", client =>
{
client.BaseAddress = new Uri("https://api.example.com");
client.Timeout = TimeSpan.FromSeconds(30);
});
// In your function
public class ExternalApiFunction
{
private readonly IHttpClientFactory _httpClientFactory;
public ExternalApiFunction(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
[Function("CallExternalApi")]
public async Task<HttpResponseData> Run(
[HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequestData req)
{
var client = _httpClientFactory.CreateClient("ExternalApi");
var result = await client.GetStringAsync("/data");
var response = req.CreateResponse(HttpStatusCode.OK);
await response.WriteStringAsync(result);
return response;
}
}
3. Configuration and Secrets
Never hardcode connection strings or API keys. Use Azure App Settings for configuration and Azure Key Vault for secrets:
// Access configuration through standard .NET patterns
services.AddOptions<ApiSettings>()
.Configure<IConfiguration>((settings, config) =>
{
config.GetSection("ApiSettings").Bind(settings);
});
public class ApiSettings
{
public string ApiKey { get; set; } = string.Empty;
public string BaseUrl { get; set; } = string.Empty;
}
4. Idempotency
Azure Functions may execute your function more than once for the same event (at-least-once delivery). Design your functions to be idempotent — processing the same input twice should produce the same result without side effects.
5. Function Timeout
Functions on the Consumption plan have a maximum execution time of 10 minutes (configurable up to 10 minutes in host.json). For long-running work, use Durable Functions which can orchestrate multi-step workflows that run for hours or days.
Azure Functions vs AWS Lambda vs Google Cloud Functions
If you are evaluating serverless platforms, here is how they compare for C# developers:
- Azure Functions — Best .NET integration, native Visual Studio support, broadest trigger ecosystem. First choice for teams already using Azure or .NET.
- AWS Lambda — Supports .NET but the tooling is less polished. Better if your infrastructure is already on AWS.
- Google Cloud Functions — Limited .NET support. Not recommended for C# workloads.
For C# developers, Azure Functions is the clear winner in developer experience, tooling, and trigger variety.
When to Use Azure Functions (and When Not To)
Use Azure Functions when:
- You need event-driven processing (webhooks, queue consumers, file uploads).
- Your workload is bursty — high traffic at some times, zero at others.
- You want to build microservices without managing containers.
- You need scheduled background jobs (timer triggers replace Windows Task Scheduler and Hangfire).
Consider alternatives when:
- You need persistent WebSocket connections — use Azure SignalR Service instead.
- Your function runs longer than 10 minutes — use Durable Functions or Azure Container Apps.
- You need full control over the runtime environment — use Azure App Service or AKS.
Conclusion — Azure Functions C# for Serverless Development
Building serverless applications with Azure Functions C# eliminates infrastructure overhead and lets you focus entirely on business logic. In this tutorial, you learned how to create HTTP and timer triggers, structure your code with dependency injection, test locally, and deploy to the cloud.
Key takeaways:
- Use the isolated worker model with .NET 8 for all new Azure Functions projects.
- Structure your code with dependency injection and service layers — do not put business logic in function methods.
- Always use IHttpClientFactory for outbound HTTP calls to avoid socket exhaustion.
- Design functions to be idempotent because Azure may execute them more than once.
- Start with the Consumption plan for cost savings, then move to Premium if cold starts become a problem.
Start with the HTTP trigger example above, swap in your own data source, and you will have a production-ready serverless API in under an hour. Azure Functions is one of the most practical tools in a C# developer's toolkit — now you know how to use it.
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.
Comments
Post a Comment