Azure Functions in ConnectSoft Microservice Template¶
Purpose & Overview¶
Azure Functions is a serverless compute service that enables you to run code on-demand without managing infrastructure. In the ConnectSoft Microservice Template, Azure Functions can be implemented as an alternative service model, allowing you to expose microservice functionality through event-driven, serverless functions that integrate with the template's Clean Architecture and CQRS patterns.
Why Azure Functions?¶
Azure Functions offers several advantages for microservices:
- Serverless: No infrastructure management, automatic scaling
- Event-Driven: Respond to HTTP requests, timers, queues, events, and more
- Cost-Effective: Pay only for execution time
- Integration: Native integration with Azure services (Storage, Service Bus, Event Grid, etc.)
- Scalability: Automatically scales to handle varying workloads
- Multiple Languages: Support for C#, JavaScript, Python, and more
- Flexibility: Support for isolated and in-process hosting models
Architecture Overview¶
Azure Functions integrate with the template's architecture:
Azure Function Triggers
├── HTTP Trigger (REST API)
├── Queue Trigger (Message Processing)
├── Timer Trigger (Scheduled Tasks)
├── Blob Trigger (File Processing)
└── Event Grid Trigger (Event Processing)
↓
Azure Function (ServiceModel.AzureFunction)
├── Function Entry Point
├── Request/Response DTOs
└── Dependency Injection
↓
Domain Model (DomainModel)
├── Processors (Commands/Writes)
└── Retrievers (Queries/Reads)
↓
Repository Layer (PersistenceModel)
└── Data Store
Implementation Approaches¶
Isolated Worker Process Model (Recommended)¶
The isolated worker process model provides better isolation and compatibility:
// Program.cs for Azure Functions
using Microsoft.Azure.Functions.Worker;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
var host = new HostBuilder()
.ConfigureFunctionsWebApplication()
.ConfigureServices(services =>
{
// Register domain services
services.AddMicroserviceDomainModel();
// Register repositories
services.AddMicroservicePersistenceModel();
// Register AutoMapper
services.AddAutoMapper(typeof(Program));
// Register other services
})
.Build();
host.Run();
HTTP-Triggered Functions¶
// MicroserviceAggregateRootsFunctions.cs
public class MicroserviceAggregateRootsFunctions
{
private readonly ILogger<MicroserviceAggregateRootsFunctions> logger;
private readonly IMicroserviceAggregateRootsProcessor processor;
private readonly IMicroserviceAggregateRootsRetriever retriever;
private readonly IMapper mapper;
public MicroserviceAggregateRootsFunctions(
ILogger<MicroserviceAggregateRootsFunctions> logger,
IMicroserviceAggregateRootsProcessor processor,
IMicroserviceAggregateRootsRetriever retriever,
IMapper mapper)
{
this.logger = logger;
this.processor = processor;
this.retriever = retriever;
this.mapper = mapper;
}
/// <summary>
/// HTTP POST function to create a MicroserviceAggregateRoot.
/// </summary>
[Function("CreateMicroserviceAggregateRoot")]
public async Task<HttpResponseData> CreateMicroserviceAggregateRoot(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "microserviceaggregateroots")] HttpRequestData req,
FunctionContext executionContext)
{
this.logger.LogInformation("CreateMicroserviceAggregateRoot function triggered");
try
{
// Deserialize request
var request = await req.ReadFromJsonAsync<CreateMicroserviceAggregateRootRequest>();
if (request == null)
{
var badResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badResponse.WriteAsJsonAsync(new { error = "Invalid request body" });
return badResponse;
}
// Map to domain input
var input = this.mapper.Map<CreateMicroserviceAggregateRootInput>(request);
// Execute domain service
var entity = await this.processor.CreateMicroserviceAggregateRoot(
input,
executionContext.CancellationToken);
// Map to response
var response = this.mapper.Map<CreateMicroserviceAggregateRootResponse>(entity);
// Return success response
var httpResponse = req.CreateResponse(HttpStatusCode.Created);
await httpResponse.WriteAsJsonAsync(response);
return httpResponse;
}
catch (ValidationException ex)
{
this.logger.LogWarning(ex, "Validation error in CreateMicroserviceAggregateRoot");
var errorResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await errorResponse.WriteAsJsonAsync(new { error = ex.Message });
return errorResponse;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error in CreateMicroserviceAggregateRoot");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteAsJsonAsync(new { error = "An internal error occurred" });
return errorResponse;
}
}
/// <summary>
/// HTTP GET function to retrieve a MicroserviceAggregateRoot.
/// </summary>
[Function("GetMicroserviceAggregateRoot")]
public async Task<HttpResponseData> GetMicroserviceAggregateRoot(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "microserviceaggregateroots/{id}")]
HttpRequestData req,
string id,
FunctionContext executionContext)
{
this.logger.LogInformation("GetMicroserviceAggregateRoot function triggered for ID: {Id}", id);
try
{
if (!Guid.TryParse(id, out var objectId))
{
var badResponse = req.CreateResponse(HttpStatusCode.BadRequest);
await badResponse.WriteAsJsonAsync(new { error = "Invalid ID format" });
return badResponse;
}
// Map to domain input
var input = new GetMicroserviceAggregateRootDetailsInput { ObjectId = objectId };
// Execute domain service
var entity = await this.retriever.GetMicroserviceAggregateRootDetails(
input,
executionContext.CancellationToken);
if (entity == null)
{
var notFoundResponse = req.CreateResponse(HttpStatusCode.NotFound);
await notFoundResponse.WriteAsJsonAsync(new { error = "Resource not found" });
return notFoundResponse;
}
// Map to response
var response = this.mapper.Map<GetMicroserviceAggregateRootDetailsResponse>(entity);
// Return success response
var httpResponse = req.CreateResponse(HttpStatusCode.OK);
await httpResponse.WriteAsJsonAsync(response);
return httpResponse;
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error in GetMicroserviceAggregateRoot");
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
await errorResponse.WriteAsJsonAsync(new { error = "An internal error occurred" });
return errorResponse;
}
}
}
Queue-Triggered Functions¶
// QueueProcessingFunctions.cs
public class QueueProcessingFunctions
{
private readonly ILogger<QueueProcessingFunctions> logger;
private readonly IMicroserviceAggregateRootsProcessor processor;
private readonly IMapper mapper;
public QueueProcessingFunctions(
ILogger<QueueProcessingFunctions> logger,
IMicroserviceAggregateRootsProcessor processor,
IMapper mapper)
{
this.logger = logger;
this.processor = processor;
this.mapper = mapper;
}
/// <summary>
/// Process messages from Azure Service Bus queue.
/// </summary>
[Function("ProcessMicroserviceAggregateRootCommand")]
public async Task ProcessMicroserviceAggregateRootCommand(
[ServiceBusTrigger("microservice-commands", Connection = "ServiceBusConnection")]
string message,
FunctionContext executionContext)
{
this.logger.LogInformation("Processing command from queue: {Message}", message);
try
{
// Deserialize command
var command = JsonSerializer.Deserialize<CreateMicroserviceAggregateRootCommand>(message);
if (command == null)
{
this.logger.LogWarning("Invalid command format: {Message}", message);
throw new InvalidOperationException("Invalid command format");
}
// Map to domain input
var input = this.mapper.Map<CreateMicroserviceAggregateRootInput>(command);
// Execute domain service
await this.processor.CreateMicroserviceAggregateRoot(
input,
executionContext.CancellationToken);
this.logger.LogInformation("Command processed successfully: {CommandId}", command.ObjectId);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error processing command: {Message}", message);
throw; // Azure Functions will handle retry logic
}
}
/// <summary>
/// Process blob uploads.
/// </summary>
[Function("ProcessBlobUpload")]
public async Task ProcessBlobUpload(
[BlobTrigger("uploads/{name}", Connection = "StorageConnection")]
Stream blobStream,
string name,
FunctionContext executionContext)
{
this.logger.LogInformation("Processing blob upload: {BlobName}", name);
try
{
// Process blob content
// This could involve parsing, validation, and storing results
using var reader = new StreamReader(blobStream);
var content = await reader.ReadToEndAsync();
// Execute domain logic based on blob content
// ...
this.logger.LogInformation("Blob processed successfully: {BlobName}", name);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error processing blob: {BlobName}", name);
throw;
}
}
}
Timer-Triggered Functions¶
// ScheduledFunctions.cs
public class ScheduledFunctions
{
private readonly ILogger<ScheduledFunctions> logger;
private readonly IMicroserviceAggregateRootsProcessor processor;
public ScheduledFunctions(
ILogger<ScheduledFunctions> logger,
IMicroserviceAggregateRootsProcessor processor)
{
this.logger = logger;
this.processor = processor;
}
/// <summary>
/// Scheduled function that runs every hour.
/// </summary>
[Function("ProcessScheduledTasks")]
public async Task ProcessScheduledTasks(
[TimerTrigger("0 0 * * * *")] // Every hour at minute 0
TimerInfo timerInfo,
FunctionContext executionContext)
{
this.logger.LogInformation("Scheduled task triggered at: {ScheduleStatus}", timerInfo.ScheduleStatus);
try
{
// Perform scheduled operations
// This could involve cleanup, data aggregation, reporting, etc.
this.logger.LogInformation("Scheduled task completed successfully");
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error in scheduled task");
throw;
}
}
}
Event Grid Triggered Functions¶
// EventGridFunctions.cs
public class EventGridFunctions
{
private readonly ILogger<EventGridFunctions> logger;
private readonly IMicroserviceAggregateRootsProcessor processor;
public EventGridFunctions(
ILogger<EventGridFunctions> logger,
IMicroserviceAggregateRootsProcessor processor)
{
this.logger = logger;
this.processor = processor;
}
/// <summary>
/// Process Event Grid events.
/// </summary>
[Function("ProcessEventGridEvent")]
public async Task ProcessEventGridEvent(
[EventGridTrigger] EventGridEvent eventGridEvent,
FunctionContext executionContext)
{
this.logger.LogInformation(
"Event Grid event received: {EventType} - {Subject}",
eventGridEvent.EventType,
eventGridEvent.Subject);
try
{
// Process event based on event type
switch (eventGridEvent.EventType)
{
case "Microsoft.Storage.BlobCreated":
// Handle blob created event
await this.HandleBlobCreatedEvent(eventGridEvent, executionContext.CancellationToken);
break;
case "Custom.DomainEvent":
// Handle custom domain event
await this.HandleCustomDomainEvent(eventGridEvent, executionContext.CancellationToken);
break;
default:
this.logger.LogWarning("Unknown event type: {EventType}", eventGridEvent.EventType);
break;
}
this.logger.LogInformation("Event processed successfully: {EventId}", eventGridEvent.Id);
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error processing Event Grid event: {EventId}", eventGridEvent.Id);
throw;
}
}
private async Task HandleBlobCreatedEvent(EventGridEvent eventGridEvent, CancellationToken cancellationToken)
{
// Extract blob information from event data
var blobData = JsonSerializer.Deserialize<BlobCreatedEventData>(eventGridEvent.Data.ToString());
// Process blob creation...
}
private async Task HandleCustomDomainEvent(EventGridEvent eventGridEvent, CancellationToken cancellationToken)
{
// Handle custom domain event
var domainEvent = JsonSerializer.Deserialize<MicroserviceAggregateRootCreatedEvent>(
eventGridEvent.Data.ToString());
// Process domain event...
}
}
Integration with CQRS¶
Azure Functions integrate with CQRS the same way as REST API:
// Query Function (Read Operation)
[Function("GetMicroserviceAggregateRoot")]
public async Task<HttpResponseData> GetMicroserviceAggregateRoot(
[HttpTrigger(AuthorizationLevel.Function, "get", Route = "microserviceaggregateroots/{id}")]
HttpRequestData req,
string id,
FunctionContext executionContext)
{
var input = new GetMicroserviceAggregateRootDetailsInput { ObjectId = Guid.Parse(id) };
// Use Retriever (query side)
var entity = await this.retriever.GetMicroserviceAggregateRootDetails(input, executionContext.CancellationToken);
var response = this.mapper.Map<GetMicroserviceAggregateRootDetailsResponse>(entity);
var httpResponse = req.CreateResponse(HttpStatusCode.OK);
await httpResponse.WriteAsJsonAsync(response);
return httpResponse;
}
// Command Function (Write Operation)
[Function("CreateMicroserviceAggregateRoot")]
public async Task<HttpResponseData> CreateMicroserviceAggregateRoot(
[HttpTrigger(AuthorizationLevel.Function, "post", Route = "microserviceaggregateroots")]
HttpRequestData req,
FunctionContext executionContext)
{
var request = await req.ReadFromJsonAsync<CreateMicroserviceAggregateRootRequest>();
var input = this.mapper.Map<CreateMicroserviceAggregateRootInput>(request);
// Use Processor (command side)
var entity = await this.processor.CreateMicroserviceAggregateRoot(input, executionContext.CancellationToken);
var response = this.mapper.Map<CreateMicroserviceAggregateRootResponse>(entity);
var httpResponse = req.CreateResponse(HttpStatusCode.Created);
await httpResponse.WriteAsJsonAsync(response);
return httpResponse;
}
Configuration¶
Function App Configuration¶
{
"AzureFunctionsJobHost": {
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true,
"maxTelemetryItemsPerSecond": 20
}
}
},
"extensions": {
"http": {
"routePrefix": "api",
"maxOutstandingRequests": 200,
"maxConcurrentRequests": 100,
"dynamicThrottlesEnabled": true
},
"serviceBus": {
"prefetchCount": 0,
"messageHandlerOptions": {
"autoComplete": true,
"maxConcurrentCalls": 16,
"maxAutoRenewDuration": "00:05:00"
}
}
}
},
"ConnectionStrings": {
"ServiceBusConnection": "Endpoint=sb://...",
"StorageConnection": "DefaultEndpointsProtocol=https;..."
},
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated"
}
}
host.json¶
{
"version": "2.0",
"logging": {
"applicationInsights": {
"samplingSettings": {
"isEnabled": true
}
}
},
"extensionBundle": {
"id": "Microsoft.Azure.Functions.ExtensionBundle",
"version": "[3.*, 4.0.0)"
}
}
Best Practices¶
Do's¶
- Use dependency injection
- Register domain services in
ConfigureServices - Use constructor injection for dependencies
-
Leverage Azure Functions dependency injection
-
Handle errors appropriately
- Catch and log exceptions
- Return appropriate HTTP status codes
-
Use retry policies for transient failures
-
Optimize cold starts
- Minimize dependencies
- Use pre-compiled functions
-
Consider connection pooling
-
Implement proper logging
- Use structured logging
- Log function entry and exit
-
Include correlation IDs
-
Use appropriate authorization levels
AuthorizationLevel.Functionfor function keysAuthorizationLevel.Adminfor master keys-
AuthorizationLevel.Anonymousonly when appropriate -
Integrate with domain services
- Use Processors for commands
- Use Retrievers for queries
-
Maintain architectural boundaries
-
Configure scaling appropriately
- Set concurrency limits
- Use host.json settings
- Monitor function performance
Don'ts¶
- Don't bypass domain services
- Always use Processors/Retrievers
- Don't access repositories directly
-
Maintain architectural boundaries
-
Don't ignore error handling
- Always handle exceptions
- Don't expose internal errors to clients
-
Implement proper retry logic
-
Don't hardcode configuration
- Use environment variables
- Use Azure Key Vault for secrets
-
Use connection strings properly
-
Don't create long-running functions
- Azure Functions have timeout limits
- Use Durable Functions for long-running workflows
- Consider breaking into smaller functions
Testing¶
Unit Testing Functions¶
[Fact]
public async Task CreateMicroserviceAggregateRoot_Should_Return_Created()
{
// Arrange
var mockProcessor = new Mock<IMicroserviceAggregateRootsProcessor>();
var mockMapper = new Mock<IMapper>();
var function = new MicroserviceAggregateRootsFunctions(
Mock.Of<ILogger<MicroserviceAggregateRootsFunctions>>(),
mockProcessor.Object,
Mock.Of<IMicroserviceAggregateRootsRetriever>(),
mockMapper.Object);
var request = new CreateMicroserviceAggregateRootRequest { ObjectId = Guid.NewGuid() };
var input = new CreateMicroserviceAggregateRootInput { ObjectId = request.ObjectId };
var entity = Mock.Of<IMicroserviceAggregateRoot>();
var response = new CreateMicroserviceAggregateRootResponse();
mockMapper.Setup(m => m.Map<CreateMicroserviceAggregateRootInput>(request))
.Returns(input);
mockProcessor.Setup(p => p.CreateMicroserviceAggregateRoot(input, It.IsAny<CancellationToken>()))
.ReturnsAsync(entity);
mockMapper.Setup(m => m.Map<CreateMicroserviceAggregateRootResponse>(entity))
.Returns(response);
// Create mock HTTP request
var context = new Mock<FunctionContext>();
var requestData = new Mock<HttpRequestData>(context.Object);
requestData.Setup(r => r.ReadFromJsonAsync<CreateMicroserviceAggregateRootRequest>(It.IsAny<JsonSerializerOptions>(), It.IsAny<CancellationToken>()))
.ReturnsAsync(request);
// Act
var result = await function.CreateMicroserviceAggregateRoot(requestData.Object, context.Object);
// Assert
Assert.Equal(HttpStatusCode.Created, result.StatusCode);
}
Summary¶
Azure Functions in the ConnectSoft Microservice Template provide:
- ✅ Serverless Architecture: No infrastructure management
- ✅ Multiple Trigger Types: HTTP, Queue, Timer, Blob, Event Grid
- ✅ CQRS Integration: Processors for commands, Retrievers for queries
- ✅ Scalability: Automatic scaling based on demand
- ✅ Cost-Effective: Pay only for execution time
- ✅ Azure Integration: Native integration with Azure services
- ✅ Flexibility: Support for isolated and in-process models
By following these patterns, Azure Functions become a powerful serverless service model that maintains architectural integrity while providing cost-effective, scalable execution.