What bindings are (and why they matter)
Bindings are declarative connectors that let a function read from or write to Azure services without you writing the “plumbing” code (client creation, serialization, endpoint wiring, and basic error handling). You describe what you want to connect to (for example, a blob path or a queue name), and the Functions runtime handles the integration at execution time.
Bindings are most valuable when your function logic is simple and the integration pattern is standard: read an input document, transform it, write an output document, enqueue a message, or return an HTTP response. They reduce boilerplate and make the code focus on business logic.
Trigger vs binding (and input vs output)
- Trigger: defines how the function starts (the event source). There is exactly one trigger per function.
- Binding: additional inputs/outputs connected to services. A function can have multiple bindings.
- Input binding: data is provided to your function (for example, a blob’s content, a Cosmos DB document, a queue message).
- Output binding: data is written by your function (for example, a new blob, a Cosmos DB upsert, a queue message).
In practice, you often combine an event trigger with one or more bindings. For example: an HTTP trigger plus an output binding to Blob Storage; or a queue trigger plus an output binding to Cosmos DB.
How bindings are configured
Bindings are configured through attributes in code (C#) or through function.json (for some languages/runtimes). The configuration typically includes:
- Binding type (Blob, Cosmos DB, Queue, Service Bus, etc.).
- Direction (
inorout). - Target (container name, queue name, topic/subscription, database/container, etc.).
- Connection setting: the name of an app setting that holds the connection string or identity configuration.
- Binding expressions: placeholders that are resolved at runtime (for example, values from the trigger payload or route parameters).
Keep connection settings in application settings (local settings for development, app settings in Azure). Avoid hardcoding secrets in code. Prefer managed identity where supported by the binding and your hosting plan/runtime.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
Example 1: Write to Blob Storage with an output binding
This pattern is ideal when you receive data (often via HTTP) and want to persist it as a blob without manually creating a Blob client.
Step-by-step
- Choose a blob naming strategy (for example, by date and a generated ID).
- Define an output binding to the target container and blob path.
- Write your payload to the bound output stream or output value.
- Return an HTTP response that includes the blob location or an ID.
C# example (HTTP trigger + Blob output)
using System.Net;using System.Text.Json;using Microsoft.Azure.Functions.Worker;using Microsoft.Azure.Functions.Worker.Http;using Microsoft.Extensions.Logging;public class UploadOrder{ private readonly ILogger _logger; public UploadOrder(ILoggerFactory loggerFactory) => _logger = loggerFactory.CreateLogger<UploadOrder>(); public record Order(string orderId, string customerId, decimal total); [Function("UploadOrder")] public async Task<HttpResponseData> Run( [HttpTrigger(AuthorizationLevel.Function, "post", Route = "orders")] HttpRequestData req, [BlobOutput("orders/{rand-guid}.json", Connection = "StorageConnection")] IAsyncCollector<string> blobOut) { var body = await req.ReadAsStringAsync(); Order? order; try { order = JsonSerializer.Deserialize<Order>(body, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } catch (JsonException) { var bad = req.CreateResponse(HttpStatusCode.BadRequest); await bad.WriteStringAsync("Invalid JSON."); return bad; } if (order is null || string.IsNullOrWhiteSpace(order.orderId) || order.total <= 0) { var bad = req.CreateResponse(HttpStatusCode.BadRequest); await bad.WriteStringAsync("Missing/invalid fields: orderId, total."); return bad; } await blobOut.AddAsync(JsonSerializer.Serialize(order)); var ok = req.CreateResponse(HttpStatusCode.Accepted); await ok.WriteStringAsync("Order accepted and stored."); return ok; }}Notes on configuration: StorageConnection is an app setting containing the storage connection string (or identity-based configuration if supported for your environment). The blob path uses {rand-guid} to avoid collisions.
When to prefer SDK instead: if you need conditional writes (ETags), block blob staging/commits, customer-provided keys, or advanced retry/timeout policies beyond what the binding offers.
Example 2: Update (upsert) Cosmos DB with an output binding
Cosmos DB bindings are great for straightforward create/update (upsert) operations where the document shape is known and you don’t need complex queries or transactional batches.
Step-by-step
- Define a document contract that matches your container’s partition key strategy.
- Validate required fields, especially
idand partition key. - Return a document to the output binding for upsert.
C# example (HTTP trigger + Cosmos DB output)
using System.Net;using System.Text.Json;using Microsoft.Azure.Functions.Worker;using Microsoft.Azure.Functions.Worker.Http;public class UpsertCustomer{ public record CustomerDoc(string id, string tenantId, string name, string email); [Function("UpsertCustomer")] public async Task<HttpResponseData> Run( [HttpTrigger(AuthorizationLevel.Function, "put", Route = "tenants/{tenantId}/customers/{id}")] HttpRequestData req, string tenantId, string id, [CosmosDBOutput( databaseName: "appdb", containerName: "customers", Connection = "CosmosConnection")] IAsyncCollector<CustomerDoc> customersOut) { var body = await req.ReadAsStringAsync(); CustomerDoc? incoming; try { incoming = JsonSerializer.Deserialize<CustomerDoc>(body, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); } catch (JsonException) { var bad = req.CreateResponse(HttpStatusCode.BadRequest); await bad.WriteStringAsync("Invalid JSON."); return bad; } if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(tenantId)) { var bad = req.CreateResponse(HttpStatusCode.BadRequest); await bad.WriteStringAsync("Route parameters are required."); return bad; } if (incoming is null || string.IsNullOrWhiteSpace(incoming.name) || string.IsNullOrWhiteSpace(incoming.email)) { var bad = req.CreateResponse(HttpStatusCode.BadRequest); await bad.WriteStringAsync("Missing required fields: name, email."); return bad; } var doc = new CustomerDoc(id: id, tenantId: tenantId, name: incoming.name, email: incoming.email); await customersOut.AddAsync(doc); var ok = req.CreateResponse(HttpStatusCode.OK); await ok.WriteStringAsync("Customer upserted."); return ok; }}Data contract guidance: Cosmos DB documents must include an id. Your partition key (for example, tenantId) should be present and stable. Keep the function’s contract aligned with the container’s indexing and partitioning decisions.
When to prefer SDK instead: if you need advanced queries, transactional batch, stored procedures, conditional updates, patch operations, custom consistency levels, or fine-grained control over RU consumption and retries.
Example 3: Enqueue messages with Storage Queues and Service Bus
Queue output bindings are ideal for decoupling: your function validates input and emits a message for downstream processing. This keeps the HTTP path fast and resilient.
Storage Queue output binding
Use Storage Queues for simple, cost-effective messaging. Messages are typically small and represent a work item to be processed later.
Step-by-step
- Define a message schema (JSON) with explicit fields and versioning.
- Validate and normalize inputs.
- Write a message to the queue via the output binding.
using System.Net;using System.Text.Json;using Microsoft.Azure.Functions.Worker;using Microsoft.Azure.Functions.Worker.Http;public class EnqueueWorkItem{ public record WorkItemMessage(string version, string workId, string tenantId, string action); [Function("EnqueueWorkItem")] public async Task<HttpResponseData> Run( [HttpTrigger(AuthorizationLevel.Function, "post", Route = "tenants/{tenantId}/work")] HttpRequestData req, string tenantId, [QueueOutput("work-items", Connection = "StorageConnection")] IAsyncCollector<WorkItemMessage> queueOut) { var body = await req.ReadAsStringAsync(); using var doc = JsonDocument.Parse(body); var action = doc.RootElement.TryGetProperty("action", out var a) ? a.GetString() : null; if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(action)) { var bad = req.CreateResponse(HttpStatusCode.BadRequest); await bad.WriteStringAsync("tenantId and action are required."); return bad; } var msg = new WorkItemMessage( version: "1", workId: Guid.NewGuid().ToString("N"), tenantId: tenantId, action: action); await queueOut.AddAsync(msg); var accepted = req.CreateResponse(HttpStatusCode.Accepted); await accepted.WriteStringAsync(msg.workId); return accepted; }}Message contract tip: include a version field so consumers can evolve safely. Keep messages small and include identifiers rather than entire large payloads (store large payloads in Blob Storage and send a reference).
Service Bus output binding
Use Service Bus when you need richer messaging features such as sessions, duplicate detection, topics/subscriptions, dead-lettering controls, and more predictable delivery semantics for enterprise workflows.
With Service Bus, you often want to set message metadata (correlation ID, session ID, custom properties). If your binding supports binding to a Service Bus message type, you can set those fields; otherwise, use the SDK for full control.
using System.Net;using System.Text.Json;using Azure.Messaging.ServiceBus;using Microsoft.Azure.Functions.Worker;using Microsoft.Azure.Functions.Worker.Http;public class PublishToTopic{ public record EventEnvelope(string type, string version, string tenantId, string id, object data); [Function("PublishToTopic")] public async Task<HttpResponseData> Run( [HttpTrigger(AuthorizationLevel.Function, "post", Route = "tenants/{tenantId}/events" )] HttpRequestData req, string tenantId, [ServiceBusOutput("events-topic", Connection = "ServiceBusConnection")] IAsyncCollector<ServiceBusMessage> sbOut) { var body = await req.ReadAsStringAsync(); if (string.IsNullOrWhiteSpace(tenantId) || string.IsNullOrWhiteSpace(body)) { var bad = req.CreateResponse(HttpStatusCode.BadRequest); await bad.WriteStringAsync("tenantId and body are required."); return bad; } var envelope = new EventEnvelope( type: "TenantEvent", version: "1", tenantId: tenantId, id: Guid.NewGuid().ToString("N"), data: JsonSerializer.Deserialize<object>(body) ?? new { }); var msg = new ServiceBusMessage(JsonSerializer.Serialize(envelope)); msg.ContentType = "application/json"; msg.CorrelationId = envelope.id; msg.ApplicationProperties["tenantId"] = tenantId; await sbOut.AddAsync(msg); var accepted = req.CreateResponse(HttpStatusCode.Accepted); await accepted.WriteStringAsync(envelope.id); return accepted; }}When to prefer SDK instead: if you need advanced features like transactions across multiple sends, scheduled messages, session management with strict rules, fine-tuned retry policies, or custom settlement patterns.
Example 4: Returning HTTP responses as an output binding
HTTP-triggered functions always return an HTTP response, but you can treat the response as an output binding conceptually: your function produces a response object and the runtime writes it to the client. The key is to keep response shaping consistent and explicit.
Step-by-step response shaping
- Define a response envelope (for example,
{ data, error, traceId }). - Return appropriate status codes for validation errors vs server errors.
- Set content type and include identifiers (request ID, correlation ID) for troubleshooting.
using System.Net;using System.Text.Json;using Microsoft.Azure.Functions.Worker;using Microsoft.Azure.Functions.Worker.Http;public class GetHealth{ [Function("GetHealth")] public async Task<HttpResponseData> Run( [HttpTrigger(AuthorizationLevel.Anonymous, "get", Route = "health")] HttpRequestData req) { var res = req.CreateResponse(HttpStatusCode.OK); res.Headers.Add("Content-Type", "application/json"); var payload = new { status = "ok" }; await res.WriteStringAsync(JsonSerializer.Serialize(payload)); return res; }}This pattern becomes more important when you combine HTTP with other output bindings: you can return 202 Accepted after enqueueing a message, or 201 Created after writing a blob, while still letting bindings handle the side effects.
Choosing bindings vs using SDKs directly
Bindings are ideal when
- You are doing straightforward reads/writes with minimal customization.
- You want to reduce boilerplate and keep functions small.
- Your data access pattern is simple (single document upsert, single message send, single blob write).
- You can accept the runtime’s default retry and serialization behavior.
Use SDKs directly when
- Complex transactions or multi-step workflows: for example, Cosmos DB transactional batch, or coordinated writes across multiple resources.
- Advanced queries and projections: complex Cosmos DB queries, pagination, continuation tokens, or custom indexing hints.
- Custom retry/timeout behavior: you need per-operation policies, circuit breakers, or differentiated handling for transient vs permanent failures.
- Fine-grained control over request options: ETags, conditional operations, consistency levels, encryption settings, message scheduling, sessions, or dead-letter management.
- Performance tuning: connection reuse, bulk operations, streaming uploads, or explicit batching.
A practical hybrid approach is common: use bindings for the simple path, and introduce SDK usage only where needed. Keep the decision local to the function’s responsibilities rather than enforcing a single approach everywhere.
Shaping data contracts for reliable bindings
Bindings work best when your inputs and outputs have stable, explicit contracts. Treat the payloads you read/write as APIs between components.
Design principles
- Explicit schemas: define DTOs/records for messages and documents rather than passing anonymous objects.
- Versioning: include a
versionfield in messages/events; evolve by adding optional fields. - Identifiers over payloads: store large content in Blob Storage and send references (blob path, URL, or ID) in queue/topic messages.
- Partition-aware documents: for Cosmos DB, ensure the partition key field is present and validated.
- Deterministic naming: for blobs, use predictable paths (tenant/date/id) to simplify lifecycle management and debugging.
Validation guidance (inputs and outputs)
- Validate early: reject invalid HTTP requests before writing to any output binding.
- Validate required fields: IDs, partition keys, and routing fields should be non-empty and normalized.
- Validate size constraints: queue messages have size limits; enforce maximum payload sizes and move large data to blobs.
- Validate serialization: ensure your DTOs serialize to the expected JSON shape; avoid ambiguous property names and consider explicit JSON property naming if needed.
- Handle partial failures intentionally: if you have multiple outputs (for example, blob + queue), decide whether you can tolerate one succeeding without the other. If not, consider an SDK-based transactional pattern or redesign to an outbox-like approach.
Binding expressions and contract alignment
Binding expressions (placeholders in paths/names) should map to stable fields. For example, if you use "orders/{tenantId}/{orderId}.json", ensure tenantId and orderId are always available from route parameters or the trigger payload, and validate them before writing outputs. Avoid expressions that depend on optional fields unless you provide defaults.