Thanks for visiting my blog! See more about me here: About Me
I have a couple of public projects (this blog, and the code camp website) that both use Azure Blob storage to store images for the sites. I’ve felt guilty that I’ve copy/pasted the code between them for a while. I decided to fix it.
While the code was pretty simple, I did need to change it a bit to generalize it. But then I wondered, would it be useful for anyone else? I decided to just make it a Nuget package. But should I have? We’ll get to that in a minute, let’s talk about how I generalized it first.
Generalizing the Code
Originally, the code was a simple class that did all the work in a single method. But to generalize it I needed to move code that was specific to my implementations out. First to go was the IConfiguration usage. In the old code, I was simply importing configuration (as AppSettings):
public ImageStorageService(IOptions<AppSettings> settings,
ILogger<ImageStorageService> logger)
{
_settings = settings;
_logger = logger;
}
Then using the app settings to create the credential into Azure Blob Storage:
var creds = new StorageSharedKeyCredential(_settings.Value.BlobStorage.Account,
_settings.Value.BlobStorage.Key);
var client = new BlobServiceClient(new Uri(_settings.Value.BlobStorage.StorageUrl), creds);
I didn’t like this. To generalize this, I needed to derive the credentials to store the extra piece of information (the StorageUrl):
public class AzureImageStorageCredentials : StorageSharedKeyCredential
{
public AzureImageStorageCredentials(string accountName, string accountKey, string url)
:base(accountName, accountKey)
{
AccountUrl = url;
}
public string AccountUrl { get; private set; }
}
This way we could just inject it into the service as needed:
public AzureImageStorageService(ILogger<AzureImageStorageService> logger,
AzureImageStorageServiceClient client)
{
_logger = logger;
_client = client;
}
FInally, to clean up the code, I hid the service registration in an extension method:
public static IServiceCollection AddAzureImageStorageService(this IServiceCollection coll,
[NotNull] string azureAccountName,
[NotNull] string azureAccountKey,
[NotNull] string azureStorageUrl)
{
coll.AddScoped(coll => new AzureImageStorageCredentials(azureAccountName,
azureAccountKey,
azureStorageUrl));
coll.AddScoped<AzureImageStorageServiceClient>();
coll.AddTransient<IAzureImageStorageService, AzureImageStorageService>();
return coll;
}
This seemed all good, but now that i’ve generalized it with the derived credential, the dummy credentials that I used during development simply broke. I needed to verify that the key was Base64 encoded:
public static IServiceCollection AddAzureImageStorageService(this IServiceCollection coll,
[NotNull] string azureAccountName,
[NotNull] string azureAccountKey,
[NotNull] string azureStorageUrl)
{
// Test for valid Base64 Key
Span<byte> buffer = new Span<byte>(new byte[azureAccountKey.Length]);
if (!Convert.TryFromBase64String(azureAccountKey, buffer, out int bytesParsed))
{
throw new InvalidOperationException("Azure Account Key must be a Base64 Encoded String. If running in development, mock the IImageStorageService for development instead.");
}
coll.AddScoped(coll => new AzureImageStorageCredentials(azureAccountName, azureAccountKey, azureStorageUrl));
coll.AddScoped<AzureImageStorageServiceClient>();
coll.AddTransient<IAzureImageStorageService, AzureImageStorageService>();
return coll;
}
That’s all and well…but…
Should I Share It?
I could have just created a shared DLL and just gone about my day, but it wasn’t that simple. Both of these projects are public (https://github.com/shawnwildermuth/wilderblog and https://github.com/shawnwildermuth/corecodecamp). I thought it would make more sense to release it as a Nuget package. Maybe no one ever needs to store images in Blob Storage but maybe they do.
This code isn’t much more than an abstraction layer around the Azure SDK, right? Maybe not.
The real trick of the code (and why I wanted to share it) was to support updating images. I need this quite a bit as I might upload an image then optimize it and want to overwrite this. Here’s the guts of the main process of uploading the image:
public async Task<ImageResponse> StoreImage(string storeImagePath, Stream imageStream)
{
var response = new ImageResponse();
try
{
var imageName = Path.GetFileName(storeImagePath);
var imagePath = Path.GetFullPath(storeImagePath);
var container = _client.GetBlobContainerClient(imagePath);
// Get old Image to update
var blob = container.GetBlobClient(imageName);
bool shouldUpload = true;
if (await blob.ExistsAsync())
{
var props = await blob.GetPropertiesAsync();
if (props.Value.ContentLength == imageStream.Length)
{
shouldUpload = false;
response.ImageChanged = false;
response.Success = true;
}
}
if (shouldUpload)
{
var result = await blob.UploadAsync(imageStream, true);
if (result != null)
{
response.ImageChanged = true;
response.Success = true;
response.ImageUrl = blob.Uri.AbsoluteUri;
}
}
}
catch (Exception ex)
{
_logger.LogError($"Failed to upload blob: {ex}");
response.Success = false;
response.Exception = ex;
}
return response;
}
Note that part of the code is to see if the blog exists, and if so check to see if the size of the file is different. NOTE, this code overwrites blobs with the same image name but for my needs, that’s fine. You might be able to change this to create a new blob and change the name for you but that wasn’t my use case.
In my case, if the blob has changed in size (the only check I’m doing), I re-upload it.
So, what do you think? Should small little libraries like this be packaged up? Or is this like the one-liner problem in NPM packages that everyone eventually uses when they could just write the code themselves?
You can see the new package here: