Skip to content

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

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

  1. Use dependency injection
  2. Register domain services in ConfigureServices
  3. Use constructor injection for dependencies
  4. Leverage Azure Functions dependency injection

  5. Handle errors appropriately

  6. Catch and log exceptions
  7. Return appropriate HTTP status codes
  8. Use retry policies for transient failures

  9. Optimize cold starts

  10. Minimize dependencies
  11. Use pre-compiled functions
  12. Consider connection pooling

  13. Implement proper logging

  14. Use structured logging
  15. Log function entry and exit
  16. Include correlation IDs

  17. Use appropriate authorization levels

  18. AuthorizationLevel.Function for function keys
  19. AuthorizationLevel.Admin for master keys
  20. AuthorizationLevel.Anonymous only when appropriate

  21. Integrate with domain services

  22. Use Processors for commands
  23. Use Retrievers for queries
  24. Maintain architectural boundaries

  25. Configure scaling appropriately

  26. Set concurrency limits
  27. Use host.json settings
  28. Monitor function performance

Don'ts

  1. Don't bypass domain services
  2. Always use Processors/Retrievers
  3. Don't access repositories directly
  4. Maintain architectural boundaries

  5. Don't ignore error handling

  6. Always handle exceptions
  7. Don't expose internal errors to clients
  8. Implement proper retry logic

  9. Don't hardcode configuration

  10. Use environment variables
  11. Use Azure Key Vault for secrets
  12. Use connection strings properly

  13. Don't create long-running functions

  14. Azure Functions have timeout limits
  15. Use Durable Functions for long-running workflows
  16. 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.