When working with Azure Functions, it’s common to have different triggers that handle different jobs. For example:
A Blob Trigger that runs whenever a file is uploaded to Azure Storage.
A Timer Trigger that runs on a schedule to clean up or summarize data.
A common mistake is to duplicate logic across these functions. Instead, we can share code through services/helpers. This way, we keep functions focused on their triggers but reuse common business logic.
1. Architecture
The Blob Trigger fires when a new file is uploaded.
The Timer Trigger fires every hour (or your chosen schedule).
Both functions call a shared service class that contains the actual logic for processing and cleanup.
2. Create the Project
Create a new .NET 9 isolated worker Azure Functions project
func init FileProcessorApp --worker-runtime dotnet-isolated --target-framework net9.0
cd FileProcessorAppAdd dependencies
dotnet add package Azure.Storage.Blobs
dotnet add package Microsoft.Azure.Functions.Worker.Extensions.Storage.Blobs3. The Shared Service
We put our main logic in a service class.
File: Services/FileProcessorService.cs
using System;
using System.IO;
using System.Threading.Tasks;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.Extensions.Logging;
public class FileProcessorService
{
private readonly BlobServiceClient _blobServiceClient;
private readonly ILogger<FileProcessorService> _logger;
public FileProcessorService(BlobServiceClient blobServiceClient, ILogger<FileProcessorService> logger)
{
_blobServiceClient = blobServiceClient;
_logger = logger;
}
// Process uploaded files
public async Task ProcessFileAsync(Stream fileStream, string fileName)
{
_logger.LogInformation($"Processing file: {fileName}");
// Example: Check if CSV
if (!fileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
{
_logger.LogWarning($"Skipping non-CSV file: {fileName}");
return;
}
using var reader = new StreamReader(fileStream);
int lineNum = 0;
while (!reader.EndOfStream)
{
var line = await reader.ReadLineAsync();
lineNum++;
_logger.LogInformation($"Line {lineNum}: {line}");
}
// Move file to "processed" container
var processedContainer = _blobServiceClient.GetBlobContainerClient("processed");
await processedContainer.CreateIfNotExistsAsync(PublicAccessType.None);
var processedBlob = processedContainer.GetBlobClient(fileName);
fileStream.Position = 0; // reset stream
await processedBlob.UploadAsync(fileStream, overwrite: true);
_logger.LogInformation($"File {fileName} moved to 'processed' container.");
}
// Cleanup processed files older than 7 days
public async Task CleanupOldFilesAsync()
{
var container = _blobServiceClient.GetBlobContainerClient("processed");
await container.CreateIfNotExistsAsync();
await foreach (var blobItem in container.GetBlobsAsync())
{
if (blobItem.Properties.CreatedOn < DateTimeOffset.UtcNow.AddDays(-7))
{
var blobClient = container.GetBlobClient(blobItem.Name);
await blobClient.DeleteIfExistsAsync();
_logger.LogInformation($"Deleted old file: {blobItem.Name}");
}
}
}
}
Join the conversation! Your thoughts help the community grow.