Using Azure Storage in ASP.NET Core


Tools This blog has existed for 15 years now and I've moved it from server to server, service to service, in many forms over the years. As I moved servers, one of my biggest pains was copying all the images and downloads from server to server.

My site code took up about 1% of the space, and all those embedded images and downloads took a majority of the space. I was sick of it, especially on deploying the site (or saving the site in Git), so I decided to switch to storing it in Azure Storage (or AWS if you prefer).

So when I wrote my .NET Core version of the blog, I decided to bite the bullet and start storing them there. But I wanted to enable it directly from Blog authoring. I'm using my version of Metalog API middleware I wrote to do this (see more about that at Github). But I needed a small service to actually support saving new images to the storage service.

First, I set up an Azure Storage account:

Next you'll want to create a container for your images or files in the Azure Storage. A Container is essentially a subdirectory where you can store things. You can have sub-directories under that too. You'll do this by going "Containers" in your storage account and creating a new container:

When you create a new container, you can specify what access you want to give. I choose Container to allow anonymous access to the images (so they can be served):

Once you create a storage accont, go to the "Access Keys" and get one of the keys to use to store objects in it:

My first step was getting the Azure Storage library added as a package:

In my project, I started by creating a new service called ImageStorageService. I created a single method StoreImage and refactored an interface to represent my service:

  public class ImageStorageService : IImageStorageService
  {
    public async Task<string> StoreImage(string filename, byte[] image)
    {
          
    }
  }

The method is asynchronous because I knew that the Azure Storage API was async as well. To use the API, you'll need to construct an URL that matches your Azure URL, Container name, and the new blog (e.g. file) name:

  https://your-storage-name.azure.blob.core.windows.net/your-container-name/new-file-name.jpg
  // e.g.
  http://wilderminds.azure.blog.core.windows.net/img/foo.jpg

I do this by using path to get the raw filename sent to the method and concatenating it with the URL name. In my case I'm using configuration to store this in a JSON file so I need to inject IConfiguration too:

  public class ImageStorageService : IImageStorageService
  {
    private readonly IConfiguration _config;

    public ImageStorageService(IConfiguration config)
    {
      _config = config;
    }

    public async Task<string> StoreImage(string filename, byte[] image)
    {
      var filenameonly = Path.GetFileName(filename);

      var url = string.Concat(_config["BlobService:StorageUrl"], filenameonly);
     
      // TODO
     
      return url;
    }
  }

Next I create storage credentials object by getting the name of the storage account (usually the name of the storage service), and the access key that we copied earlier:

  var creds = new StorageCredentials(_config["BlobStorage:Account"], _config["BlobStorage:Key"]);

Next I create a CloudBlockBlog object to store the file:

  var blob = new CloudBlockBlob(new Uri(url), creds);

Next I can use the blob object to check and see if it exists and if so, if it is the same size. I do this by first calling ExistsAsync, and then retrieving the attributes via FetchAttributesAsync and checking the length against the length. This makes sure that we don't re-upload the same file over and over:

      bool shouldUpload = true;
      if (await blob.ExistsAsync())
      {
        await blob.FetchAttributesAsync();
        if (blob.Properties.Length == image.Length)
        {
          shouldUpload = false;
        }
      }

Lastly, I send the file via UploadFromByteArrayAsync. If you have a rather large file to upload, you might need to chunk it up, but this call is like a stream so you can send it in pieces:

  if (shouldUpload) await blob.UploadFromByteArrayAsync(image, 0, image.Length);

So the complete method looks like this:

  public async Task<string> StoreImage(string filename, byte[] image)
  {
    var filenameonly = Path.GetFileName(filename);
    
    var url = string.Concat(_config["BlobService:StorageUrl"], filenameonly);
    var creds = new StorageCredentials(_config["BlobStorage:Account"], _config["BlobStorage:Key"]);
    var blob = new CloudBlockBlob(new Uri(url), creds);
    
    if (!(await blob.ExistsAsync()))
    {
      await blob.UploadFromByteArrayAsync(image, 0, image.Length);
    }
    
    return url;
  }

Now that it's complete, I just register the service in Startup.cs:

svcs.AddTransient<IImageStorageService, ImageStorageService>();

To call this, I just inject the service into my Metaweblog service like so:

  public class WilderWeblogProvider : IMetaWeblogProvider
  {
    private IWilderRepository _repo;
    private UserManager<WilderUser> _userMgr;
    private IConfiguration _config;
    private IHostingEnvironment _appEnv;
    private readonly IImageStorageService _imageService;

    public WilderWeblogProvider(UserManager<WilderUser> userMgr, 
                                IWilderRepository repo, 
                                IConfiguration config, 
                                IHostingEnvironment appEnv, 
                                IImageStorageService imageService)
    {
      _repo = repo;
      _userMgr = userMgr;
      _config = config;
      _appEnv = appEnv;
      _imageService = imageService; 
    }

And finally, just call the StoreImage method:

    public MediaObjectInfo NewMediaObject(string blogid, string username, string password, MediaObject mediaObject)
    {
      EnsureUser(username, password).Wait();

      var bits = Convert.FromBase64String(mediaObject.bits);
      var op = _imageService.StoreImage(mediaObject.name, bits);

      op.Wait();
      if (!op.IsCompletedSuccessfully) throw op.Exception;
      var url = op.Result;

      // Create the response
      MediaObjectInfo objectInfo = new MediaObjectInfo();
      objectInfo.url = url;

      return objectInfo;
    }

Note the work involved with the op.Wait() is mostly because the Metaweblog must be synchronous and the method I'm calling is asynchronous, if you use this in a controller, you can juse use async and await.

You could do this via a web upload or even in an API if you needed to. You can see the code on the blog's github:

https://github.com/shawnwildermuth/wilderblog

Hope this helps!




Application Name WilderBlog Environment Name Production
Application Ver 2.0.0.0 Runtime Framework .NETCoreApp,Version=v2.0
App Path D:\home\site\wwwroot\ Runtime Version .NET Core 4.6.25815.02
Operating System Microsoft Windows 6.2.9200 Runtime Arch X86