In this article we will talk about the “Unit of Work” and “Repository” design patterns in the context of a test ASP.NET Core web application (using the embedded DI), which we will develop together. As a result, we get two implementations of interaction with the repository: the real one, based on the SQLite database, and fake, for quick testing, based on the enumeration in memory. Switching between these two implementations will be performed by changing one line of code.

Training
Traditionally, if you have not worked with ASP.NET Core, then
there are links to everything that is needed for this.
Launch Visual Studio, create a new web application:
')


Web application is ready. If desired, you can run it.
Getting started
Models
Let's start with the models. Let's take out their classes in a separate project - the
AspNetCoreStorage.Data.Models class
library :

Add a class to our only
Item model:
public class Item { public int Id { get; set; } public string Name { get; set; } }
For our example, this is enough.
Storage Interaction Abstractions
Now we will proceed directly to the interaction with the repository, which will be implemented in our web application using two design patterns -
Unit of Work and
Repository . Simplified implementation of these templates means that interaction with the repository within a single request will be guaranteed to be performed in a single repository context, and a separate repository will be created for each model containing all the necessary methods for manipulating it.
To enable simple switching between different implementations of interacting with the repository, our web application should not use any specific implementation directly. Instead, all interaction with the repository should be done through a layer of abstractions. We describe it in the
AspNetCoreStorage.Data.Abstractions class
library (we will create the corresponding project).
First,
let's add the
IStorageContext interface without any properties or methods:
public interface IStorageContext { }
Classes that implement this interface will directly describe the storage (for example, a database with a connection string to it).
Next, add an
IStorage interface. It contains two methods -
GetRepository and
Save :
public interface IStorage { T GetRepository<T>() where T : IRepository; void Save(); }
This interface describes the implementation of the design pattern of the Unit of Work. The object of the class implementing this interface will be the only access point to the repository and must exist in a single instance as part of a single request to the web application. For the creation of this object we will be responsible for the built-in ASP.NET Core DI.
The
GetRepository method will find and return a repository of the corresponding type (for the corresponding model), and the Save method will
detect changes made by all repositories.
Finally, add the
IRepository interface with the only
SetStorageContext method:
public interface IRepository { void SetStorageContext(IStorageContext storageContext); }
Obviously, this interface describes the classes of the repositories. At the time of the repository request, an object of the class implementing the
IStorage interface will transfer the single storage context to the returned repository using the
SetStorageContext method
so that all calls to the repository are made within this single context, as we said above.
On this common interfaces are described. Now we add the repository interface of our only
Item model
, the IItemRepository . This interface contains only one method -
All :
public interface IItemRepository : IRepository { IEnumerable<Item> All(); }
In a real web application, the methods
Create ,
Edit ,
Delete , some methods for extracting objects by various parameters and so on could also be described here, but in our simplified example there is no need for them.
Specific implementations of interaction with the repository: enumeration in memory
As we have already agreed above, we will have two implementations of interaction with the repository: based on the SQLite database and based on the enumeration in memory. Let's start with the second, as it is easier. We will describe it in the
AspNetCoreStorage.Data.Mock class
library (we will create the corresponding project).
We will need to implement 3 interfaces from our abstraction layer:
IStorageContext ,
IStorage, and
IItemRepository (since IItemRepository extends IRepository).
The implementation of the
IStorageContext interface in the case of enumeration in memory will not contain any code, it is just an empty class, so let's go straight to
IStorage . The class is small, so we present it here in its entirety:
public class Storage : IStorage { public StorageContext StorageContext { get; private set; } public Storage() { this.StorageContext = new StorageContext(); } public T GetRepository<T>() where T : IRepository { foreach (Type type in this.GetType().GetTypeInfo().Assembly.GetTypes()) { if (typeof(T).GetTypeInfo().IsAssignableFrom(type) && type.GetTypeInfo().IsClass) { T repository = (T)Activator.CreateInstance(type); repository.SetStorageContext(this.StorageContext); return repository; } } return default(T); } public void Save() {
As you can see, the class contains the
StorageContext property, which is initialized in the constructor. The
GetRepository method
enumerates all types of the current assembly in search of the implementation of the repository interface specified by parameter
T. If a suitable type is found, the corresponding repository object is created, its
SetStorageContext method is
called, and then this object is returned. The
Save method does nothing. (Actually, we could not use StorageContext at all in this implementation, passing null to SetStorageContext, but leave it for consistency.)
Now let's look at the implementation of the
IItemRepository interface:
public class ItemRepository : IItemRepository { public readonly IList<Item> items; public ItemRepository() { this.items = new List<Item>(); this.items.Add(new Item() { Id = 1, Name = "Mock item 1" }); this.items.Add(new Item() { Id = 2, Name = "Mock item 2" }); this.items.Add(new Item() { Id = 3, Name = "Mock item 3" }); } public void SetStorageContext(IStorageContext storageContext) {
Everything is very simple. The
All method returns a set of elements from the
items variable, which is initialized in the constructor. The
SetStorageContext method does nothing, since we do not need any context in this case.
Specific implementations of interaction with the repository: SQLite database
Now we are implementing the same interfaces, but for working with the SQLite database. This time, the implementation of
IStorageContext will require writing some code:
public class StorageContext : DbContext, IStorageContext { private string connectionString; public StorageContext(string connectionString) { this.connectionString = connectionString; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { base.OnConfiguring(optionsBuilder); optionsBuilder.UseSqlite(this.connectionString); } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Entity<Item>(etb => { etb.HasKey(e => e.Id); etb.Property(e => e.Id); etb.ForSqliteToTable("Items"); } ); } }
As you can see, besides the implementation of the
IStorageContext interface
, this class also inherits
DbContext , which represents the database context in the Entity Framework Core, whose methods
OnConfiguring and
OnModelCreating it overrides (we will not dwell on them). Also note the
connectionString variable.
The implementation of the
IStorage interface
is identical to the one above, except that you need to pass a connection string to the constructor of the
StorageContext class (of course, you should specify the connection string in this way incorrectly in a real application, you should take it from the configuration parameters):
this.StorageContext = new StorageContext("Data Source=..\\..\\..\\db.sqlite");
Also, the
Save method should now call the
SaveChanges method of the storage context inherited from
DbContext :
public void Save() { this.StorageContext.SaveChanges(); }
The implementation of the
IItemRepository interface
now looks like this:
public class ItemRepository : IItemRepository { private StorageContext storageContext; private DbSet<Item> dbSet; public void SetStorageContext(IStorageContext storageContext) { this.storageContext = storageContext as StorageContext; this.dbSet = this.storageContext.Set<Item>(); } public IEnumerable<Item> All() { return this.dbSet.OrderBy(i => i.Name); } }
The
SetStorageContext method takes a class object that implements the
IStorageContext interface and leads it to the
StorageContext (that is, to the specific implementation that this repository is aware of as it is part of it), then using the
Set method initializes the
dbSet variable that represents the table in the database SQLite data. The
All method this time returns real data from a database table using the
dbSet variable.
Of course, if we had more than one repository, it would be logical to make a general implementation in some RepositoryBase, where the parameter
T would describe the model type, parameterize the
dbSet and then be passed to the
Set method of the storage context.
Web application interaction with the repository
Now we are ready to slightly modify our web application to make it display the list of objects of our
Item class on the main page.
First, let's add links to both specific implementations of interaction with the repository in the
dependencies section of the
project.json file of the main project of the web application. The result will be something like this:
"dependencies": { "AspNetCoreStorage.Data.Mock": "1.0.0", "AspNetCoreStorage.Data.Sqlite": "1.0.0", "Microsoft.AspNetCore.Diagnostics": "1.0.0", "Microsoft.AspNetCore.Mvc": "1.0.1", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", "Microsoft.Extensions.Logging.Console": "1.0.0", "Microsoft.NETCore.App": { "version": "1.0.1", "type": "platform" } }
Now we’ll go to the
Startup class
ConfigureServices method and add the
IStorage service
registration for two different implementations (note one of them, note that implementations are registered using the
AddScoped method, which means that the lifetime of the object is one request):
public void ConfigureServices(IServiceCollection services) { services.AddMvc();
Now let's go to the
HomeController controller:
public class HomeController : Controller { private IStorage storage; public HomeController(IStorage storage) { this.storage = storage; } public ActionResult Index() { return this.View(this.storage.GetRepository<IItemRepository>().All()); } }
We added a
storage variable of type
IStorage and initialize it in the constructor. The built-in ASP.NET Core DI will transfer the registered implementation of the IStorage interface to the controller's constructor during its creation.
Next, in the
Index method, we get an accessible repository that implements the
IItemRepository interface (remember, all the repositories obtained in this way will have a single storage context due to the use of the Design Pattern of the Unit of Work) and pass the set of
Item objects to the view by retrieving them using the
All repository method.
Now we will display the resulting list of objects in the view. To do this, we specify the enumeration of the objects of the class
Item as a view model for the view, and then in the loop we will display the values of the
Name property of each of the objects:
@model IEnumerable<AspNetCoreStorage.Data.Models.Item> <h1>Items from the storage:</h1> <ul> @foreach (var item in this.Model) { <li>@item.Name</li> } </ul>
If we run our web application now, we should get the following result:

If we change the registration of the implementation of the
IStorage interface to another, then the result will change:

As you can see, everything works!
Conclusion
The built-in dependency injection (DI) mechanism in ASP.NET greatly simplifies the implementation of similar tasks to ours and makes it closer, simpler and more understandable to beginners. As for the Unit of Work and the Repository itself, for typical web applications it is the most successful solution for interacting with data, which simplifies team development and testing.
Test project posted on
GitHub .
about the author
Dmitry Sikorsky is the owner and head of the software development company Yubreinjans, as well as the co-owner of the Pizza Bake delivery service in Kiev.
Latest articles on ASP.NET Core
1.
Creating the web service front end for the application .
2.
Keeping up with the times: Using JWT in ASP.NET Core .
3.
ASP.NET Core on Nano Server .