Skip to main content

Azure Functions C# Tutorial: Build & Deploy Step by Step

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 ILogger is 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 record type 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.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Send an Email via SMTP with MailKit Using .NET 6

How to Send an Email in .NET Core This tutorial show you how to send an email in .NET 6.0 using the MailKit email client library. Install MailKit via NuGet Visual Studio Package Manager Console: Install-Package MailKit How to Send an HTML Email in .NET 6.0 This code sends a simple HTML email using the Gmail SMTP service. There are instructions further below on how to use a few other popular SMTP providers - Gmail, Hotmail, Office 365. // create email message var email = new MimeMessage(); email.From.Add(MailboxAddress.Parse("from_address@example.com")); email.To.Add(MailboxAddress.Parse("to_address@example.com")); email.Subject = "Email Subject"; email.Body = new TextPart(TextFormat.Html) { Text = "<h1>Test HTML Message Body</h1>" }; // send email using var smtp = new SmtpClient(); smtp.Connect("smtp.gmail.com", 587, SecureSocketOptions.StartTls); smtp.Authenticate("[Username]", "[Password]"); smtp.Se...