📜 ⬆️ ⬇️

ASP.NET Core: An Example of Implementing Design Patterns Unit of Work and Repository

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() { // Do nothing } } 

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) { // Do nothing } public IEnumerable<Item> All() { return this.items.OrderBy(i => i.Name); } } 

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(); // Uncomment to use mock storage services.AddScoped(typeof(IStorage), typeof(AspNetCoreStorage.Data.Mock.Storage)); // Uncomment to use SQLite storage //services.AddScoped(typeof(IStorage), typeof(AspNetCoreStorage.Data.Sqlite.Storage)); } 

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 .

Source: https://habr.com/ru/post/312214/


All Articles