From 45b95a423ea4ffcb58f84f7b21a330ba32ead123 Mon Sep 17 00:00:00 2001 From: Kyle McMaster Date: Thu, 13 Jan 2022 12:48:24 -0500 Subject: [PATCH 1/3] Update abstract repository documentation --- .../usage/use-built-in-abstract-repository.md | 334 +++++++++++++++++- 1 file changed, 332 insertions(+), 2 deletions(-) diff --git a/docs/usage/use-built-in-abstract-repository.md b/docs/usage/use-built-in-abstract-repository.md index 680cd7ff..601a8c48 100644 --- a/docs/usage/use-built-in-abstract-repository.md +++ b/docs/usage/use-built-in-abstract-repository.md @@ -7,6 +7,336 @@ nav_order: 5 # How to use the Built In Abstract Repository -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. +## Introduction -(todo) +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). + +To use the abstract generic repository provided in this library, first define a repository class that inherits from `RepositoryBase` 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. + +```csharp +public class YourRepository : RepositoryBase where T : class +{ + private readonly YourDbContext dbContext; + + public YourRepository(YourDbContext dbContext) : base(dbContext) + { + this.dbContext = dbContext; + } + + // Not required to implement anything. Add additional functionalities if required. +} +``` + +It is important to remember to register this generic repository as a service with the application's Dependency Injection provider. + +```csharp +services.AddScoped(typeof(YourRepository<>)); +``` + +In the example below, two different services inject the same `YourRepository` 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. + +```csharp +public class HeroByNameContainsFilterSpec : Specification +{ + public HeroByNameContainsFilterSpec(string name) + { + if (!string.IsNullOrEmpty(name)) + { + Query.Where(h => h.Name.Contains(name)); + } + } +} + +public class HeroService +{ + private readonly YourRepository repository; + + public HeroService(YourRepository repository) + { + this.repository = repository; + } + + public async Task> GetHeroesFilteredByName(string name) + { + var spec = new HeroByNameContainsFilterSpec(name); + + return await repository.ListAsync(spec); + } +} + +public class CustomerByNameContainsFilterSpec : Specification +{ + public CustomerByNameContainsFilterSpec(string name) + { + if (!string.IsNullOrEmpty(name)) + { + Query.Where(c => c.Name.Contains(name)); + } + } +} + +public class CustomerService +{ + private readonly YourRepository repository; + + public CustomerService(YourRepository repository) + { + this.repository = repository; + } + + public async Task> GetCustomersFilteredByName(string name) + { + var spec = new CustomerByNameContainsFilterSpec(name); + + return await repository.ListAsync(spec); + } +} +``` + +## Features of `RepositoryBase` + +The section above introduced using `RepositoryBase` 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` 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. + +```csharp +public async Task Create(string name, string superPower, bool isAlive, bool isAvenger) +{ + var hero = new Hero(name, superPower, isAlive, isAvenger); + + await repository.AddAsync(hero); + + await respository.SaveChangesAsync(); + + return hero; +} +``` + +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. + +```csharp +public class HeroByNameSpec : Specification, ISingleResultSpecification +{ + public HeroByNameSpec(string name) + { + if (!string.IsNullOrEmpty(name)) + { + Query.Where(h => h.Name == name); + } + } +} + +public async Task GetById(int id) +{ + return await repository.GetByIdAsync(id); +} + +public async Task GetByName(string name) +{ + var spec = new HeroByNameSpec(name); + + return await repository.GetBySpecAsync(spec); +} +``` + +Next, a Hero can be updated using `UpdateAsync`. `HeroService` defines a method `SetIsAlive` that takes an existing Hero and updates the IsAlive property. + +```csharp +public async Task SetIsAlive(int id, bool isAlive) +{ + var hero = await repository.GetByIdAsync(id); + + hero.IsAlive = isAlive; + + await repository.UpdateAsync(hero); + + await respository.SaveChangesAsync(); + + return hero; +} +``` + +Removing Heroes can be done either by Hero using `DeleteAsync` or by collection using `DeleteRangeAsync`. + +```csharp +public async Task Delete(Hero hero) +{ + await repository.DeleteAsync(hero); + + await respository.SaveChangesAsync(); +} + +public async Task DeleteRange(Hero[] heroes) +{ + await repository.DeleteRangeAsync(heroes); + + await respository.SaveChangesAsync(); +} +``` + +The `RepositoryBase` also provides two common Linq operations `CountAsync` and `AnyAsync`. + +```csharp +public async Task SeedData(Hero[] heroes) +{ + // only seed if no Heroes exist + if (await repository.AnyAsync()) + { + return; + } + + // alternatively + if (await repository.CountAsync() > 0) + { + return; + } + + foreach (var hero in heroes) + { + await repository.AddAsync(hero); + } + + await repository.SaveChangesAsync(); +} +``` + +The full HeroService implementation is shown below. + +```csharp +public class HeroService +{ + private readonly YourRepository repository; + + public HeroService(YourRepository repository) + { + this.repository = repository; + } + + public async Task Create(string name, string superPower, bool isAlive, bool isAvenger) + { + var hero = new Hero(name, superPower, isAlive, isAvenger); + + await repository.AddAsync(hero); + + await repository.SaveChangesAsync(); + + return hero; + } + + public async Task Delete(Hero hero) + { + await repository.DeleteAsync(hero); + + await repository.SaveChangesAsync(); + } + + public async Task DeleteRange(List heroes) + { + await repository.DeleteRangeAsync(heroes); + + await repository.SaveChangesAsync(); + } + + public async Task GetById(int id) + { + return await repository.GetByIdAsync(id); + } + + public async Task GetByName(string name) + { + var spec = new HeroByNameSpec(name); + + return await repository.GetBySpecAsync(spec); + } + + public async Task> GetHeroesFilteredByName(string name) + { + var spec = new HeroByNameContainsFilterSpec(name); + + return await repository.ListAsync(spec); + } + + public async Task SetIsAlive(int id, bool isAlive) + { + var hero = await repository.GetByIdAsync(id); + + hero.IsAlive = isAlive; + + await repository.UpdateAsync(hero); + + await repository.SaveChangesAsync(); + + return hero; + } + + public async Task SeedData(Hero[] heroes) + { + // only seed if no Heroes exist + if (await repository.AnyAsync()) + { + return; + } + + // alternatively + if (await repository.CountAsync() > 0) + { + return; + } + + foreach (var hero in heroes) + { + await repository.AddAsync(hero); + } + + await repository.SaveChangesAsync(); + } +} +``` + +## Putting it all together + +The following sample program puts the methods described above together. Note the handling of dependencies is excluded for brevity. + +```csharp +public async Task Run() +{ + var seedData = new[] + { + new Hero( + name: "Batman", + superPower: "Intelligence", + isAlive: true, + isAvenger: false), + new Hero( + name: "Iron Man", + superPower: "Intelligence", + isAlive: true, + isAvenger: true), + new Hero( + name: "Spiderman", + superPower: "Spidey Sense", + isAlive: true, + isAvenger: true), + }; + + await heroService.SeedData(seedData); + + var captainAmerica = await heroService.Create("Captain America", "Shield", true, true); + + var ironMan = await heroService.GetByName("Iron Man"); + + var alsoIronMan = await heroService.GetById(ironMan.Id); + + await heroService.SetIsAlive(ironMan.Id, false); + + var shouldOnlyContainBatman = await heroService.GetHeroesFilteredByName("Bat"); + + await heroService.Delete(captainAmerica); + + var allRemainingHeroes = await heroService.GetHeroesFilteredByName(""); + + await heroService.DeleteRange(allRemainingHeroes); +} +``` + +## Resources + +A in depth demo of a similar implementation of the Repository Pattern and `RepositoryBase` can be found in the Repositories section of this [Pluralsight course](https://www.pluralsight.com/courses/domain-driven-design-fundamentals). From 980e2ff909375047570bbbd0b45df5976206bb79 Mon Sep 17 00:00:00 2001 From: Kyle McMaster Date: Thu, 13 Jan 2022 14:08:20 -0500 Subject: [PATCH 2/3] Update use-built-in-abstract-repository.md --- .../usage/use-built-in-abstract-repository.md | 103 +++++++++--------- 1 file changed, 54 insertions(+), 49 deletions(-) diff --git a/docs/usage/use-built-in-abstract-repository.md b/docs/usage/use-built-in-abstract-repository.md index 601a8c48..d0b3c882 100644 --- a/docs/usage/use-built-in-abstract-repository.md +++ b/docs/usage/use-built-in-abstract-repository.md @@ -36,31 +36,36 @@ services.AddScoped(typeof(YourRepository<>)); In the example below, two different services inject the same `YourRepository` 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. ```csharp -public class HeroByNameContainsFilterSpec : Specification +public class HeroByNameAndSuperPowerContainsFilterSpec : Specification { - public HeroByNameContainsFilterSpec(string name) + public HeroByNameAndSuperPowerContainsFilterSpec(string name, string superPower) { if (!string.IsNullOrEmpty(name)) { Query.Where(h => h.Name.Contains(name)); } + + if (!string.IsNullOrEmpty(superPower)) + { + Query.Where(h => h.SuperPower.Contains(superPower)); + } } } public class HeroService { - private readonly YourRepository repository; + private readonly YourRepository heroRepository; - public HeroService(YourRepository repository) + public HeroService(YourRepository heroRepository) { - this.repository = repository; + this.heroRepository = heroRepository; } - public async Task> GetHeroesFilteredByName(string name) + public async Task> GetHeroesFilteredByNameAndSuperPower(string name, string superPower) { - var spec = new HeroByNameContainsFilterSpec(name); + var spec = new HeroByNameAndSuperPowerContainsFilterSpec(name, superPower); - return await repository.ListAsync(spec); + return await heroRepository.ListAsync(spec); } } @@ -77,18 +82,18 @@ public class CustomerByNameContainsFilterSpec : Specification public class CustomerService { - private readonly YourRepository repository; + private readonly YourRepository customerRepository; - public CustomerService(YourRepository repository) + public CustomerService(YourRepository customerRepository) { - this.repository = repository; + this.customerRepository = customerRepository; } public async Task> GetCustomersFilteredByName(string name) { var spec = new CustomerByNameContainsFilterSpec(name); - return await repository.ListAsync(spec); + return await customerRepository.ListAsync(spec); } } ``` @@ -102,9 +107,9 @@ public async Task Create(string name, string superPower, bool isAlive, boo { var hero = new Hero(name, superPower, isAlive, isAvenger); - await repository.AddAsync(hero); + await heroRepository.AddAsync(hero); - await respository.SaveChangesAsync(); + await heroRepository.SaveChangesAsync(); return hero; } @@ -126,14 +131,14 @@ public class HeroByNameSpec : Specification, ISingleResultSpecification public async Task GetById(int id) { - return await repository.GetByIdAsync(id); + return await heroRepository.GetByIdAsync(id); } public async Task GetByName(string name) { var spec = new HeroByNameSpec(name); - return await repository.GetBySpecAsync(spec); + return await heroRepository.GetBySpecAsync(spec); } ``` @@ -146,9 +151,9 @@ public async Task SetIsAlive(int id, bool isAlive) hero.IsAlive = isAlive; - await repository.UpdateAsync(hero); + await heroRepository.UpdateAsync(hero); - await respository.SaveChangesAsync(); + await heroRepository.SaveChangesAsync(); return hero; } @@ -159,16 +164,16 @@ Removing Heroes can be done either by Hero using `DeleteAsync` or by collection ```csharp public async Task Delete(Hero hero) { - await repository.DeleteAsync(hero); + await heroRepository.DeleteAsync(hero); - await respository.SaveChangesAsync(); + await heroRepository.SaveChangesAsync(); } public async Task DeleteRange(Hero[] heroes) { - await repository.DeleteRangeAsync(heroes); + await heroRepository.DeleteRangeAsync(heroes); - await respository.SaveChangesAsync(); + await heroRepository.SaveChangesAsync(); } ``` @@ -178,23 +183,23 @@ The `RepositoryBase` also provides two common Linq operations `CountAsync` an public async Task SeedData(Hero[] heroes) { // only seed if no Heroes exist - if (await repository.AnyAsync()) + if (await heroRepository.AnyAsync()) { return; } // alternatively - if (await repository.CountAsync() > 0) + if (await heroRepository.CountAsync() > 0) { return; } foreach (var hero in heroes) { - await repository.AddAsync(hero); + await heroRepository.AddAsync(hero); } - await repository.SaveChangesAsync(); + await heroRepository.SaveChangesAsync(); } ``` @@ -203,66 +208,66 @@ The full HeroService implementation is shown below. ```csharp public class HeroService { - private readonly YourRepository repository; + private readonly YourRepository heroRepository; - public HeroService(YourRepository repository) + public HeroService(YourRepository heroRepository) { - this.repository = repository; + this.heroRepository = heroRepository; } public async Task Create(string name, string superPower, bool isAlive, bool isAvenger) { var hero = new Hero(name, superPower, isAlive, isAvenger); - await repository.AddAsync(hero); + await heroRepository.AddAsync(hero); - await repository.SaveChangesAsync(); + await heroRepository.SaveChangesAsync(); return hero; } public async Task Delete(Hero hero) { - await repository.DeleteAsync(hero); + await heroRepository.DeleteAsync(hero); - await repository.SaveChangesAsync(); + await heroRepository.SaveChangesAsync(); } public async Task DeleteRange(List heroes) { - await repository.DeleteRangeAsync(heroes); + await heroRepository.DeleteRangeAsync(heroes); - await repository.SaveChangesAsync(); + await heroRepository.SaveChangesAsync(); } public async Task GetById(int id) { - return await repository.GetByIdAsync(id); + return await heroRepository.GetByIdAsync(id); } public async Task GetByName(string name) { var spec = new HeroByNameSpec(name); - return await repository.GetBySpecAsync(spec); + return await heroRepository.GetBySpecAsync(spec); } - public async Task> GetHeroesFilteredByName(string name) + public async Task> GetHeroesFilteredByNameAndSuperPower(string name, string superPower) { - var spec = new HeroByNameContainsFilterSpec(name); + var spec = new HeroByNameAndSuperPowerFilterSpec(name, superPower); - return await repository.ListAsync(spec); + return await heroRepository.ListAsync(spec); } public async Task SetIsAlive(int id, bool isAlive) { - var hero = await repository.GetByIdAsync(id); + var hero = await heroRepository.GetByIdAsync(id); hero.IsAlive = isAlive; - await repository.UpdateAsync(hero); + await heroRepository.UpdateAsync(hero); - await repository.SaveChangesAsync(); + await heroRepository.SaveChangesAsync(); return hero; } @@ -270,23 +275,23 @@ public class HeroService public async Task SeedData(Hero[] heroes) { // only seed if no Heroes exist - if (await repository.AnyAsync()) + if (!await heroRepository.AnyAsync()) { return; } // alternatively - if (await repository.CountAsync() > 0) + if (await heroRepository.CountAsync() > 0) { return; } foreach (var hero in heroes) { - await repository.AddAsync(hero); + await heroRepository.AddAsync(hero); } - await repository.SaveChangesAsync(); + await heroRepository.SaveChangesAsync(); } } ``` @@ -327,11 +332,11 @@ public async Task Run() await heroService.SetIsAlive(ironMan.Id, false); - var shouldOnlyContainBatman = await heroService.GetHeroesFilteredByName("Bat"); + var shouldOnlyContainBatman = await heroService.GetHeroesFilteredByNameAndSuperPower("Bat", "Intel"); await heroService.Delete(captainAmerica); - var allRemainingHeroes = await heroService.GetHeroesFilteredByName(""); + var allRemainingHeroes = await heroService.GetHeroesFilteredByNameAndSuperPower("", ""); await heroService.DeleteRange(allRemainingHeroes); } From 8945dcd497b0b93a169328b271452f863f9734c4 Mon Sep 17 00:00:00 2001 From: Kyle McMaster Date: Thu, 13 Jan 2022 14:11:51 -0500 Subject: [PATCH 3/3] Update docs/usage/use-built-in-abstract-repository.md Co-authored-by: Steve Smith --- docs/usage/use-built-in-abstract-repository.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/usage/use-built-in-abstract-repository.md b/docs/usage/use-built-in-abstract-repository.md index d0b3c882..31100fe6 100644 --- a/docs/usage/use-built-in-abstract-repository.md +++ b/docs/usage/use-built-in-abstract-repository.md @@ -344,4 +344,4 @@ public async Task Run() ## Resources -A in depth demo of a similar implementation of the Repository Pattern and `RepositoryBase` can be found in the Repositories section of this [Pluralsight course](https://www.pluralsight.com/courses/domain-driven-design-fundamentals). +An in-depth demo of a similar implementation of the Repository Pattern and `RepositoryBase` can be found in the Repositories section of this [Pluralsight course](https://www.pluralsight.com/courses/domain-driven-design-fundamentals).