Skip to content

Commit 5d8b036

Browse files
Update abstract repository documentation (#220)
* Update abstract repository documentation * Update use-built-in-abstract-repository.md * Update docs/usage/use-built-in-abstract-repository.md Co-authored-by: Steve Smith <steve@kentsmiths.com> Co-authored-by: Steve Smith <steve@kentsmiths.com>
1 parent f84e7b1 commit 5d8b036

File tree

1 file changed

+337
-2
lines changed

1 file changed

+337
-2
lines changed

docs/usage/use-built-in-abstract-repository.md

Lines changed: 337 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,341 @@ nav_order: 5
77

88
# How to use the Built In Abstract Repository
99

10-
Specifications shine when combined with the [Repository Pattern](https://deviq.com/design-patterns/repository-pattern). Get started using the one included in this package by following these steps.
10+
## Introduction
1111

12-
(todo)
12+
Specifications shine when combined with the [Repository Pattern](https://deviq.com/design-patterns/repository-pattern). Get started using the one included in this package by following these steps. This example builds off the steps described in the [Quick Start Guide](../getting-started/quick-start-guide.md).
13+
14+
To use the abstract generic repository provided in this library, first define a repository class that inherits from `RepositoryBase<T>` in the Infrastructure or data access layer of your project. An example of this is provided in the sample web application in the [Specification repo](https://github.com/ardalis/Specification/blob/main/sample/Ardalis.SampleApp.Infrastructure/Data/MyRepository.cs). By inheriting from this base class, the generic repository class can now be used with any Entity supported by the provided DbContext. It also inherits many useful methods typically defined on a Repository without having to define them for each Entity type. This allows access to typical CRUD actions like `Add`, `Get`, `Update`, `Delete`, and `List` with minimal configuration and less duplicate code to maintain.
15+
16+
```csharp
17+
public class YourRepository<T> : RepositoryBase<T> where T : class
18+
{
19+
private readonly YourDbContext dbContext;
20+
21+
public YourRepository(YourDbContext dbContext) : base(dbContext)
22+
{
23+
this.dbContext = dbContext;
24+
}
25+
26+
// Not required to implement anything. Add additional functionalities if required.
27+
}
28+
```
29+
30+
It is important to remember to register this generic repository as a service with the application's Dependency Injection provider.
31+
32+
```csharp
33+
services.AddScoped(typeof(YourRepository<>));
34+
```
35+
36+
In the example below, two different services inject the same `YourRepository<T>` class but with different type arguments to perform similar actions. This allows for the creation of different services that can apply Specifications to collections of Entities without having to develop and maintain Repositories for each type.
37+
38+
```csharp
39+
public class HeroByNameAndSuperPowerContainsFilterSpec : Specification<Hero>
40+
{
41+
public HeroByNameAndSuperPowerContainsFilterSpec(string name, string superPower)
42+
{
43+
if (!string.IsNullOrEmpty(name))
44+
{
45+
Query.Where(h => h.Name.Contains(name));
46+
}
47+
48+
if (!string.IsNullOrEmpty(superPower))
49+
{
50+
Query.Where(h => h.SuperPower.Contains(superPower));
51+
}
52+
}
53+
}
54+
55+
public class HeroService
56+
{
57+
private readonly YourRepository<Hero> heroRepository;
58+
59+
public HeroService(YourRepository<Hero> heroRepository)
60+
{
61+
this.heroRepository = heroRepository;
62+
}
63+
64+
public async Task<List<Hero>> GetHeroesFilteredByNameAndSuperPower(string name, string superPower)
65+
{
66+
var spec = new HeroByNameAndSuperPowerContainsFilterSpec(name, superPower);
67+
68+
return await heroRepository.ListAsync(spec);
69+
}
70+
}
71+
72+
public class CustomerByNameContainsFilterSpec : Specification<Customer>
73+
{
74+
public CustomerByNameContainsFilterSpec(string name)
75+
{
76+
if (!string.IsNullOrEmpty(name))
77+
{
78+
Query.Where(c => c.Name.Contains(name));
79+
}
80+
}
81+
}
82+
83+
public class CustomerService
84+
{
85+
private readonly YourRepository<Customer> customerRepository;
86+
87+
public CustomerService(YourRepository<Customer> customerRepository)
88+
{
89+
this.customerRepository = customerRepository;
90+
}
91+
92+
public async Task<List<Customer>> GetCustomersFilteredByName(string name)
93+
{
94+
var spec = new CustomerByNameContainsFilterSpec(name);
95+
96+
return await customerRepository.ListAsync(spec);
97+
}
98+
}
99+
```
100+
101+
## Features of `RepositoryBase<T>`
102+
103+
The section above introduced using `RepositoryBase<T>` to provide similar functionality across two entities and their services. This section aims to go into more detail about the methods made available by `RepositoryBase<T>` and provide some examples of their usages. Continuing with the HeroService example, it is possible to create heroes as follows using the `AddAsync` method. The `SaveChangesAsync` method exposes the underlying DbContext method of the same name to persist changes to the database.
104+
105+
```csharp
106+
public async Task<Hero> Create(string name, string superPower, bool isAlive, bool isAvenger)
107+
{
108+
var hero = new Hero(name, superPower, isAlive, isAvenger);
109+
110+
await heroRepository.AddAsync(hero);
111+
112+
await heroRepository.SaveChangesAsync();
113+
114+
return hero;
115+
}
116+
```
117+
118+
Now that a Hero has been created, it's possible to retrieve that Hero using either the Hero's Id or by using a Specification. Note that since the `HeroByNameSpec` returns a single Hero entity, the Specification inherits the interface `ISingleResultSpecification` which `GetBySpecAsync` uses to constrain the return type to a single Entity.
119+
120+
```csharp
121+
public class HeroByNameSpec : Specification<Hero>, ISingleResultSpecification
122+
{
123+
public HeroByNameSpec(string name)
124+
{
125+
if (!string.IsNullOrEmpty(name))
126+
{
127+
Query.Where(h => h.Name == name);
128+
}
129+
}
130+
}
131+
132+
public async Task<Hero> GetById(int id)
133+
{
134+
return await heroRepository.GetByIdAsync(id);
135+
}
136+
137+
public async Task<Hero> GetByName(string name)
138+
{
139+
var spec = new HeroByNameSpec(name);
140+
141+
return await heroRepository.GetBySpecAsync(spec);
142+
}
143+
```
144+
145+
Next, a Hero can be updated using `UpdateAsync`. `HeroService` defines a method `SetIsAlive` that takes an existing Hero and updates the IsAlive property.
146+
147+
```csharp
148+
public async Task<Hero> SetIsAlive(int id, bool isAlive)
149+
{
150+
var hero = await repository.GetByIdAsync(id);
151+
152+
hero.IsAlive = isAlive;
153+
154+
await heroRepository.UpdateAsync(hero);
155+
156+
await heroRepository.SaveChangesAsync();
157+
158+
return hero;
159+
}
160+
```
161+
162+
Removing Heroes can be done either by Hero using `DeleteAsync` or by collection using `DeleteRangeAsync`.
163+
164+
```csharp
165+
public async Task Delete(Hero hero)
166+
{
167+
await heroRepository.DeleteAsync(hero);
168+
169+
await heroRepository.SaveChangesAsync();
170+
}
171+
172+
public async Task DeleteRange(Hero[] heroes)
173+
{
174+
await heroRepository.DeleteRangeAsync(heroes);
175+
176+
await heroRepository.SaveChangesAsync();
177+
}
178+
```
179+
180+
The `RepositoryBase<T>` also provides two common Linq operations `CountAsync` and `AnyAsync`.
181+
182+
```csharp
183+
public async Task SeedData(Hero[] heroes)
184+
{
185+
// only seed if no Heroes exist
186+
if (await heroRepository.AnyAsync())
187+
{
188+
return;
189+
}
190+
191+
// alternatively
192+
if (await heroRepository.CountAsync() > 0)
193+
{
194+
return;
195+
}
196+
197+
foreach (var hero in heroes)
198+
{
199+
await heroRepository.AddAsync(hero);
200+
}
201+
202+
await heroRepository.SaveChangesAsync();
203+
}
204+
```
205+
206+
The full HeroService implementation is shown below.
207+
208+
```csharp
209+
public class HeroService
210+
{
211+
private readonly YourRepository<Hero> heroRepository;
212+
213+
public HeroService(YourRepository<Hero> heroRepository)
214+
{
215+
this.heroRepository = heroRepository;
216+
}
217+
218+
public async Task<Hero> Create(string name, string superPower, bool isAlive, bool isAvenger)
219+
{
220+
var hero = new Hero(name, superPower, isAlive, isAvenger);
221+
222+
await heroRepository.AddAsync(hero);
223+
224+
await heroRepository.SaveChangesAsync();
225+
226+
return hero;
227+
}
228+
229+
public async Task Delete(Hero hero)
230+
{
231+
await heroRepository.DeleteAsync(hero);
232+
233+
await heroRepository.SaveChangesAsync();
234+
}
235+
236+
public async Task DeleteRange(List<Hero> heroes)
237+
{
238+
await heroRepository.DeleteRangeAsync(heroes);
239+
240+
await heroRepository.SaveChangesAsync();
241+
}
242+
243+
public async Task<Hero> GetById(int id)
244+
{
245+
return await heroRepository.GetByIdAsync(id);
246+
}
247+
248+
public async Task<Hero> GetByName(string name)
249+
{
250+
var spec = new HeroByNameSpec(name);
251+
252+
return await heroRepository.GetBySpecAsync(spec);
253+
}
254+
255+
public async Task<List<Hero>> GetHeroesFilteredByNameAndSuperPower(string name, string superPower)
256+
{
257+
var spec = new HeroByNameAndSuperPowerFilterSpec(name, superPower);
258+
259+
return await heroRepository.ListAsync(spec);
260+
}
261+
262+
public async Task<Hero> SetIsAlive(int id, bool isAlive)
263+
{
264+
var hero = await heroRepository.GetByIdAsync(id);
265+
266+
hero.IsAlive = isAlive;
267+
268+
await heroRepository.UpdateAsync(hero);
269+
270+
await heroRepository.SaveChangesAsync();
271+
272+
return hero;
273+
}
274+
275+
public async Task SeedData(Hero[] heroes)
276+
{
277+
// only seed if no Heroes exist
278+
if (!await heroRepository.AnyAsync())
279+
{
280+
return;
281+
}
282+
283+
// alternatively
284+
if (await heroRepository.CountAsync() > 0)
285+
{
286+
return;
287+
}
288+
289+
foreach (var hero in heroes)
290+
{
291+
await heroRepository.AddAsync(hero);
292+
}
293+
294+
await heroRepository.SaveChangesAsync();
295+
}
296+
}
297+
```
298+
299+
## Putting it all together
300+
301+
The following sample program puts the methods described above together. Note the handling of dependencies is excluded for brevity.
302+
303+
```csharp
304+
public async Task Run()
305+
{
306+
var seedData = new[]
307+
{
308+
new Hero(
309+
name: "Batman",
310+
superPower: "Intelligence",
311+
isAlive: true,
312+
isAvenger: false),
313+
new Hero(
314+
name: "Iron Man",
315+
superPower: "Intelligence",
316+
isAlive: true,
317+
isAvenger: true),
318+
new Hero(
319+
name: "Spiderman",
320+
superPower: "Spidey Sense",
321+
isAlive: true,
322+
isAvenger: true),
323+
};
324+
325+
await heroService.SeedData(seedData);
326+
327+
var captainAmerica = await heroService.Create("Captain America", "Shield", true, true);
328+
329+
var ironMan = await heroService.GetByName("Iron Man");
330+
331+
var alsoIronMan = await heroService.GetById(ironMan.Id);
332+
333+
await heroService.SetIsAlive(ironMan.Id, false);
334+
335+
var shouldOnlyContainBatman = await heroService.GetHeroesFilteredByNameAndSuperPower("Bat", "Intel");
336+
337+
await heroService.Delete(captainAmerica);
338+
339+
var allRemainingHeroes = await heroService.GetHeroesFilteredByNameAndSuperPower("", "");
340+
341+
await heroService.DeleteRange(allRemainingHeroes);
342+
}
343+
```
344+
345+
## Resources
346+
347+
An in-depth demo of a similar implementation of the Repository Pattern and `RepositoryBase<T>` can be found in the Repositories section of this [Pluralsight course](https://www.pluralsight.com/courses/domain-driven-design-fundamentals).

0 commit comments

Comments
 (0)