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.
- Open Visual Studio with our ‘simple-rpg-services’ solution.
- Create a Helpers folder in the SimpleRPG.Game.Services project.
- 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.
- 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. - 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.
- 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.

- 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 theIBlobStorageAdapter
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 theBlobClient
. - The
ReadAsync
method (lines #24-28) uses theBlobDownloadInfo
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 fromInMemoryReadRepository
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 theIReadableRepository
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
andknownSorts
are passed directly to the base constructor (so theInMemoryReadRepository
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 theInMemoryReadRepository
. And each method just calls theLoadDataIfEmpty
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 theIBlobStorageAdapter
.- 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 asT
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.
- First, it checks if there are any
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 theIBlobStorageAdapter
. 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 methodCreateBlobStorageRepository
. - The
CreateBlobStorageRepository
method (lines #56-62) uses the entity type to find the appropriateIRepositoryBuilder
. Then, it calls itsCreateRepository
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 theBlobStorageReadRepository
for anItemTemplate
. - 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 (fromFact
). - Add two
[InlineData]
attributes for: resource and blob. - Update the method signature to take the
repoSource
parameter. - Finally, we call
CreateRepository
with therepoSource
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”