I’ve previously written about how to return an image from an Azure Function which deals with returning a generated image from the function, however it’s also useful to return images which might be stored elsewhere such as in Azure Blob Storage or an AWS S3 bucket.

I’ve created an example project to demonstrate how to do that here with details below.

BlobStorageFileService

The code for accessing the files from Azure Blob Storage and AWS S3 is based on work I’ve done previously where I’ve needed to move files between different locations so is a bit overkill for this example but demonstrates some other functionality quite nicely.

This service adds methods (abbreviated example code below) for interacting with the files, in the case of this example we’re only interested in seeing if the images exist or not and returning a stream of them if they do.

public class BlobStorageFileService : IFileService
{
    private readonly BlobServiceClient _blobServiceClient;
    private readonly ILogger<BlobStorageFileService> _logger;

    public BlobStorageFileService(ILoggerFactory loggerFactory, BlobServiceClient blobServiceClient)
    {
        _blobServiceClient = blobServiceClient;
        _logger = loggerFactory.CreateLogger<BlobStorageFileService>();
    }

    public async Task<Stream> GetFileStreamAsync(string container, string fileName)
    {
        return await _blobServiceClient
            .GetBlobContainerClient(container)
            .GetBlobClient(fileName)
            .OpenReadAsync();
    }

    public async Task<bool> GetFileExistsAsync(string container, string fileName)
    {
        return await _blobServiceClient
            .GetBlobContainerClient(container)
            .GetBlobClient(fileName)
            .ExistsAsync();
    }
}

The BlobStorageFileService accesses Azure Blob Storage using an injected instance of BlobServiceClient. An extension method is created to make injecting the client easier.

FileServiceCollectionExtensions

public static class FileServiceCollectionExtensions
{
    /// <summary>
    /// 
    /// </summary>
    /// <param name="collection"></param>
    /// <param name="storageUrl">The URL of the storage container</param>
    /// <param name="tenantId">The tennant ID of the the user accessing the resource</param>
    /// <returns></returns>
    /// <exception cref="ArgumentNullException"></exception>
    public static IServiceCollection AddSourceBlobStorageFileService(this IServiceCollection collection, string storageUrl, string? tenantId = null)
    {
        if (collection == null) throw new ArgumentNullException(nameof(collection));
        if (storageUrl == null) throw new ArgumentNullException(nameof(storageUrl));

        if (tenantId != null)
            Environment.SetEnvironmentVariable("AZURE_TENANT_ID", tenantId);

        // Add blob storage client
        collection.AddAzureClients(builder =>
        {
            // Add a storage account client
            builder.AddBlobServiceClient(new Uri(storageUrl));

            // Select the appropriate credentials based on enviroment
            builder.UseCredential(new DefaultAzureCredential());
        });

        return collection
            .AddSingleton<ISourceFileService, BlobStorageFileService>();
    }
}

The DefaultAzureCredential object automatically authenticates the user based on a heirarchy of credentials. When developing locally in Visual Studio it will use the logged in users credentials, when deployed to Azure you will need to enable managed identity for the function and then grant the function “Storage Blob Contributor” rights on the storage account.

Program

The Program class is pretty simple and just adds logging and the file source services.

public class Program
{
    public static void Main()
    {
        // Initialize serilog logger
        Log.Logger = new LoggerConfiguration()
                .WriteTo.Console(Serilog.Events.LogEventLevel.Debug)
                .MinimumLevel.Debug()
                .Enrich.FromLogContext()
                .CreateLogger();

        var host = new HostBuilder()
            .ConfigureFunctionsWorkerDefaults()
            .ConfigureServices(services =>
            {
                // Add logging
                services.AddSingleton(LoggerFactory.Create(builder =>
                {
                    builder
                        .AddSerilog(dispose: true);
                }));
                // Add file service
                services.AddSourceBlobStorageFileService(
                    Environment.GetEnvironmentVariable("StorageUrl")!,
                    Environment.GetEnvironmentVariable("AZURE_TENANT_ID")!
                    );
            })
            .Build();

        host.Run();
    }
}

ImageServe

The actual function is pretty basic, it takes an image name, looks up a file in the storage account that has that name and returns it if found. I also added optional height and width URL parameters to allow for resizing of the returned image.

public class ImageServe
{
    private readonly ILogger<ImageServe> _logger;
    private readonly ISourceFileService _sourceService;
    private readonly string _container;

    public ImageServe(ISourceFileService sourceService, ILoggerFactory loggerFactory)
    {
        _sourceService = sourceService;
        _logger = loggerFactory.CreateLogger<ImageServe>();
        _container = Environment.GetEnvironmentVariable("StorageUrl")!;
    }

    [Function("{imageName}")]
    public async Task<HttpResponseData> GetImageAsync([HttpTrigger(AuthorizationLevel.Anonymous, "get", "post")] HttpRequestData req, string imageName, int? width, int? height)
    {
        _logger.LogInformation("C# HTTP trigger function processed a request.");

        if (!await _sourceService.GetFileExistsAsync(_container, imageName))
            return req.CreateResponse(HttpStatusCode.NotFound);

        var response = req.CreateResponse(HttpStatusCode.OK);
        response.Headers.Add("Content-Type", "image/jpeg");

        if (width.HasValue && height.HasValue)
        {
            Image.Load(await _sourceService.GetFileStreamAsync(_container, imageName))
                .Clone(x => x.Resize(width.Value, height.Value))
                .SaveAsJpeg(response.Body);
        }
        else if (width.HasValue && !height.HasValue)
        {
            Image.Load(await _sourceService.GetFileStreamAsync(_container, imageName))
                .Clone(x => x.Resize(width.Value, 0))
                .SaveAsJpeg(response.Body);
        }
        else if (!width.HasValue && height.HasValue)
        {
            Image.Load(await _sourceService.GetFileStreamAsync(_container, imageName))
                .Clone(x => x.Resize(0, height.Value))
                .SaveAsJpeg(response.Body);
        }
        else
        {
            response.Body = await _sourceService.GetFileStreamAsync(_container, imageName);
        }

        return response;
    }
}

0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *