Lesson 6.3: Create Blob Storage Repository

In Chapter 5, we created a repository interface (IReadableRepository) and repository implementation (InMemoryReadRepository) that loaded data from embedded resource files. We designed our system with these abstractions because we want to try out several Azure storage technologies, and the repository interface gives our service code a measure of isolation from the data implementation technologies. As a matter of fact, we are going to create several repository implementations that implement the same interface. And if our design works well, we won’t have to change very much of our service implementation.

In this lesson, we are going to create the BlobStorageReadRepository class. This class will encapsulate all of the logic for connecting to the Azure Blob service, loading our game data from blobs saved in the ‘game-data’ container, and surfacing it our callers.

Minor Cleanup

As we start, we need to take on a few cleanup tasks to make things easier in our new repository implementation, and to move some code into more logical folders.

  1. Open Visual Studio with our ‘simple-rpg-services’ solution.
  2. Create a Helpers folder in the SimpleRPG.Game.Services project.
  3. Move the JsonSerializationHelper.cs file into the Helpers folder. In git, this will look like we deleted the file from the Repositories folder and added a new file to the Helpers folder.
  4. Change the JsonSerializationHelper.cs file namespace to SimpleRPG.Game.Services.Helpers. This will require us to clean up some using statements in our code and unit tests.
  5. Rename the RepositoryBuilders.cs file to ResourceRepositoryBuilders.cs. Since we will have builders for other types of repositories, we want to dedicate a file for each type.
  6. Install the ‘Azure.Storage.Blobs’ NuGet package in the SimpleRPG.Game.Services project. This package is a client library that integrates with the Azure Blob Services APIs.
    • Launch the NuGet Package Manager for our project.
    • Search for ‘Azure.Storage.Blobs’.
    • Click the ‘Install’ button on the top result.
    • Be sure to accept any license dialogs that pop-up.
Fig 1 – NuGet Package Manager with Azure.Storage.Blobs Package
  1. Modify the InMemoryReadRepository class to make its public methods virtual. We will derive our new repository from this one, but need to override some of the behavior in the base repository.
using SimpleRPG.Game.Services.DTO;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Services.Repositories
{
    public class InMemoryReadRepository<T, TId> : IReadableRepository<T, TId>
        where T : NamedElement<TId>
        where TId : struct
    {
        private readonly Dictionary<string, Func<T, string, bool>> _filterChecks = new Dictionary<string, Func<T, string, bool>>();
        private readonly Dictionary<string, Func<T, object>> _sortChecks = new Dictionary<string, Func<T, object>>();

        public InMemoryReadRepository(
            IList<T> entities,
            IList<NameValuePair<Func<T, string, bool>>> knownFilters = null,
            IList<NameValuePair<Func<T, object>>> knownSorts = null)
            : this()
        {
            Entities = entities ?? new List<T>();

            if (knownFilters != null)
            {
                foreach (var f in knownFilters)
                {
                    _filterChecks.Add(f.Name, f.Value);
                }
            }

            if (knownSorts != null)
            {
                foreach (var s in knownSorts)
                {
                    _sortChecks.Add(s.Name, s.Value);
                }
            }
        }

        public InMemoryReadRepository()
        {
            _filterChecks.Add("Name", (f, v) => f.Name.Contains(v, StringComparison.InvariantCultureIgnoreCase));

            _sortChecks.Add("Name", p => p.Name);
            _sortChecks.Add("Id", p => p.Id);
        }

        protected virtual IList<T> Entities { get; set; } = new List<T>();

        public virtual Task<IEnumerable<T>> GetEntities(
            int? offset,
            int? limit,
            IEnumerable<NameValuePair<string>> filters,
            IEnumerable<NameValuePair<string>> sorts)
        {
            var o = offset ?? 0;
            var l = limit ?? Entities.Count;
            var filtered = ApplyFilters(Entities, filters);
            var sorted = GetSortedEntities(filtered, sorts);

            return Task.FromResult(sorted.Skip(o).Take(l).ToList().AsEnumerable());
        }

        public virtual Task<int> GetEntityCount(IEnumerable<NameValuePair<string>> filters)
        {
            var filtered = ApplyFilters(Entities, filters);
            return Task.FromResult(filtered.Count());
        }

        public virtual Task<T> GetEntityById(TId id)
        {
            var spell = Entities.FirstOrDefault(t => t.Id.Equals(id));
            if (spell == null)
            {
                throw new EntityNotFoundException(nameof(id), id);
            }

            return Task.FromResult(spell);
        }

        public virtual Task<IEnumerable<T>> GetBulkEntitiesById(IEnumerable<TId> ids)
        {
            _ = ids ?? throw new ArgumentNullException(nameof(ids));

            var entities = (List<T>)Entities;
            var list = entities.FindAll(p => ids.Contains(p.Id));
            return Task.FromResult(list.OrderBy(e => e.Name).ToList().AsEnumerable());
        }

        protected virtual IEnumerable<T> ApplyFilters(
            IEnumerable<T> items,
            IEnumerable<NameValuePair<string>> filters)
        {
            if (filters == null)
            {
                return items;
            }

            foreach(var f in filters)
            {
                if (_filterChecks.TryGetValue(f.Name, out var filterAction))
                {
                    items = items.Where(p => filterAction.Invoke(p, f.Value)).ToList();
                }
            }

            return items;
        }

        protected virtual IEnumerable<T> GetSortedEntities(
            IEnumerable<T> items,
            IEnumerable<NameValuePair<string>> sorts)
        {
            if (sorts == null || sorts.Any() == false)
            {
                if (_sortChecks.TryGetValue("Name", out var sortAction))
                {
                    items = items.OrderBy(p => sortAction.Invoke(p)).ToList();
                }

                return items;
            }

            foreach (var s in sorts)
            {
                if (_sortChecks.TryGetValue(s.Name, out var sortAction))
                {
                    if (s.Value.ToLower() == "dsc")
                    {
                        items = items.OrderByDescending(p => sortAction.Invoke(p)).ToList();
                    }
                    else
                    {
                        items = items.OrderBy(p => sortAction.Invoke(p)).ToList();
                    }
                }
            }

            return items;
        }
    }
}

If you want to see the results of the structural changes in the project, please review the commit for this lesson. These are mostly cosmetic changes but they put classes in proper folders and create less confusing filenames.

With the clean up done, we can start creating some new code.

Build BlobStorageAdapter

We are going to use the proxy design pattern as a surrogate for our communication with the Azure Blob Service API. This proxy interface and class will allow our code to remain agnostic to the changes in the Blob Service API. Also by having the proxy as an interface, it will be easier for us to test various cases and error conditions without have to directly simulate them with Azure Storage. This layer is mainly here to improve our testability.

Let’s start by creating the IBlobStorageAdapter interface in the SimpleRPG.Game.Services project and Helpers folder.

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Services.Helpers
{
    public interface IBlobStorageAdapter
    {
        BlobClient OpenBlobClient(string connection, string containerName, string blobName);

        Task<BlobDownloadInfo> DownloadAsync(BlobClient client);

        Task<int> ReadAsync(BlobDownloadInfo download, byte[] buffer);
    }
}

This interface defines three methods that we use to interact with the Blob Service API. We are able to open a connection to the Blob Service, download a particular blob file, and read the data into a memory buffer.

Now, let’s create the BlobStorageAdapter implementation class in the same folder.

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using System;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Services.Helpers
{
    public class BlobStorageAdapter : IBlobStorageAdapter
    {
        public BlobClient OpenBlobClient(string connection, string containerName, string blobName)
        {
            BlobServiceClient blobServiceClient = new BlobServiceClient(connection);
            BlobContainerClient containerClient = blobServiceClient.GetBlobContainerClient(containerName);

            return containerClient.GetBlobClient(blobName);
        }

        public async Task<BlobDownloadInfo> DownloadAsync(BlobClient client)
        {
            _ = client ?? throw new ArgumentNullException(nameof(client));
            return await client.DownloadAsync().ConfigureAwait(false);
        }

        public async Task<int> ReadAsync(BlobDownloadInfo download, byte[] buffer)
        {
            _ = download ?? throw new ArgumentNullException(nameof(download));
            return await download.Content.ReadAsync(buffer, 0, (int)download.ContentLength).ConfigureAwait(false);
        }
    }
}
  • In line #8, we define the BlobStorageAdapter class that implements the IBlobStorageAdapter interface.
  • The OpenBlobClient method (lines #10-16) interfaces with Blob Service API for multiple calls:
    • It opens a connection to the Blob Service using our client API library. We pass the connection string to use for our storage account.
    • We then open a container in that storage account. For our service, that will be the ‘game-data’ container.
    • Then we get a particular blob file in our container. These blob filenames will be the names of our game data files, like items.json for example.
    • This is a short method, but it does a lot of work with the help of the Blob Service client library. If you want to review the code more closely and what it does, please read the Azure.Storage.Blobs library API reference.
  • The DownloadAsync method (lines #18-22) downloads the blob that we previously opened in the BlobClient.
  • The ReadAsync method (lines #24-28) uses the BlobDownloadInfo to then read its data. It returns the data in the memory buffer that was passed in. And returns the number of bytes that were read.

Create BlobStorageReadRepository

With our proxy in place, we can create the BlobStorageReadRepository in the Repositories folder.

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using SimpleRPG.Game.Services.DTO;
using SimpleRPG.Game.Services.Helpers;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;

namespace SimpleRPG.Game.Services.Repositories
{
    public class BlobStorageReadRepository<T, TId> : InMemoryReadRepository<T, TId>
        where T : NamedElement<TId>
        where TId : struct
    {
        private readonly string _connectionString;
        private readonly string _blobName;
        private readonly IBlobStorageAdapter _blobAdapter;

        public BlobStorageReadRepository(
            string connectionString,
            string blobName,
            IBlobStorageAdapter blobAdapter,
            IList<NameValuePair<Func<T, string, bool>>> knownFilters = null,
            IList<NameValuePair<Func<T, object>>> knownSorts = null)
            : base(null, knownFilters, knownSorts)
        {
            if (string.IsNullOrEmpty(connectionString)) throw new ArgumentNullException(nameof(connectionString));
            _connectionString = connectionString;

            if (string.IsNullOrEmpty(blobName)) throw new ArgumentNullException(nameof(blobName));
            _blobName = blobName;

            _blobAdapter = blobAdapter ?? throw new ArgumentNullException(nameof(blobAdapter));
        }

        public override async Task<IEnumerable<T>> GetEntities(
            int? offset,
            int? limit,
            IEnumerable<NameValuePair<string>> filters,
            IEnumerable<NameValuePair<string>> sorts)
        {
            await LoadDataIfEmpty().ConfigureAwait(false);

            return await base.GetEntities(offset, limit, filters, sorts).ConfigureAwait(false);
        }

        public override async Task<int> GetEntityCount(IEnumerable<NameValuePair<string>> filters)
        {
            await LoadDataIfEmpty().ConfigureAwait(false);

            return await base.GetEntityCount(filters).ConfigureAwait(false);
        }

        public override async Task<T> GetEntityById(TId id)
        {
            await LoadDataIfEmpty().ConfigureAwait(false);

            return await base.GetEntityById(id).ConfigureAwait(false);
        }

        public override async Task<IEnumerable<T>> GetBulkEntitiesById(IEnumerable<TId> ids)
        {
            await LoadDataIfEmpty().ConfigureAwait(false);

            return await base.GetBulkEntitiesById(ids).ConfigureAwait(false);
        }

        private async Task LoadDataIfEmpty()
        {
            if (Entities.Any())
            {
                return;
            }

            BlobClient blobClient = _blobAdapter.OpenBlobClient(_connectionString, "game-data", _blobName);
            BlobDownloadInfo download = await _blobAdapter.DownloadAsync(blobClient).ConfigureAwait(false);

            if (download.ContentType == "application/json")
            {
                byte[] data = new byte[download.ContentLength];
                var result = await _blobAdapter.ReadAsync(download, data).ConfigureAwait(false);
                if (result >= download.ContentLength)
                {
                    var json = Encoding.UTF8.GetString(data);
                    Entities = JsonSerializer.Deserialize<IList<T>>(json);
                }
                else
                {
                    throw new Exception("Incomplete download of blob storage file.");
                }
            }
            else
            {
                throw new UnsupportedContentTypeException($"Unsupported content type retrieved: {download.ContentType}.");
            }
        }
    }
}
  • First, in line #15, we define the BlobStorageReadRepository. This is a generic class with similar type constraints that we used before. This class derives from InMemoryReadRepository because we will also load our data into memory and then access it from there, but how it is loaded will be very different.
  • Recall that InMemoryReadRepository implements the IReadableRepository interface, so now our new repository does as well.
  • Then, in lines #16-18, we define the same constraints on this generic class as we have on our interface.
  • Lines #19-21 define members to hold data about our storage connection and our Storage service proxy.
  • The constructor (lines #23-38) takes parameters to initialize the repository. The knownFilters and knownSorts are passed directly to the base constructor (so the InMemoryReadRepository can handle those). The remaining parameters are saved to our member variables for use later.
  • The Get* methods (lines #40-70) are overrides of the same methods from the InMemoryReadRepository. And each method just calls the LoadDataIfEmpty helper method before calling the base version of the method. This allows us to ensure that the data is loaded from the Blob Service before we try to perform any operations on it.
  • The LoadDataIfEmpty method (lines # 72-100) performs several actions with the help of the IBlobStorageAdapter.
    • First, it checks if there are any Entities already loaded. If there are, then the method completes immediately. We are only loading the data once per instantiation of the repository.
    • Otherwise, we open a BlobClient to our data file using the provided connection string, the “game-data” container name, and the blob filename we wish to load.
    • After the client opens to the blob file, we request a download.
    • We verify that the download content type is JSON. If it isn’t then we throw an exception because it’s an unexpected message format, and we won’t be able to deserialize its contents.
    • Then, we create a memory buffer the size of the expected content.
    • And, call ReadAsync to retrieve the data from the downloaded file.
    • We verify that the amount of data loaded is the full size of the content. If it’s not then we throw an exception because the download was incomplete.
    • If everything succeeds to this point, we convert the memory buffer into a string.
    • And finally, we deserialize the string with the JsonSerializer to convert it into a list of elements (their type defined as T in the class definition).
    • This code is a little complex, but lets us load all of our different data types in a general way that we can reuse in other projects.

There were a couple of design decisions we made in this repository that were different from our InMemoryReadRepository. First, we don’t load the data for the repository in the constructor. Because the Blob Service is running in the cloud, we want to use asynchronous methods to download and read the files. We cannot use async methods in our constructor. And, we didn’t want to complicate the IRepositoryBuilder with both sync and async versions. So, we are going to load our data on demand when it is first requested.

Second, in the LoadDataIfEmpty method, we throw an exception if the data size isn’t the full size of our blob. This works in our scenarios because our data files are small. If there are large blobs, the ReadAsync API actually streams pieces of the file at a time. We could have implemented the method to loop through and keep retrieving data chunks until the full file is downloaded. That would have complicated the code even further and was unnecessary for our data. So, we will leave that as an exercise for the reader, if you need to read large blobs of data. 🙂

Blob Repository Builders

With the repository complete, we need to create repository builders that map to our expected data types. This will follow the pattern we used for the *MemoryRepositoryBuilder classes (from lesson 5.8).

First, we create the BlobRepositoryBuilders.cs file in the Repositories folder. This file will contain all of the builders for typed Blob storage repositories.

using SimpleRPG.Game.Services.DTO;
using SimpleRPG.Game.Services.Helpers;
using System;
using System.Collections.Generic;

namespace SimpleRPG.Game.Services.Repositories
{
    internal struct BlobStorageData
    {
        public const string BlobConnection = "DefaultEndpointsProtocol=https;AccountName=simplerpgv2;AccountKey=BBNJEF0p5qBOaL46OLIf7+3ZIEbmKVK2ENflkX+eA68YjqHjA1gRW3R7+poZeW2sV4XVE2+rT4U5af8QyNNsog==;EndpointSuffix=core.windows.net";
        public static IBlobStorageAdapter BlobStorageAdapter = new BlobStorageAdapter();
    }

    internal class ItemTemplateBlobRepositoryBuilder : IRepositoryBuilder
    {
        private const string _blobName = "items.json";

        private static readonly List<NameValuePair<Func<ItemTemplate, string, bool>>> _knownFilters =
            new List<NameValuePair<Func<ItemTemplate, string, bool>>>
            {
                new NameValuePair<Func<ItemTemplate, string, bool>>(
                    "category", (f, v) => f.Category == Convert.ToInt32(v)),
            };

        private static readonly List<NameValuePair<Func<ItemTemplate, object>>> _knownSorts =
            new List<NameValuePair<Func<ItemTemplate, object>>>
            {
                new NameValuePair<Func<ItemTemplate, object>>("category", p => Convert.ToInt32(p.Category)),
                new NameValuePair<Func<ItemTemplate, object>>("price", p => p.Price)
            };

        public IRepository CreateRepository()
        {
            return new BlobStorageReadRepository<ItemTemplate, int>(
                BlobStorageData.BlobConnection,
                _blobName,
                BlobStorageData.BlobStorageAdapter,
                _knownFilters,
                _knownSorts);
        }
    }

    internal class MonsterTemplateBlobRepositoryBuilder : IRepositoryBuilder
    {
        private const string _blobName = "monsters.json";

        public IRepository CreateRepository()
        {
            return new BlobStorageReadRepository<MonsterTemplate, int>(
                BlobStorageData.BlobConnection, _blobName, BlobStorageData.BlobStorageAdapter);
        }
    }

    internal class LocationTemplateBlobRepositoryBuilder : IRepositoryBuilder
    {
        private const string _blobName = "locations.json";

        public IRepository CreateRepository()
        {
            return new BlobStorageReadRepository<LocationTemplate, int>(
                BlobStorageData.BlobConnection, _blobName, BlobStorageData.BlobStorageAdapter);
        }
    }

    internal class TraderTemplateBlobRepositoryBuilder : IRepositoryBuilder
    {
        private const string _blobName = "traders.json";

        public IRepository CreateRepository()
        {
            return new BlobStorageReadRepository<TraderTemplate, int>(
                BlobStorageData.BlobConnection, _blobName, BlobStorageData.BlobStorageAdapter);
        }
    }

    internal class QuestTemplateBlobRepositoryBuilder : IRepositoryBuilder
    {
        private const string _blobName = "quests.json";

        public IRepository CreateRepository()
        {
            return new BlobStorageReadRepository<QuestTemplate, int>(
                BlobStorageData.BlobConnection, _blobName, BlobStorageData.BlobStorageAdapter);
        }
    }

    internal class RecipeTemplateBlobRepositoryBuilder : IRepositoryBuilder
    {
        private const string _blobName = "recipes.json";

        public IRepository CreateRepository()
        {
            return new BlobStorageReadRepository<RecipeTemplate, int>(
                BlobStorageData.BlobConnection, _blobName, BlobStorageData.BlobStorageAdapter);
        }
    }
}
  • Each repository builder matches the ones in the ResourceRepositoryBuilders.cs file.
  • They each create a typed BlobStorageReadRepository for each game data type using the constructor.
  • The BlobStorageData class (lines #8-12) holds configuration for the Blob Service, like the storage account connection string and the IBlobStorageAdapter. This configuration data is used by all of these builders.
  • The ItemTemplateBlobRepositoryBuilder class also has definitions for filters and sorts, just like in the resource repository version of this builder.

RepositoryFactory Changes

After creating the repository builders, we need to update the RespositoryFactory to make them available to our services.

using SimpleRPG.Game.Services.DTO;
using System;
using System.Collections.Generic;

namespace SimpleRPG.Game.Services.Repositories
{
    public static class RepositoryFactory
    {
        private const string _repoResources = "resource";
        private static readonly Dictionary<Type, IRepositoryBuilder> _resourceBuilderMapping =
            new Dictionary<Type, IRepositoryBuilder>
            {
                { typeof(ItemTemplate), new ItemTemplateMemoryRepositoryBuilder() },
                { typeof(MonsterTemplate), new MonsterTemplateMemoryRepositoryBuilder() },
                { typeof(LocationTemplate), new LocationTemplateMemoryRepositoryBuilder() },
                { typeof(TraderTemplate), new TraderTemplateMemoryRepositoryBuilder() },
                { typeof(QuestTemplate), new QuestTemplateMemoryRepositoryBuilder() },
                { typeof(RecipeTemplate), new RecipeTemplateMemoryRepositoryBuilder() },
            };

        private const string _repoBlobStorage = "blob";
        private static readonly Dictionary<Type, IRepositoryBuilder> _blobBuilderMapping =
            new Dictionary<Type, IRepositoryBuilder>
            {
                { typeof(ItemTemplate), new ItemTemplateBlobRepositoryBuilder() },
                { typeof(MonsterTemplate), new MonsterTemplateBlobRepositoryBuilder() },
                { typeof(LocationTemplate), new LocationTemplateBlobRepositoryBuilder() },
                { typeof(TraderTemplate), new TraderTemplateBlobRepositoryBuilder() },
                { typeof(QuestTemplate), new QuestTemplateBlobRepositoryBuilder() },
                { typeof(RecipeTemplate), new RecipeTemplateBlobRepositoryBuilder() },
            };

        public static IReadableRepository<T, TId> CreateRepository<T, TId>(string repoSource)
            where T : NamedElement<TId>
            where TId : struct
        {
            switch (repoSource)
            {
                case _repoResources:
                    return CreateResourceRepository<T, TId>();
                case _repoBlobStorage:
                    return CreateBlobStorageRepository<T, TId>();
                default:
                    throw new ArgumentOutOfRangeException(nameof(repoSource));
            }
        }

        private static IReadableRepository<T, TId> CreateResourceRepository<T, TId>()
            where T : NamedElement<TId>
            where TId : struct
        {
            var builder = _resourceBuilderMapping[typeof(T)];
            return builder.CreateRepository() as IReadableRepository<T, TId>;
        }

        private static IReadableRepository<T, TId> CreateBlobStorageRepository<T, TId>()
            where T : NamedElement<TId>
            where TId : struct
        {
            var builder = _blobBuilderMapping[typeof(T)];
            return builder.CreateRepository() as IReadableRepository<T, TId>;
        }
    }
}
  • Define the blob identifier’s name used to differentiate which repository to build (line #21).
  • Define another dictionary for entity types and IRepositoryBuilder to use in mappings (lines #22-31).
  • Add another case statement to the repository source switch statement (lines #41-42). For _repoBlobStorage, call the new method CreateBlobStorageRepository.
  • The CreateBlobStorageRepository method (lines #56-62) uses the entity type to find the appropriate IRepositoryBuilder. Then, it calls its CreateRepository method to construct the repository that was requested.

With the RepositoryFactory changes complete, our new repositories are available to the game services. We can build all of the code successfully again.

Testing the BlobStorageReadRepository

While we haven’t consumed the blob storage repository from our services yet, we can validate that it works with some tests. Let’s review one of the BlobStorageReadRepository tests to see how they work.

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Moq;
using SimpleRPG.Game.Services.DTO;
using SimpleRPG.Game.Services.Helpers;
using SimpleRPG.Game.Services.Repositories;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace SimpleRPG.Game.Services.Tests.Repositories
{
    public class BlobStorageReadRepositoryTests
    {
        private const string _connectionString = "DefaultEndpointsProtocol=https;AccountName=simplerpgv2;AccountKey=BBNJEF0p5qBOaL46OLIf7+3ZIEbmKVK2ENflkX+eA68YjqHjA1gRW3R7+poZeW2sV4XVE2+rT4U5af8QyNNsog==;EndpointSuffix=core.windows.net";
        private const string _blobName = "items.json";
        private static IBlobStorageAdapter _adapter = new BlobStorageAdapter();

        [Fact]
        public async Task ItemTemplate_GetEntities()
        {
            // arrange
            var repo = new BlobStorageReadRepository<ItemTemplate, int>(_connectionString, _blobName, _adapter);

            // act
            var result = await repo.GetEntities(null, null, null, null).ConfigureAwait(false);
            var count = await repo.GetEntityCount(null).ConfigureAwait(false);

            // assert
            Assert.Equal(count, result.Count());
            Assert.Contains<ItemTemplate>(result, f => f.Name == "Pointy stick - blob");
        }
}
  • The ItemTemplate_GetEntities method starts by creating the BlobStorageReadRepository for an ItemTemplate.
  • Our test class defines the connection string, blob name, and adapter to use in all of the tests.
  • We then call GetEntities to retrieve all of the items from blob storage.
  • We validate the count of items returned.
  • Finally, we validate that the items list contains an item named ‘Pointy stick – blob’. This verifies that we got an item from our game data Blob Storage… and not from somewhere else (like the embedded resource files).

Let’s also look at the changes to RepositoryFactory tests:

        [Theory]
        [InlineData("resource")]
        [InlineData("blob")]
        public void CreateRepository_ItemTemplateRepository(string repoSource)
        {
            // arrange

            // act
            var repo = RepositoryFactory.CreateRepository<ItemTemplate, int>(repoSource);

            // assert
            Assert.NotNull(repo);
            Assert.IsAssignableFrom<IReadableRepository<ItemTemplate, int>>(repo);
        }
  • Change these test methods to use the [Theory] attribute (from Fact).
  • Add two [InlineData] attributes for: resource and blob.
  • Update the method signature to take the repoSource parameter.
  • Finally, we call CreateRepository with the repoSource parameter.

The [Theory] attribute instructs xUnit to run variations of our test methods. These same test methods are run for both resource and blob repository types. And results get reported separately in the Test Explorer. This allows us to build test variations easily without creating duplicate test methods in our test project.

In conclusion, we now have a repository class that reads data from our Blob Storage container. We plumbed this all the way through the RepositoryFactory. And we built tests that validate all of the functionality of the BlobStorageReadRepository. In the next lesson, we will expose this data from web services.

3 thoughts on “Lesson 6.3: Create Blob Storage Repository

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s