The Generic repository pattern enables us to dynamically change the type so that we don't have to create duplicate methods. However, one downside is that we can use features provided by Entity Framework such as 'Include' to show the data of related entities in the response. But, as always, there is a way around this, and today we will use a pattern called 'Specification'.
List of Contents
- Project Set Up
- Implementation
- Getting Referenced Data from the Primary Entity with a Specification Pattern
Project Set Up
Creating Application
Adding Relationships to DB
For better demonstration, I have created three entities
▶ Adding Base Entity
We will create a common entity for all the entities we are going to use in this example. So that we can remove common properties from each entity. Another purpose of creating a base entity is because it makes it easier to specify a type limitation in the generic interface later.
Add a new class under the entity folder
Add the common properties in the base entity and remove them from each entity
Add the base entity as a parent to each entity
▶ Data Files for Testing
You can download the seed data for testing
Repository Pattern
Generic Repository Pattern
▶ Updating Type Limitation in the Generic Interface
Go to the interface class under the core folder and update the type limitation to the base entity
▶ Updating Type Limitation in the Generic Repository
Go to the repository class under the infrastructure folder and update the type limitation to the base entity
Implementation
Adding Methods in the Interface
Replace the previous methods that each gets a list of entities with one method that uses generic with dynamic type <T>
Implementing Interface
Go to the repository and implement the methods
Then update the method with the desired code. Use the code below to set a dynamic type
.Set<T>()
Adding Services
Register the service in the Program.cs file as shown below
builder.Services.AddScoped(typeof(IGenericRepo<>), typeof(GenericRepo<>));
Updating the Controller
Add additional endpoints for added entities. To specify the path use parenthesis and put a path in it as a string
[HttpGet("targets")]
Go to the controller class and inject a generic interface repository for each entity separately to specify a type for each request and get the desired result
private readonly IGenericRepo<Item> _itemRepo;
private readonly IGenericRepo<ItemTarget> _itemTargetRepo;
private readonly IGenericRepo<ItemType> _itemTypeRepo;
public StoreController(
IGenericRepo<Item> itemRepo,
IGenericRepo<ItemTarget> itemTargetRepo,
IGenericRepo<ItemType> itemTypeRepo)
{
this._itemTypeRepo = itemTypeRepo;
this._itemTargetRepo = itemTargetRepo;
this._itemRepo = itemRepo;
}
Update the methods
Run
Move to the API folder
cd /API
And run the app
dotnet watch
You will see added endpoints right away like below if you are using Swagger
Open each and try
Getting Referenced Data from the Primary Entity with a Specification Pattern
As you can see, referenced data in the primary entity show a 'null' value. We need an additional step to show their data. The thing is, however, with the generic pattern, we cannot use the 'Include' syntax directly from within the generic repository method.
A way to get around this issue is to use a pattern called 'specification'. Specification pattern allows us to limit the scope of the generic pattern and to add options. Let's see how it is done.
First, we need to change our request to an asynchronous way.
Asynchronous Request
Creating a Specification
Create a folder to hold specifications classes
Add a specification interface in the folder
Add generic type to the interface
Add methods that return a type with a generic expression (here I have added one for getting an individual item and the other for getting a list of items)
Expression<Func<T, bool>> Criteria { get; }
List<Expression<Func<T, object>>> Includes { get; }
▶ Base Specification
Add a class file that we will use as the base
Implement the interface
Update the methods with {get;} at the end. For the include method, initialize it as shown below. And add two constructors with and without a parameter
new List<Expression<Func<T, object>>>();
Add another method that includes entities
protected void AddInclude(Expression<Func<T, object>> includeExpression) {
Includes.Add(includeExpression);
}
Go to the infrastructure folder and create a class that we will use to include entities
Add a generic type and a type condition
Add the static method as shown below
public static IQueryable<T> GetQuery(IQueryable<T> input, ISpec<T> spec)
{
var query = input;
if (spec.Criteria != null)
{
query = query.Where(spec.Criteria);
}
query = spec.Includes.Aggregate(query, (entity, expression) => entity.Include(expression));
return query;
}
Go to the generic interface and add the specification as a parameter to the methods
Task<List<T>> GetListAsync(ISpec<T> spec);
Task<T> GetItem(ISpec<T> spec);
Go to the generic repository
Add another method that applies the specification
private IQueryable<T> ApplySpec(ISpec<T> spec)
{
return SpecEvaluator<T>.GetQuery(_context.Set<T>().AsQueryable(), spec);
}
Update the existing methods with the specification parameter and use the newly created method to pass the specification
public async Task<List<T>> GetListAsync(ISpec<T> spec)
{
return await ApplySpec(spec).ToListAsync();
}
▶ List Specification (Include)
Create an item specification class in the core folder
Inherit from the base specification
Add a constructor that includes entities
public ItemWithTargetAndTypeSpec()
{
AddInclude(x => x.ItemTarget);
AddInclude(x => x.ItemType);
}
Updating the Controller
Go to the controller class and update the method that gets a list of items like the one below.
[HttpGet]
public async Task<List<Item>> GetItems()
{
var spec = new ItemWithTargetAndTypeSpec();
return await _itemRepo.GetListAsync(spec);
}
▶ Getting an Individual item
Let's see how we can get an item too.
Update the 'GetItem' method that we implemented above to use the specification method
public async Task<T> GetItem(ISpec<T> spec)
{
return await ApplySpec(spec).FirstOrDefaultAsync();
}
Go to the individual specification class and add the constructor with the parameter we created earlier
Replace the automatically added parameter with an id of an int type. Then add code that includes referenced entities
public ItemWithTargetAndTypeSpec(int id) : base(x => x.Id == id) {
AddInclude(x => x.ItemTarget);
AddInclude(x => x.ItemType);
}
Go to the controller class and add an endpoint with an id parameter. Instantiate the individual specification and pass it as a parameter to the generic method that gets an item
[HttpGet("{id}")]
public async Task<ActionResult<Item>> GetItem(int id)
{
var spec = new ItemWithTargetAndTypeSpec(id);
return await _itemRepo.GetItem(spec);
}
We have seen how we can get data from an entity that has reference to other entities with a generic pattern.
'Backend > .NET' 카테고리의 다른 글
Data Transfer Object (DTO) (0) | 2023.04.19 |
---|---|
Relational DB - Getting Data Asynchronous (0) | 2023.04.16 |
Relational DB - Getting Data including Data from Other Entities (Repository Pattern) (0) | 2023.04.12 |
Application Architecture - Generic Repository (0) | 2023.04.09 |
Application Architecture - Repository with Service (0) | 2023.04.07 |