├── docs ├── toc.yml ├── docs │ ├── toc.yml │ ├── efcore │ │ ├── introduction.md │ │ ├── getting-started.md │ │ ├── advanced-tutorials.md │ │ └── basic-tutorials.md │ └── abstractions │ │ ├── introduction.md │ │ └── getting-started.md ├── docfx.json └── index.md ├── tests ├── RepositoryPattern.EntityFrameworkCore.Tests │ ├── GlobalUsings.cs │ ├── Laboratory │ │ ├── InsideRelatedTestEntity.cs │ │ ├── TestEntity.cs │ │ ├── RelatedTestEntity.cs │ │ ├── TestDbContext.cs │ │ └── Lab.cs │ ├── RepositoryPattern.EntityFrameworkCore.Tests.csproj │ ├── UnitsOfWork │ │ └── UnitOfWorkTest.cs │ └── Repositories │ │ └── RepositoryOfTEntityTests.cs └── RepositoryPattern.Abstractions.Tests │ ├── Lab │ ├── NotImplementedRepo.cs │ ├── NotImplementedUoW.cs │ ├── TestEntity.cs │ ├── UnitOfWork.cs │ └── Repository.cs │ ├── RepositoryPattern.Abstractions.Tests.csproj │ └── Builder │ ├── UnitOfWorkOptionsTests.cs │ └── RepositoryOptionsTests.cs ├── .gitignore ├── src ├── RepositoryPattern.Abstractions │ ├── Extensions │ │ └── TypeExtensions.cs │ ├── Builder │ │ ├── UnitOfWorkOptions.cs │ │ ├── RepositoryOptions.cs │ │ └── RepositoryPatternBuilder.cs │ ├── DependencyInjection.cs │ ├── RepositoryPattern.Abstractions.csproj │ ├── README.md │ ├── Repositories │ │ └── IRepository.cs │ └── UnitOfWork │ │ └── IUnitOfWork.cs └── RepositoryPattern.EntityFrameworkCore │ ├── Extensions │ ├── CollectionExtensions.cs │ ├── RepositoryQueryExtensions.cs │ └── RepositoryPatternOptionsExtensions.cs │ ├── README.md │ ├── RepositoryPattern.EntityFrameworkCore.csproj │ ├── Repositories │ └── RepositoryOfTEntity.cs │ └── UnitsOfWork │ └── UnitOfWorkImpl.cs ├── LICENSE ├── .github └── workflows │ ├── abstraction-ci.yml │ ├── efcore-ci.yml │ ├── github-page-deploy.yml │ ├── abstraction-cd.yml │ └── efcore-cd.yml ├── RepositoryPattern.sln.DotSettings.user ├── RepositoryPattern.sln └── README.md /docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Docs 2 | href: docs/ 3 | - name: API 4 | href: api/ -------------------------------------------------------------------------------- /tests/RepositoryPattern.EntityFrameworkCore.Tests/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | obj/ 3 | /packages/ 4 | riderModule.iml 5 | /_ReSharper.Caches/ 6 | docs/_site 7 | /docs/api 8 | .idea/ -------------------------------------------------------------------------------- /tests/RepositoryPattern.Abstractions.Tests/Lab/NotImplementedRepo.cs: -------------------------------------------------------------------------------- 1 | namespace RepositoryPattern.Abstractions.Tests.Lab; 2 | 3 | public class NotImplementedRepo 4 | { 5 | 6 | } -------------------------------------------------------------------------------- /tests/RepositoryPattern.Abstractions.Tests/Lab/NotImplementedUoW.cs: -------------------------------------------------------------------------------- 1 | namespace RepositoryPattern.Abstractions.Tests.Lab; 2 | 3 | public class NotImplementedUoW 4 | { 5 | 6 | } -------------------------------------------------------------------------------- /tests/RepositoryPattern.Abstractions.Tests/Lab/TestEntity.cs: -------------------------------------------------------------------------------- 1 | namespace RepositoryPattern.Abstractions.Tests.Lab; 2 | 3 | public class TestEntity 4 | { 5 | public int Id { get; set; } 6 | } -------------------------------------------------------------------------------- /tests/RepositoryPattern.EntityFrameworkCore.Tests/Laboratory/InsideRelatedTestEntity.cs: -------------------------------------------------------------------------------- 1 | namespace RepositoryPattern.EntityFrameworkCore.Tests.Laboratory; 2 | 3 | public class InsideRelatedTestEntity 4 | { 5 | public required int Id { get; set; } 6 | 7 | public ICollection RelatedTestEntities { get; set; } = new HashSet(); 8 | } -------------------------------------------------------------------------------- /src/RepositoryPattern.Abstractions/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace RepositoryPattern.Abstractions.Extensions; 2 | 3 | internal static class TypeExtensions 4 | { 5 | public static bool IsImplementingRepository(this Type type, Type interfaceType) 6 | => type.GetInterfaces().Any(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType); 7 | 8 | public static bool IsImplementingUnitOfWork(this Type type, Type interfaceType) 9 | => type.GetInterfaces().Any(i => i == interfaceType); 10 | } -------------------------------------------------------------------------------- /docs/docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: Repository Pattern Abstraction 2 | href: abstractions/introduction.md 3 | items: 4 | - name: Getting Started 5 | href: abstractions/getting-started.md 6 | - name: Repository Pattern Implementation for EFCore 7 | href: efcore/introduction.md 8 | items: 9 | - name: Getting Started 10 | href: efcore/getting-started.md 11 | - name: Basic Tutorials 12 | href: efcore/basic-tutorials.md 13 | - name: Advanced Usage 14 | href: efcore/advanced-tutorials.md -------------------------------------------------------------------------------- /tests/RepositoryPattern.EntityFrameworkCore.Tests/Laboratory/TestEntity.cs: -------------------------------------------------------------------------------- 1 | namespace RepositoryPattern.EntityFrameworkCore.Tests.Laboratory; 2 | 3 | public class TestEntity 4 | { 5 | public required int Id { get; set; } 6 | public required string Name { get; set; } 7 | public string? Description { get; set; } 8 | 9 | public virtual ICollection RelatedTestEntities { get; set; } = new HashSet(); 10 | 11 | public override bool Equals(object? obj) 12 | => obj is TestEntity entity && Equals(entity); 13 | 14 | protected bool Equals(TestEntity other) 15 | { 16 | return Id == other.Id && Name == other.Name && Description == other.Description; 17 | } 18 | 19 | public override int GetHashCode() 20 | { 21 | return HashCode.Combine(Id, Name, Description); 22 | } 23 | } -------------------------------------------------------------------------------- /tests/RepositoryPattern.EntityFrameworkCore.Tests/Laboratory/RelatedTestEntity.cs: -------------------------------------------------------------------------------- 1 | namespace RepositoryPattern.EntityFrameworkCore.Tests.Laboratory; 2 | 3 | public class RelatedTestEntity 4 | { 5 | public required int Id { get; set; } 6 | public required string Name { get; set; } 7 | public string? Description { get; set; } 8 | 9 | public int? TestEntityId { get; set; } 10 | public TestEntity? TestEntity { get; set; } 11 | 12 | public int? InsideRelatedTestEntityId { get; set; } 13 | public InsideRelatedTestEntity? InsideRelatedTestEntity { get; set; } 14 | 15 | public override bool Equals(object? obj) 16 | => obj is TestEntity entity && Equals(entity); 17 | 18 | protected bool Equals(TestEntity other) 19 | { 20 | return Id == other.Id && Name == other.Name && Description == other.Description; 21 | } 22 | 23 | public override int GetHashCode() 24 | { 25 | return HashCode.Combine(Id, Name, Description); 26 | } 27 | } -------------------------------------------------------------------------------- /tests/RepositoryPattern.Abstractions.Tests/RepositoryPattern.Abstractions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /docs/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "src": "../src", 7 | "files": [ 8 | "**/*.csproj" 9 | ] 10 | } 11 | ], 12 | "dest": "api", 13 | "allowCompilationErrors": true, 14 | "outputFormat": "markdown" 15 | } 16 | ], 17 | "build": { 18 | "content": [ 19 | { 20 | "files": [ 21 | "**/*.{md,yml}" 22 | ], 23 | "exclude": [ 24 | "_site/**" 25 | ] 26 | } 27 | ], 28 | "resource": [ 29 | { 30 | "files": [ 31 | "images/**" 32 | ] 33 | } 34 | ], 35 | "output": "_site", 36 | "template": [ 37 | "default", 38 | "modern" 39 | ], 40 | "globalMetadata": { 41 | "_appName": "Repository Pattern", 42 | "_appTitle": "Repository Pattern", 43 | "_enableSearch": true, 44 | "_appFooter": "Copyright © 2024 Carlos J. Ortiz", 45 | "_disableContribution": true, 46 | "_lang": "EN", 47 | "pdf": false 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Carlos J. Ortiz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/abstraction-ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: Repository Pattern Abstractions CI 5 | 6 | on: 7 | push: 8 | branches: [ "develop" ] 9 | pull_request: 10 | branches: [ "develop" ] 11 | 12 | env: 13 | PATH_PROJECT: src\RepositoryPattern.Abstractions 14 | PROJECT_NAME: RepositoryPattern.Abstractions.csproj 15 | PATH_TEST: tests\RepositoryPattern.Abstractions.Tests 16 | 17 | jobs: 18 | build: 19 | name: Build & Test project 20 | runs-on: windows-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Setting up .NET 24 | uses: actions/setup-dotnet@v3 25 | with: 26 | dotnet-version: | 27 | 8.x 28 | 7.x 29 | 6.x 30 | # cache: true 31 | - name: Restoring dependencies 32 | run: dotnet restore 33 | working-directory: ${{ env.PATH_PROJECT }} 34 | - name: Building the project 35 | run: dotnet build --no-restore --configuration Debug 36 | working-directory: ${{ env.PATH_PROJECT }} 37 | # - name: Running the Unit Tests 38 | # run: dotnet test --verbosity normal 39 | # working-directory: ${{ env.PATH_TEST }} -------------------------------------------------------------------------------- /.github/workflows/efcore-ci.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: Repository Pattern EFCore CI 5 | 6 | on: 7 | push: 8 | branches: [ "develop" ] 9 | pull_request: 10 | branches: [ "develop" ] 11 | 12 | env: 13 | PATH_PROJECT: src\RepositoryPattern.EntityFrameworkCore 14 | PROJECT_NAME: RepositoryPattern.EntityFrameworkCore.csproj 15 | PATH_TEST: tests\RepositoryPattern.EntityFrameworkCore.Tests 16 | 17 | jobs: 18 | build: 19 | name: Build & Test project 20 | runs-on: windows-latest 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Setting up .NET 24 | uses: actions/setup-dotnet@v3 25 | with: 26 | dotnet-version: | 27 | 8.x 28 | 7.x 29 | 6.x 30 | # cache: true 31 | - name: Restoring dependencies 32 | run: dotnet restore 33 | working-directory: ${{ env.PATH_PROJECT }} 34 | - name: Building the project 35 | run: dotnet build --no-restore --configuration Debug 36 | working-directory: ${{ env.PATH_PROJECT }} 37 | # - name: Running the Unit Tests 38 | # run: dotnet test --verbosity normal 39 | # working-directory: ${{ env.PATH_TEST }} -------------------------------------------------------------------------------- /tests/RepositoryPattern.Abstractions.Tests/Builder/UnitOfWorkOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using RepositoryPattern.Abstractions.Builder; 4 | 5 | namespace RepositoryPattern.Abstractions.Tests.Builder; 6 | 7 | public class UnitOfWorkOptionsTests 8 | { 9 | [Fact] 10 | public void CreateUnitOfWorkOptions_ShouldBeSuccessful() 11 | { 12 | // Act 13 | var options = new UnitOfWorkOptions(typeof(Lab.UnitOfWork)); 14 | 15 | // Assert 16 | options.Should().NotBeNull(); 17 | options.ImplementationType.Should().Be(typeof(Lab.UnitOfWork)); 18 | options.Lifetime.Should().Be(ServiceLifetime.Scoped); 19 | } 20 | 21 | [Fact] 22 | public void CreateUnitOfWorkOptions_ShouldThrowArgumentException() 23 | { 24 | // Act 25 | Action act = () => new UnitOfWorkOptions(typeof(ArgumentException)); 26 | 27 | // Assert 28 | act.Should().Throw(); 29 | } 30 | 31 | [Fact] 32 | public void CreateUnitOfWorkOptions_ShouldBeSuccessful_WithLifetime() 33 | { 34 | // Act 35 | var options = new UnitOfWorkOptions(typeof(Lab.UnitOfWork)).UseLifetime(ServiceLifetime.Singleton); 36 | 37 | // Assert 38 | options.Should().NotBeNull(); 39 | options.ImplementationType.Should().Be(typeof(Lab.UnitOfWork)); 40 | options.Lifetime.Should().Be(ServiceLifetime.Singleton); 41 | } 42 | } -------------------------------------------------------------------------------- /tests/RepositoryPattern.Abstractions.Tests/Builder/RepositoryOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using RepositoryPattern.Abstractions.Builder; 4 | using RepositoryPattern.Abstractions.Tests.Lab; 5 | 6 | namespace RepositoryPattern.Abstractions.Tests.Builder; 7 | 8 | public class RepositoryOptionsTests 9 | { 10 | [Fact] 11 | public void CreateRepositoryOptions_ShouldBeSuccessful() 12 | { 13 | // Act 14 | var options = new RepositoryOptions(typeof(Repository<>)); 15 | 16 | 17 | // Assert 18 | options.Should().NotBeNull(); 19 | options.ImplementationType.Should().Be(typeof(Repository<>)); 20 | options.Lifetime.Should().Be(ServiceLifetime.Scoped); 21 | } 22 | 23 | [Fact] 24 | public void CreateRepositoryOptions_ShouldThrowArgumentException() 25 | { 26 | // Act 27 | Action act = () => new RepositoryOptions(typeof(NotImplementedRepo)); 28 | 29 | // Assert 30 | act.Should().Throw(); 31 | } 32 | 33 | [Fact] 34 | public void CreateRepositoryOptions_ShouldBeSuccessful_WithLifetime() 35 | { 36 | // Act 37 | var options = new RepositoryOptions(typeof(Repository<>)).UseLifetime(ServiceLifetime.Singleton); 38 | 39 | // Assert 40 | options.Should().NotBeNull(); 41 | options.ImplementationType.Should().Be(typeof(Repository<>)); 42 | options.Lifetime.Should().Be(ServiceLifetime.Singleton); 43 | } 44 | } -------------------------------------------------------------------------------- /.github/workflows/github-page-deploy.yml: -------------------------------------------------------------------------------- 1 | # Your GitHub workflow file under .github/workflows/ 2 | 3 | name: Github Page deploy 4 | 5 | # Trigger the action on push to master 6 | on: 7 | push: 8 | branches: ["master"] 9 | 10 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 11 | permissions: 12 | actions: read 13 | pages: write 14 | id-token: write 15 | 16 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 17 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 18 | concurrency: 19 | group: "pages" 20 | cancel-in-progress: false 21 | 22 | jobs: 23 | publish-docs: 24 | name: Publish docs 25 | environment: 26 | name: github-pages 27 | url: ${{ steps.deployment.outputs.page_url }} 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Checkout 31 | uses: actions/checkout@v3 32 | - name: Dotnet Setup 33 | uses: actions/setup-dotnet@v3 34 | with: 35 | dotnet-version: 8.x 36 | 37 | - run: dotnet tool update -g docfx 38 | - run: docfx docs/docfx.json 39 | 40 | - name: Upload artifact 41 | uses: actions/upload-pages-artifact@v3 42 | with: 43 | # Upload entire repository 44 | path: 'docs/_site' 45 | - name: Deploy to GitHub Pages 46 | id: deployment 47 | uses: actions/deploy-pages@v4 48 | -------------------------------------------------------------------------------- /tests/RepositoryPattern.EntityFrameworkCore.Tests/RepositoryPattern.EntityFrameworkCore.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | all 22 | 23 | 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | all 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/RepositoryPattern.EntityFrameworkCore.Tests/Laboratory/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace RepositoryPattern.EntityFrameworkCore.Tests.Laboratory; 4 | 5 | public class TestDbContext : DbContext 6 | { 7 | public DbSet TestEntities { get; set; } 8 | public DbSet RelatedTestEntities { get; set; } 9 | public DbSet InsideRelatedTestEntities { get; set; } 10 | 11 | public TestDbContext(DbContextOptions options) 12 | : base(options) { } 13 | 14 | protected override void OnModelCreating(ModelBuilder builder) 15 | { 16 | builder.Entity() 17 | .HasKey(e => e.Id); 18 | 19 | builder.Entity() 20 | .Property(e => e.Id) 21 | .IsRequired(); 22 | 23 | builder.Entity() 24 | .Property(e => e.Name) 25 | .IsRequired() 26 | .HasMaxLength(120); 27 | 28 | builder.Entity() 29 | .Property(e => e.Description) 30 | .HasMaxLength(250); 31 | 32 | builder.Entity() 33 | .HasMany() 34 | .WithOne(r => r.TestEntity) 35 | .HasForeignKey(r => r.TestEntityId); 36 | 37 | builder.Entity() 38 | .HasKey(e => e.Id); 39 | 40 | builder.Entity() 41 | .HasOne() 42 | .WithMany(ir => ir.RelatedTestEntities) 43 | .HasForeignKey(r => r.InsideRelatedTestEntityId); 44 | 45 | builder.Entity() 46 | .HasKey(e => e.Id); 47 | 48 | base.OnModelCreating(builder); 49 | } 50 | } -------------------------------------------------------------------------------- /tests/RepositoryPattern.EntityFrameworkCore.Tests/Laboratory/Lab.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using RepositoryPattern.EntityFrameworkCore.Repositories; 3 | 4 | namespace RepositoryPattern.EntityFrameworkCore.Tests.Laboratory; 5 | 6 | internal static class Lab 7 | { 8 | public static IEnumerable TestEntities => 9 | [ 10 | new TestEntity { Id = 1, Name = "Test 1", RelatedTestEntities = [RelatedTestEntities[0]] }, 11 | new TestEntity { Id = 2, Name = "Test 2", RelatedTestEntities = [RelatedTestEntities[1], RelatedTestEntities[2]]}, 12 | new TestEntity { Id = 3, Name = "Test 3", RelatedTestEntities = [RelatedTestEntities[3], RelatedTestEntities[4], RelatedTestEntities[5]]}, 13 | new TestEntity { Id = 4, Name = "Test 4"}, 14 | new TestEntity { Id = 5, Name = "Test 5"}, 15 | new TestEntity { Id = 6, Name = "Test 6"}, 16 | new TestEntity { Id = 7, Name = "Test 7"}, 17 | new TestEntity { Id = 8, Name = "Test 8"}, 18 | new TestEntity { Id = 9, Name = "Test 9"}, 19 | new TestEntity { Id = 10, Name = "Test 10"} 20 | ]; 21 | 22 | public static List RelatedTestEntities => 23 | [ 24 | new RelatedTestEntity { Id = 1, Name = "Related 1", TestEntityId = 1 }, 25 | new RelatedTestEntity { Id = 2, Name = "Related 2", TestEntityId = 2 }, 26 | new RelatedTestEntity { Id = 3, Name = "Related 3", TestEntityId = 2 }, 27 | new RelatedTestEntity { Id = 4, Name = "Related 4", TestEntityId = 3 }, 28 | new RelatedTestEntity { Id = 5, Name = "Related 5", TestEntityId = 3 }, 29 | new RelatedTestEntity { Id = 6, Name = "Related 6", TestEntityId = 3 } 30 | ]; 31 | } -------------------------------------------------------------------------------- /src/RepositoryPattern.EntityFrameworkCore/Extensions/CollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace RepositoryPattern.EntityFrameworkCore.Extensions; 4 | 5 | /// 6 | /// Extension class for collections. 7 | /// 8 | public static class CollectionExtensions 9 | { 10 | /// 11 | /// Provides access to the to facilitate navigation 12 | /// among the properties of the object within the collection. The returned object is solely for 13 | /// compliance with a requirement where this method will be called, and no operations should be 14 | /// performed on this returned object. 15 | /// 16 | /// The type of the entities in the collection. 17 | /// The type of the property to navigate to. 18 | /// The source collection. 19 | /// An expression indicating the property path for navigation. 20 | /// An object for compliance with method calling requirements; not intended for further operations. 21 | /// Thrown if or is null. 22 | public static object Then(this ICollection src, 23 | Expression> navigationPropertyPath) 24 | { 25 | ArgumentNullException.ThrowIfNull(src); 26 | ArgumentNullException.ThrowIfNull(navigationPropertyPath); 27 | 28 | return navigationPropertyPath; 29 | } 30 | } -------------------------------------------------------------------------------- /src/RepositoryPattern.Abstractions/Builder/UnitOfWorkOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using RepositoryPattern.Abstractions.Extensions; 3 | using RepositoryPattern.Abstractions.UnitOfWork; 4 | 5 | namespace RepositoryPattern.Abstractions.Builder; 6 | 7 | /// 8 | /// Represents the options for configuring the implementation types for the unit of work. 9 | /// 10 | public class UnitOfWorkOptions 11 | { 12 | /// 13 | /// Configures the unit of work implementation type to be used. 14 | /// 15 | /// 16 | /// The type to be used for the unit of work implementation. 17 | /// 18 | /// 19 | /// Thrown when the provided type does not implement the interface. 20 | /// 21 | public UnitOfWorkOptions(Type implementationType) 22 | { 23 | if (!implementationType.IsImplementingUnitOfWork(typeof(IUnitOfWork))) 24 | throw new ArgumentException("The provided type must be class that implement the IUnitOfWork interface"); 25 | ImplementationType = implementationType; 26 | } 27 | 28 | /// 29 | /// Configures the lifetime of the service. 30 | /// 31 | /// The lifetime of the service. 32 | /// The current instance of the . 33 | public UnitOfWorkOptions UseLifetime(ServiceLifetime lifetime) 34 | { 35 | Lifetime = lifetime; 36 | return this; 37 | } 38 | 39 | internal Type ImplementationType { get; private set; } 40 | internal ServiceLifetime Lifetime { get; private set; } = ServiceLifetime.Scoped; 41 | } -------------------------------------------------------------------------------- /docs/docs/efcore/introduction.md: -------------------------------------------------------------------------------- 1 | # Repository Pattern implementation for Entity Framework Core 2 | 3 | ![NuGet Version](https://img.shields.io/nuget/v/Qrtix.RepositoryPattern.EntityFrameworkCore?style=flat&logo=nuget) 4 | ![NuGet Downloads](https://img.shields.io/nuget/dt/Qrtix.RepositoryPattern.EntityFrameworkCore?style=flat&logo=nuget) 5 | ![GitHub Repo stars](https://img.shields.io/github/stars/carlosjortiz/RepositoryPattern?style=flat&logo=github) 6 | 7 | The Repository Pattern for Entity Framework Core is a versatile .NET library designed to simplify data access and 8 | promote best practices in software architecture. By providing a robust implementation of generic repositories and unit 9 | of work patterns, this library empowers developers to build scalable, maintainable, and efficient data-driven 10 | applications. 11 | 12 | With Entity Framework Core as its backbone, this library offers seamless integration with various database providers, 13 | making it suitable for a wide range of projects. Whether you're working on a small application or a large enterprise 14 | system, the Repository Pattern for Entity Framework Core provides the tools you need to manage your data access layer 15 | effectively. 16 | 17 | In this documentation, you'll find detailed information on how to get started with the library, including installation 18 | instructions, setup guides for creating and configuring your DbContext, and examples of how to inject repository pattern 19 | services into your application using dependency injection. 20 | 21 | Explore the documentation to learn more about the features and capabilities of the Repository Pattern for Entity 22 | Framework Core, and discover how it can streamline your development workflow and improve the maintainability of your 23 | codebase. 24 | -------------------------------------------------------------------------------- /src/RepositoryPattern.Abstractions/Builder/RepositoryOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using RepositoryPattern.Abstractions.Extensions; 4 | using RepositoryPattern.Abstractions.Repositories; 5 | 6 | [assembly: InternalsVisibleTo("RepositoryPattern.Abstractions.Tests")] 7 | 8 | namespace RepositoryPattern.Abstractions.Builder; 9 | 10 | /// 11 | /// Represents the options for configuring the implementation types for the repository pattern. 12 | /// 13 | public class RepositoryOptions 14 | { 15 | /// 16 | /// Configures the repository implementation type to be used. 17 | /// 18 | /// 19 | /// The type to be used for the repository implementation. 20 | /// 21 | /// 22 | /// Thrown when the provided type does not implement the interface. 23 | /// 24 | public RepositoryOptions(Type implementationType) 25 | { 26 | if (!implementationType.IsImplementingRepository(typeof(IRepository<>))) 27 | throw new ArgumentException("The provided type must be class that implement the IRepository interface"); 28 | ImplementationType = implementationType; 29 | } 30 | 31 | /// 32 | /// Configures the lifetime of the service. 33 | /// 34 | /// The lifetime of the service. 35 | /// The current instance of the . 36 | public RepositoryOptions UseLifetime(ServiceLifetime lifetime) 37 | { 38 | Lifetime = lifetime; 39 | return this; 40 | } 41 | 42 | internal Type ImplementationType { get; private set; } 43 | internal ServiceLifetime Lifetime { get; private set; } = ServiceLifetime.Scoped; 44 | } -------------------------------------------------------------------------------- /src/RepositoryPattern.EntityFrameworkCore/Extensions/RepositoryQueryExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using System.Runtime.CompilerServices; 3 | using Microsoft.EntityFrameworkCore; 4 | 5 | [assembly: InternalsVisibleTo("RepositoryPattern.EntityFrameworkCore.Tests")] 6 | namespace RepositoryPattern.EntityFrameworkCore.Extensions; 7 | 8 | internal static class RepositoryQueryExtensions 9 | { 10 | internal static IQueryable WithTrackingOption(this IQueryable query, bool disableTracking = false) 11 | where T : class 12 | { 13 | ArgumentNullException.ThrowIfNull(query); 14 | 15 | return disableTracking ? query.AsNoTracking() : query; 16 | } 17 | 18 | internal static IQueryable WithIncludedProperties(this IQueryable query, 19 | IEnumerable>>? includes = null) where T : class 20 | { 21 | ArgumentNullException.ThrowIfNull(query); 22 | 23 | if (includes is null) 24 | return query; 25 | 26 | return includes.Aggregate(query, (current, includeExpression) => 27 | { 28 | var path = includeExpression.ToString(); 29 | var split = path.Split(".").AsEnumerable(); 30 | split = split.Where(s => !s.Contains("=>") && !s.Contains("Then(")) 31 | .Select(s => s.Trim('(', ')')); 32 | path = string.Join(".", split); 33 | return current.Include(path); 34 | }); 35 | } 36 | 37 | internal static IQueryable ApplyOrdering(this IQueryable query, 38 | Func, IOrderedQueryable>? orderBy = null) 39 | { 40 | ArgumentNullException.ThrowIfNull(query); 41 | 42 | return orderBy?.Invoke(query) ?? query; 43 | } 44 | 45 | internal static IQueryable ApplyFiltering(this IQueryable query, 46 | Expression>[]? filters = null) 47 | { 48 | ArgumentNullException.ThrowIfNull(query); 49 | 50 | return filters?.Aggregate(query, (current, filter) => current.Where(filter)) 51 | ?? query; 52 | } 53 | } -------------------------------------------------------------------------------- /src/RepositoryPattern.Abstractions/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection.Extensions; 3 | using RepositoryPattern.Abstractions.Builder; 4 | using RepositoryPattern.Abstractions.Repositories; 5 | using RepositoryPattern.Abstractions.UnitOfWork; 6 | 7 | namespace RepositoryPattern.Abstractions; 8 | 9 | /// 10 | /// Contain the methods to integrate this library. 11 | /// 12 | public static class DependencyInjection 13 | { 14 | /// 15 | /// Integrate and configure the requisite dependencies and services for seamless operation of this library. 16 | /// 17 | /// An to add the requisite dependencies and services. 18 | /// 19 | /// 20 | /// An with the requisite dependencies and services for seamless operation of 21 | /// this library. 22 | /// 23 | /// Thrown when the is null 24 | /// Thrown when the repository pattern options are not configured or has invalid values 25 | public static IServiceCollection AddRepositoryPattern(this IServiceCollection services, 26 | Action builder) 27 | { 28 | ArgumentNullException.ThrowIfNull(services); 29 | 30 | var options = new RepositoryPatternBuilder(); 31 | builder(options); 32 | 33 | options.IsValid(); 34 | 35 | services.TryAdd(new ServiceDescriptor(typeof(IRepository<>), 36 | options.Repository!.ImplementationType, 37 | options.Repository.Lifetime)); 38 | 39 | services.TryAdd(new ServiceDescriptor(typeof(IUnitOfWork), options.UnitOfWork!.ImplementationType, 40 | options.UnitOfWork.Lifetime)); 41 | 42 | return services; 43 | } 44 | } -------------------------------------------------------------------------------- /src/RepositoryPattern.Abstractions/RepositoryPattern.Abstractions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net7.0;net6.0 5 | enable 6 | enable 7 | 8 | Qrtix.RepositoryPattern.Abstractions 9 | 7.0.3 10 | 11 | - Fix readme. 12 | 13 | 14 | default 15 | true 16 | true 17 | Carlos J. Ortiz 18 | 19 | This library provides a convenient 20 | abstraction to emplement the repository pattern in your .NET applications. By using this 21 | library, you can decouple your data access logic from your business logic, resulting in a more maintainable 22 | and testable codebase. 23 | 24 | https://github.com/carlosjortiz/RepositoryPattern 25 | true 26 | RepositoryPattern.Abstractions 27 | true 28 | README.md 29 | git 30 | MIT 31 | 32 | RepositoryPattern, Repository, Pattern 33 | 34 | Copyright (c) Carlos J. Ortiz. All rights reserved. 35 | https://github.com/users/carlosjortiz/projects/1 36 | 37 | 38 | 39 | bin\Debug\RepositoryPattern.xml 40 | 41 | 42 | 43 | bin\Release\RepositoryPattern.xml 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /tests/RepositoryPattern.Abstractions.Tests/Lab/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using RepositoryPattern.Abstractions.Repositories; 2 | using RepositoryPattern.Abstractions.UnitOfWork; 3 | 4 | namespace RepositoryPattern.Abstractions.Tests.Lab; 5 | 6 | public class UnitOfWork : IUnitOfWork 7 | { 8 | public void Dispose() 9 | { 10 | throw new NotImplementedException(); 11 | } 12 | 13 | public ValueTask DisposeAsync() 14 | { 15 | throw new NotImplementedException(); 16 | } 17 | 18 | public bool SupportsSavePoints { get; } 19 | public IRepository Repository() where TEntity : class 20 | { 21 | throw new NotImplementedException(); 22 | } 23 | 24 | public void BeginTransaction(bool force = false) 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | 29 | public Task BeginTransactionAsync(bool force = false, CancellationToken cancellationToken = default) 30 | { 31 | throw new NotImplementedException(); 32 | } 33 | 34 | public void RollBack() 35 | { 36 | throw new NotImplementedException(); 37 | } 38 | 39 | public Task RollBackAsync(CancellationToken cancellationToken = default) 40 | { 41 | throw new NotImplementedException(); 42 | } 43 | 44 | public void RollBackToSavePoint(string name) 45 | { 46 | throw new NotImplementedException(); 47 | } 48 | 49 | public Task RollBackToSavePointAsync(string name, CancellationToken cancellationToken = default) 50 | { 51 | throw new NotImplementedException(); 52 | } 53 | 54 | public void Commit() 55 | { 56 | throw new NotImplementedException(); 57 | } 58 | 59 | public Task CommitAsync(CancellationToken cancellationToken = default) 60 | { 61 | throw new NotImplementedException(); 62 | } 63 | 64 | public void CreateSavePoint(string name) 65 | { 66 | throw new NotImplementedException(); 67 | } 68 | 69 | public Task CreateSavePointAsync(string name, CancellationToken cancellationToken = default) 70 | { 71 | throw new NotImplementedException(); 72 | } 73 | 74 | public void ReleaseSavePoint(string name) 75 | { 76 | throw new NotImplementedException(); 77 | } 78 | 79 | public Task ReleaseSavePointAsync(string name, CancellationToken cancellationToken = default) 80 | { 81 | throw new NotImplementedException(); 82 | } 83 | 84 | public Task SaveAsync(CancellationToken cancellationToken = default) 85 | { 86 | throw new NotImplementedException(); 87 | } 88 | 89 | public int Save() 90 | { 91 | throw new NotImplementedException(); 92 | } 93 | } -------------------------------------------------------------------------------- /tests/RepositoryPattern.Abstractions.Tests/Lab/Repository.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Linq.Expressions; 3 | using RepositoryPattern.Abstractions.Repositories; 4 | 5 | namespace RepositoryPattern.Abstractions.Tests.Lab; 6 | 7 | public class Repository : IRepository where TEntity : class 8 | { 9 | public IEnumerator GetEnumerator() 10 | { 11 | throw new NotImplementedException(); 12 | } 13 | 14 | IEnumerator IEnumerable.GetEnumerator() 15 | { 16 | return GetEnumerator(); 17 | } 18 | 19 | public Type ElementType { get; } 20 | public Expression Expression { get; } 21 | public IQueryProvider Provider { get; } 22 | 23 | public IQueryable GetMany(IEnumerable>>? includes = null, bool disableTracking = false, Func, IOrderedQueryable>? orderBy = null, 24 | params Expression>[]? filters) 25 | { 26 | throw new NotImplementedException(); 27 | } 28 | 29 | public Task GetOneAsync(IEnumerable>>? includes = null, bool disableTracking = false, 30 | CancellationToken cancellationToken = default, params Expression>[] filters) 31 | { 32 | throw new NotImplementedException(); 33 | } 34 | 35 | public TEntity? GetOne(IEnumerable>>? includes = null, bool disableTracking = false, params Expression>[] filters) 36 | { 37 | throw new NotImplementedException(); 38 | } 39 | 40 | public Task AddOneAsync(TEntity entity, CancellationToken cancellationToken = default) 41 | { 42 | throw new NotImplementedException(); 43 | } 44 | 45 | public void AddOne(TEntity entity) 46 | { 47 | throw new NotImplementedException(); 48 | } 49 | 50 | public void AddMany(IEnumerable entities) 51 | { 52 | throw new NotImplementedException(); 53 | } 54 | 55 | public void UpdateOne(TEntity entity) 56 | { 57 | throw new NotImplementedException(); 58 | } 59 | 60 | public void UpdateMany(IEnumerable entities) 61 | { 62 | throw new NotImplementedException(); 63 | } 64 | 65 | public void RemoveOne(TEntity entity) 66 | { 67 | throw new NotImplementedException(); 68 | } 69 | 70 | public void RemoveOne(Expression>[] filters) 71 | { 72 | throw new NotImplementedException(); 73 | } 74 | 75 | public void RemoveMany(IEnumerable entities) 76 | { 77 | throw new NotImplementedException(); 78 | } 79 | 80 | public void RemoveMany(Expression>[] filters) 81 | { 82 | throw new NotImplementedException(); 83 | } 84 | } -------------------------------------------------------------------------------- /.github/workflows/abstraction-cd.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: Repository Pattern Abstractions CD 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | 10 | env: 11 | PATH_PROJECT: src\RepositoryPattern.Abstractions 12 | PATH_RELEASE: src\RepositoryPattern.Abstractions\bin\Release 13 | PROJECT_NAME: RepositoryPattern.Abstractions.csproj 14 | 15 | jobs: 16 | publish: 17 | name: Publish Lib 18 | runs-on: windows-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Setting up .NET 22 | uses: actions/setup-dotnet@v3 23 | with: 24 | dotnet-version: | 25 | 8.x 26 | 7.x 27 | 6.x 28 | # cache: true 29 | - name: Restoring dependencies 30 | run: dotnet restore 31 | working-directory: ${{ env.PATH_PROJECT }} 32 | - name: Building the project 33 | run: dotnet build --no-restore --configuration Release 34 | working-directory: ${{ env.PATH_PROJECT }} 35 | - name: Creatnig NuGet Package 36 | run: dotnet pack ${{ env.PROJECT_NAME }} --no-build -c Release 37 | working-directory: ${{ env.PATH_PROJECT }} 38 | - name: Publishing into NuGet 39 | run: dotnet nuget push *.nupkg --skip-duplicate --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json 40 | working-directory: ${{ env.PATH_RELEASE }} 41 | - name: Publishing into Github Packages 42 | env: 43 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 44 | run: dotnet nuget push *.nupkg --skip-duplicate --api-key ${{ secrets.GH_TOKEN }} --source https://nuget.pkg.github.com/carlosjortiz/index.json 45 | working-directory: ${{ env.PATH_RELEASE }} 46 | # - name: Creating Release in Github 47 | # env: 48 | # GH_TOKEN: ${{ secrets.GH_TOKEN }} 49 | # REPO_NAME: ${{ github.event.repository.name }} 50 | # run: | 51 | # $xml = [xml](Get-Content ${{env.PROJECT_NAME}}) 52 | # $version = $xml.Project.PropertyGroup.Version 53 | # $notes = $xml.Project.PropertyGroup.PackageReleaseNotes 54 | # $repo = $env:GITHUB_REPOSITORY 55 | # gh release create "v$version" -t "${{ env.REPO_NAME}}-v${version}" -n "${notes}" -R "$repo" 56 | # Compress-Archive -Path ".\bin\Release\*" -DestinationPath ".\bin\Release\${{ env.REPO_NAME }}-v${version}.zip" 57 | # gh release upload "v${version}" .\bin\Release\${{ env.REPO_NAME }}-v${version}.zip --clobber 58 | # working-directory: ${{ env.PATH_PROJECT }} -------------------------------------------------------------------------------- /.github/workflows/efcore-cd.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a .NET project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net 3 | 4 | name: Repository Pattern EFCore CD 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | 10 | env: 11 | PATH_PROJECT: src\RepositoryPattern.EntityFrameworkCore 12 | PATH_RELEASE: src\RepositoryPattern.EntityFrameworkCore\bin\Release 13 | PROJECT_NAME: RepositoryPattern.EntityFrameworkCore.csproj 14 | 15 | jobs: 16 | publish: 17 | name: Publish Lib 18 | runs-on: windows-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - name: Setting up .NET 22 | uses: actions/setup-dotnet@v3 23 | with: 24 | dotnet-version: | 25 | 8.x 26 | 7.x 27 | 6.x 28 | # cache: true 29 | - name: Restoring dependencies 30 | run: dotnet restore 31 | working-directory: ${{ env.PATH_PROJECT }} 32 | - name: Building the project 33 | run: dotnet build --no-restore --configuration Release 34 | working-directory: ${{ env.PATH_PROJECT }} 35 | - name: Creatnig NuGet Package 36 | run: dotnet pack ${{ env.PROJECT_NAME }} --no-build -c Release 37 | working-directory: ${{ env.PATH_PROJECT }} 38 | - name: Publishing into NuGet 39 | run: dotnet nuget push *.nupkg --skip-duplicate --api-key ${{ secrets.NUGET_TOKEN }} --source https://api.nuget.org/v3/index.json 40 | working-directory: ${{ env.PATH_RELEASE }} 41 | - name: Publishing into Github Packages 42 | env: 43 | GH_TOKEN: ${{ secrets.GH_TOKEN }} 44 | run: dotnet nuget push *.nupkg --skip-duplicate --api-key ${{ secrets.GH_TOKEN }} --source https://nuget.pkg.github.com/carlosjortiz/index.json 45 | working-directory: ${{ env.PATH_RELEASE }} 46 | # - name: Creating Release in Github 47 | # env: 48 | # GH_TOKEN: ${{ secrets.GH_TOKEN }} 49 | # REPO_NAME: ${{ github.event.repository.name }} 50 | # run: | 51 | # $xml = [xml](Get-Content ${{env.PROJECT_NAME}}) 52 | # $version = $xml.Project.PropertyGroup.Version 53 | # $notes = $xml.Project.PropertyGroup.PackageReleaseNotes 54 | # $repo = $env:GITHUB_REPOSITORY 55 | # gh release create "v$version" -t "${{ env.REPO_NAME}}-v${version}" -n "${notes}" -R "$repo" 56 | # Compress-Archive -Path ".\bin\Release\*" -DestinationPath ".\bin\Release\${{ env.REPO_NAME }}-v${version}.zip" 57 | # gh release upload "v${version}" .\bin\Release\${{ env.REPO_NAME }}-v${version}.zip --clobber 58 | # working-directory: ${{ env.PATH_PROJECT }} -------------------------------------------------------------------------------- /docs/docs/efcore/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ![NuGet Version](https://img.shields.io/nuget/v/Qrtix.RepositoryPattern.EntityFrameworkCore?style=flat&logo=nuget) 4 | ![NuGet Downloads](https://img.shields.io/nuget/dt/Qrtix.RepositoryPattern.EntityFrameworkCore?style=flat&logo=nuget) 5 | ![GitHub Repo stars](https://img.shields.io/github/stars/carlosjortiz/RepositoryPattern?style=flat&logo=github) 6 | 7 | A library for implementing generic repositories and unit of work with Entity Framework Core. 8 | This implementation uses a single instance of the DbContext for all repositories to avoid concurrency issues. 9 | 10 | ## Table of Contents 11 | 12 | - [Installation](#installation) 13 | - [Creating the DbContext](#creating-the-dbcontext) 14 | - [Injecting the Repository Pattern](#injecting-the-repositorypatterns-services) 15 | 16 | ## Installation 17 | 18 | Using the NuGet package manager console within Visual Studio run the following command: 19 | 20 | ``` 21 | Install-Package Ortix.RepositoryPattern.EntityFrameworkCore 22 | ``` 23 | 24 | Or using the .NET Core CLI from a terminal window: 25 | 26 | ``` 27 | dotnet add package Qrtix.RepositoryPattern.EntityFrameworkCore 28 | ``` 29 | 30 | ## Creating the DbContext 31 | 32 | Create your DbContext inheriting from `Microsoft.EntityFrameworkCore.DbContext`: 33 | 34 | ```csharp 35 | public class MyDbContext : DbContext 36 | { 37 | public DbSet Products { get; set; } 38 | 39 | protected override void OnModelCreating(ModelBuilder modelBuilder) 40 | { 41 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 42 | base.OnModelCreating(modelBuilder); 43 | } 44 | } 45 | ``` 46 | 47 | Configure your DbContext in the `Program` class: 48 | 49 | ```csharp 50 | builder.Services.AddDbContext(opt => 51 | { 52 | opt.UseSqlServer(builder.Configuration.GetConnectionString("ConnectionString")); 53 | }); 54 | ``` 55 | 56 | ## Injecting the RepositoryPattern's Services 57 | 58 | Add the `RepositoryPattern` services to the Service Collection: 59 | 60 | 61 | ```csharp 62 | builder.Services.AddRepositoryPattern(options => { 63 | options.UseEntityFrameworkCore(); 64 | }); 65 | ``` 66 | 67 | The default scope for injected services is scoped. If you want to change it, refer to the next examples: 68 | 69 | Using the same lifetime: 70 | ```csharp 71 | builder.Services.AddRepositoryPattern(options => { 72 | options.UseEntityFrameworCore(ServiceLifetime.Singleton); 73 | }); 74 | ``` 75 | 76 | Using individual lifetime: 77 | ```csharp 78 | builder.Services.AddRepositoryPattern(options => { 79 | options.UseEntityFrameworCore(ServiceLifetime.Scoped, SeriviceLifetime.Singleton); 80 | }); 81 | ``` -------------------------------------------------------------------------------- /src/RepositoryPattern.EntityFrameworkCore/README.md: -------------------------------------------------------------------------------- 1 | # Repository Pattern implementation for Entity Framework Core 2 | 3 | ![NuGet Version](https://img.shields.io/nuget/v/Qrtix.RepositoryPattern.EntityFrameworkCore?style=flat&logo=nuget) 4 | ![NuGet Downloads](https://img.shields.io/nuget/dt/Qrtix.RepositoryPattern.EntityFrameworkCore?style=flat&logo=nuget) 5 | ![GitHub Repo stars](https://img.shields.io/github/stars/carlosjortiz/RepositoryPattern?style=flat&logo=github) 6 | 7 | A library for implementing generic repositories and unit of work with Entity Framework Core. 8 | This implementation uses a single instance of the DbContext for all repositories to avoid concurrency issues. 9 | 10 | Consult the online [documentation](https://carlosjortiz.github.io/RepositoryPattern/) for more details. 11 | 12 | ## Table of Contents 13 | 14 | - [Installation](#installation) 15 | - [Creating the DbContext](#creating-the-dbcontext) 16 | - [Injecting the Repository Pattern](#injecting-the-repositorypatterns-services) 17 | 18 | ## Installation 19 | 20 | Using the NuGet package manager console within Visual Studio run the following command: 21 | 22 | ``` 23 | Install-Package Ortix.RepositoryPattern.EntityFrameworkCore 24 | ``` 25 | 26 | Or using the .NET Core CLI from a terminal window: 27 | 28 | ``` 29 | dotnet add package Qrtix.RepositoryPattern.EntityFrameworkCore 30 | ``` 31 | 32 | ## Creating the DbContext 33 | 34 | Create your DbContext inheriting from `Microsoft.EntityFrameworkCore.DbContext`: 35 | 36 | ```csharp 37 | public class MyDbContext : DbContext 38 | { 39 | public DbSet Products { get; set; } 40 | 41 | protected override void OnModelCreating(ModelBuilder modelBuilder) 42 | { 43 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 44 | base.OnModelCreating(modelBuilder); 45 | } 46 | } 47 | ``` 48 | 49 | Configure your DbContext in the `Program` class: 50 | 51 | ```csharp 52 | builder.Services.AddDbContext(opt => 53 | { 54 | opt.UseSqlServer(builder.Configuration.GetConnectionString("ConnectionString")); 55 | }); 56 | ``` 57 | 58 | ## Injecting the RepositoryPattern's Services 59 | 60 | Add the `RepositoryPattern` services to the Service Collection: 61 | 62 | 63 | ```csharp 64 | builder.Services.AddRepositoryPattern(options => { 65 | options.UseEntityFrameworkCore(); 66 | }); 67 | ``` 68 | 69 | The default scope for injected services is scoped. If you want to change it, refer to the next examples: 70 | 71 | Using the same lifetime: 72 | ```csharp 73 | builder.Services.AddRepositoryPattern(options => { 74 | options.UseEntityFrameworCore(ServiceLifetime.Singleton); 75 | }); 76 | ``` 77 | 78 | Using individual lifetime: 79 | ```csharp 80 | builder.Services.AddRepositoryPattern(options => { 81 | options.UseEntityFrameworCore(ServiceLifetime.Scoped, SeriviceLifetime.Singleton); 82 | }); 83 | ``` -------------------------------------------------------------------------------- /RepositoryPattern.sln.DotSettings.user: -------------------------------------------------------------------------------- 1 | 2 | True 3 | 0BD4BE47-56DB-44E5-8147-8F661DFF2841 4 | 33ff99d8-b1ad-4ed5-a4fd-376b97c2c841 5 | <SessionState ContinuousTestingMode="0" Name="CreateUnitOfWorkOptions_ShouldBeSuccessful" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 6 | <TestAncestor> 7 | <TestId>xUnit::37DD8A3E-3CC2-48F9-9BF6-E691123EF8BC::net8.0::RepositoryPattern.Abstractions.Tests.Builder.UnitOfWorkOptionsTests.CreateUnitOfWorkOptions_ShouldBeSuccessful</TestId> 8 | </TestAncestor> 9 | </SessionState> 10 | <SessionState ContinuousTestingMode="0" Name="All tests from Solution #4" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 11 | <Solution /> 12 | </SessionState> 13 | <SessionState ContinuousTestingMode="0" Name="All tests from Solution #2" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 14 | <Solution /> 15 | </SessionState> 16 | <SessionState ContinuousTestingMode="0" IsActive="True" Name="All tests from Solution #3" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 17 | <Solution /> 18 | </SessionState> 19 | <SessionState ContinuousTestingMode="0" Name="All tests from Solution" xmlns="urn:schemas-jetbrains-com:jetbrains-ut-session"> 20 | <Solution /> 21 | </SessionState> 22 | 23 | -------------------------------------------------------------------------------- /src/RepositoryPattern.Abstractions/Builder/RepositoryPatternBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using RepositoryPattern.Abstractions.Repositories; 3 | using RepositoryPattern.Abstractions.UnitOfWork; 4 | 5 | namespace RepositoryPattern.Abstractions.Builder; 6 | 7 | /// 8 | /// Represents a builder for configuring the implementation types for the repository pattern. 9 | /// 10 | /// 11 | /// This builder allows configuring the types to be used for repository and unit of work implementations in the 12 | /// repository pattern. 13 | /// 14 | public class RepositoryPatternBuilder 15 | { 16 | 17 | internal RepositoryOptions? Repository { get; private set; } 18 | 19 | internal UnitOfWorkOptions? UnitOfWork { get; private set; } 20 | 21 | /// 22 | /// Configures the repository implementation type to be used. 23 | /// 24 | /// The type to be used for the repository implementation. 25 | /// The current instance of the . 26 | /// 27 | /// Thrown when the provided type does not implement the 28 | /// interface. 29 | /// 30 | public RepositoryOptions UseRepositoryImplementation(Type repositoryImplementationType) 31 | { 32 | Repository = new RepositoryOptions(repositoryImplementationType); 33 | return Repository; 34 | } 35 | 36 | /// 37 | /// Configures the Unit of work implementation type to be used. 38 | /// 39 | /// The type to be used for the unit of work implementation. 40 | /// The current instance of the . 41 | /// 42 | /// Thrown when the provided type does not implement the 43 | /// interface. 44 | /// 45 | public UnitOfWorkOptions UseUnitOfWorkImplementation(Type unitOfWorkImplementationType) 46 | { 47 | UnitOfWork = new UnitOfWorkOptions(unitOfWorkImplementationType); 48 | return UnitOfWork; 49 | } 50 | 51 | /// 52 | /// Configures the repository implementation type to be used. 53 | /// 54 | /// The type to be used for the unit of work implementation. 55 | /// The current instance of the . 56 | public UnitOfWorkOptions UseUnitOfWorkImplementation() 57 | where TUnitOfWork : class, IUnitOfWork 58 | { 59 | UnitOfWork = new UnitOfWorkOptions(typeof(TUnitOfWork)); 60 | return UnitOfWork; 61 | } 62 | 63 | internal void IsValid() 64 | { 65 | if (Repository is null) 66 | throw new InvalidOperationException("The repository options has not been configured"); 67 | 68 | if (UnitOfWork is null) 69 | throw new InvalidOperationException("The unit of work options has not been configured"); 70 | } 71 | } -------------------------------------------------------------------------------- /RepositoryPattern.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{688023D7-6C66-4417-8DFB-47D88C08D543}" 4 | EndProject 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{43F786CE-16B2-4892-8AB1-9180439C7D94}" 6 | EndProject 7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepositoryPattern.Abstractions", "src\RepositoryPattern.Abstractions\RepositoryPattern.Abstractions.csproj", "{2B80873D-D3FB-4E31-822E-5E0A13F0E8ED}" 8 | EndProject 9 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepositoryPattern.EntityFrameworkCore", "src\RepositoryPattern.EntityFrameworkCore\RepositoryPattern.EntityFrameworkCore.csproj", "{3C38A352-40F7-4CAD-8ED5-51A748B3D144}" 10 | EndProject 11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepositoryPattern.EntityFrameworkCore.Tests", "tests\RepositoryPattern.EntityFrameworkCore.Tests\RepositoryPattern.EntityFrameworkCore.Tests.csproj", "{0BD4BE47-56DB-44E5-8147-8F661DFF2841}" 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "RepositoryPattern.Abstractions.Tests", "tests\RepositoryPattern.Abstractions.Tests\RepositoryPattern.Abstractions.Tests.csproj", "{37DD8A3E-3CC2-48F9-9BF6-E691123EF8BC}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(NestedProjects) = preSolution 21 | {2B80873D-D3FB-4E31-822E-5E0A13F0E8ED} = {688023D7-6C66-4417-8DFB-47D88C08D543} 22 | {3C38A352-40F7-4CAD-8ED5-51A748B3D144} = {688023D7-6C66-4417-8DFB-47D88C08D543} 23 | {0BD4BE47-56DB-44E5-8147-8F661DFF2841} = {43F786CE-16B2-4892-8AB1-9180439C7D94} 24 | {37DD8A3E-3CC2-48F9-9BF6-E691123EF8BC} = {43F786CE-16B2-4892-8AB1-9180439C7D94} 25 | EndGlobalSection 26 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 27 | {2B80873D-D3FB-4E31-822E-5E0A13F0E8ED}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 28 | {2B80873D-D3FB-4E31-822E-5E0A13F0E8ED}.Debug|Any CPU.Build.0 = Debug|Any CPU 29 | {2B80873D-D3FB-4E31-822E-5E0A13F0E8ED}.Release|Any CPU.ActiveCfg = Release|Any CPU 30 | {2B80873D-D3FB-4E31-822E-5E0A13F0E8ED}.Release|Any CPU.Build.0 = Release|Any CPU 31 | {3C38A352-40F7-4CAD-8ED5-51A748B3D144}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {3C38A352-40F7-4CAD-8ED5-51A748B3D144}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {3C38A352-40F7-4CAD-8ED5-51A748B3D144}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {3C38A352-40F7-4CAD-8ED5-51A748B3D144}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {0BD4BE47-56DB-44E5-8147-8F661DFF2841}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {0BD4BE47-56DB-44E5-8147-8F661DFF2841}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {0BD4BE47-56DB-44E5-8147-8F661DFF2841}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {0BD4BE47-56DB-44E5-8147-8F661DFF2841}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {37DD8A3E-3CC2-48F9-9BF6-E691123EF8BC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {37DD8A3E-3CC2-48F9-9BF6-E691123EF8BC}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {37DD8A3E-3CC2-48F9-9BF6-E691123EF8BC}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {37DD8A3E-3CC2-48F9-9BF6-E691123EF8BC}.Release|Any CPU.Build.0 = Release|Any CPU 43 | EndGlobalSection 44 | EndGlobal 45 | -------------------------------------------------------------------------------- /docs/docs/abstractions/introduction.md: -------------------------------------------------------------------------------- 1 | # Repository Pattern Abstractions 2 | 3 | ![NuGet Version](https://img.shields.io/nuget/v/Qrtix.RepositoryPattern.Abstractions?style=flat&logo=nuget) 4 | ![NuGet Downloads](https://img.shields.io/nuget/dt/Qrtix.RepositoryPattern.Abstractions?style=flat&logo=nuget) 5 | ![GitHub Repo stars](https://img.shields.io/github/stars/carlosjortiz/RepositoryPattern?style=flat&logo=github) 6 | 7 | Welcome to the documentation for Repository Pattern Abstractions, a versatile library designed to simplify the 8 | implementation of generic repositories and unit of work patterns with Entity Framework Core. This documentation serves 9 | as a comprehensive guide to understanding and using the features provided by the Repository Pattern Abstractions 10 | library. 11 | 12 | ## What is Repository Pattern Abstractions? 13 | 14 | Repository Pattern Abstractions is a versatile .NET library designed to streamline the implementation of the repository 15 | pattern and unit of work pattern in applications utilizing the ORM of your choice. By furnishing generic interfaces for 16 | repositories and unit of work, this library fosters the creation of cleaner, more maintainable data access code, while 17 | also encouraging the separation of concerns within your application architecture. 18 | 19 | ## Key Features 20 | 21 | - **Generic Repository Interface**: Define repository interfaces for different entity types using a single generic 22 | interface. 23 | - **Flexible Unit of Work Interface**: Manage database transactions and coordinate repository operations through a 24 | flexible unit of work interface. 25 | - **Dependency Injection Integration**: Seamlessly integrate the repository pattern services into your application using 26 | the built-in dependency injection container in ASP.NET Core. 27 | 28 | ## Getting Started 29 | 30 | To start using Repository Pattern Abstractions in your project, follow these simple steps: 31 | 32 | 1. **Installation**: Install the Repository Pattern Abstractions package via NuGet package manager or .NET CLI. 33 | 2. **Creating DbContext**: Create your DbContext class inheriting from `Microsoft.EntityFrameworkCore.DbContext`. 34 | 3. **Implementing Repositories**: Create repository implementations by implementing the `IRepository` 35 | interface. 36 | 4. **Implementing Unit of Work**: Implement the unit of work pattern by creating a class that implements 37 | the `IUnitOfWork` interface. 38 | 5. **Injecting Services**: Add the repository pattern services to your application's service collection using dependency 39 | injection. 40 | 41 | ## Explore the Documentation 42 | 43 | This documentation covers everything you need to know to effectively use Repository Pattern Abstractions in your 44 | projects. Browse the topics in the table of contents to learn about installation, configuration, usage guidelines, and 45 | best practices for implementing the repository pattern in your application. 46 | 47 | Whether you're a seasoned developer looking to streamline your data access layer or a newcomer exploring best practices 48 | in software architecture, the Repository Pattern Abstractions documentation is your go-to resource for mastering the 49 | repository pattern with Entity Framework Core. 50 | 51 | Let's dive in and discover how Repository Pattern Abstractions can simplify and enhance your data access code! 52 | -------------------------------------------------------------------------------- /src/RepositoryPattern.EntityFrameworkCore/RepositoryPattern.EntityFrameworkCore.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;net7.0;net6.0 5 | enable 6 | enable 7 | 8 | Qrtix.RepositoryPattern.EntityFrameworkCore 9 | 7.1.2 10 | 11 | - Update `RepositoryPattern.Abstractions` to version 7.0.3 12 | 13 | 14 | default 15 | true 16 | true 17 | Carlos J. Ortiz 18 | 19 | This library provides a convenient way to 20 | implement the repository pattern with Entity Framework Core in your .NET applications. By using this 21 | library, you can decouple your data access logic from your business logic, resulting in a more maintainable 22 | and testable codebase. 23 | 24 | https://github.com/carlosjortiz/RepositoryPattern 25 | true 26 | RepositoryPattern.EntityFrameworkCore 27 | true 28 | README.md 29 | git 30 | MIT 31 | 32 | EFCore, RepositoryPattern, EF, EFCore.RepositoryPattern, EFCore.RepositoryPattern, EntityFrameworkCore, EntityFramework.RepositoryPattern, 33 | RepositoryPattern, RepositoryPattern.Abstractions 34 | 35 | Copyright (c) Carlos J. Ortiz. All rights reserved. 36 | https://github.com/users/carlosjortiz/projects/1 37 | 38 | 39 | 40 | bin\Debug\RepositoryPattern.EntityFrameworkCore.xml 41 | 42 | 43 | 44 | bin\Release\RepositoryPattern.EntityFrameworkCore.xml 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/RepositoryPattern.EntityFrameworkCore/Repositories/RepositoryOfTEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Linq.Expressions; 3 | using System.Runtime.CompilerServices; 4 | using Microsoft.EntityFrameworkCore; 5 | using RepositoryPattern.Abstractions.Repositories; 6 | using RepositoryPattern.EntityFrameworkCore.Extensions; 7 | 8 | [assembly: InternalsVisibleTo("RepositoryPattern.EntityFrameworkCore.Tests")] 9 | 10 | namespace RepositoryPattern.EntityFrameworkCore.Repositories; 11 | 12 | internal class Repository : IRepository where TEntity : class 13 | { 14 | private readonly DbSet _dbSet; 15 | 16 | public Repository(DbContext context) 17 | { 18 | _dbSet = context.Set(); 19 | } 20 | 21 | 22 | public IQueryable GetMany(IEnumerable>>? includes = null, 23 | bool disableTracking = false, Func, IOrderedQueryable>? orderBy = null, 24 | params Expression>[]? filters) 25 | => _dbSet.WithTrackingOption(disableTracking) 26 | .WithIncludedProperties(includes) 27 | .ApplyFiltering(filters) 28 | .ApplyOrdering(orderBy); 29 | 30 | public async Task GetOneAsync(IEnumerable>>? includes = null, 31 | bool disableTracking = false, 32 | CancellationToken cancellationToken = default, 33 | params Expression>[] filters) 34 | => await _dbSet.WithTrackingOption(disableTracking) 35 | .WithIncludedProperties(includes) 36 | .ApplyFiltering(filters) 37 | .FirstOrDefaultAsync(cancellationToken); 38 | 39 | 40 | public TEntity? GetOne(IEnumerable>>? includes = null, 41 | bool disableTracking = false, params Expression>[] filters) 42 | => _dbSet.WithTrackingOption(disableTracking) 43 | .WithIncludedProperties(includes) 44 | .ApplyFiltering(filters) 45 | .FirstOrDefault(); 46 | 47 | 48 | public Task AddOneAsync(TEntity entity, 49 | CancellationToken cancellationToken = default) 50 | => _dbSet.AddAsync(entity, cancellationToken).AsTask(); 51 | 52 | public void AddOne(TEntity entity) => _dbSet.Add(entity); 53 | 54 | public Task AddManyAsync(IEnumerable entities, 55 | CancellationToken cancellationToken = default) 56 | => _dbSet.AddRangeAsync(entities, cancellationToken); 57 | 58 | public void AddMany(IEnumerable entities) => _dbSet.AddRange(entities); 59 | 60 | public void UpdateOne(TEntity entity) => _dbSet.Update(entity); 61 | 62 | public void UpdateMany(IEnumerable entities) => _dbSet.UpdateRange(entities); 63 | 64 | public void RemoveOne(TEntity entity) => _dbSet.Remove(entity); 65 | 66 | public void RemoveOne(Expression>[] filters) 67 | { 68 | var entity = GetOne(filters: filters); 69 | if (entity is null) 70 | throw new ArgumentException($"{typeof(TEntity).Name} not found", nameof(filters)); 71 | 72 | RemoveOne(entity); 73 | } 74 | 75 | public void RemoveMany(IEnumerable entities) => _dbSet.RemoveRange(entities); 76 | 77 | public void RemoveMany(Expression>[] filters) 78 | { 79 | var entities = GetMany(filters: filters); 80 | if (!entities.Any()) 81 | return; 82 | RemoveMany(entities); 83 | } 84 | 85 | public IEnumerator GetEnumerator() 86 | => _dbSet.AsQueryable().GetEnumerator(); 87 | 88 | IEnumerator IEnumerable.GetEnumerator() 89 | => GetEnumerator(); 90 | 91 | public Type ElementType => _dbSet.AsQueryable().ElementType; 92 | public Expression Expression => _dbSet.AsQueryable().Expression; 93 | public IQueryProvider Provider => _dbSet.AsQueryable().Provider; 94 | } -------------------------------------------------------------------------------- /src/RepositoryPattern.EntityFrameworkCore/Extensions/RepositoryPatternOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using RepositoryPattern.Abstractions.Builder; 3 | using RepositoryPattern.EntityFrameworkCore.Repositories; 4 | using RepositoryPattern.EntityFrameworkCore.UnitsOfWork; 5 | 6 | namespace RepositoryPattern.EntityFrameworkCore.Extensions; 7 | 8 | /// 9 | /// Extension methods for setting up the repository pattern related services in an . 10 | /// 11 | public static class RepositoryPatternOptionsExtensions 12 | { 13 | /// 14 | /// Configures the repository pattern to use Entity Framework Core as the implementation of the repository and unit of work. 15 | /// 16 | /// The instance to configure. 17 | /// The instance so that additional calls can be chained. 18 | public static RepositoryPatternBuilder UseEntityFrameworkCore(this RepositoryPatternBuilder builder) 19 | { 20 | builder.UseRepositoryImplementation(typeof(Repository<>)); 21 | builder.UseUnitOfWorkImplementation(); 22 | 23 | return builder; 24 | } 25 | 26 | /// 27 | /// Configures the repository pattern to use Entity Framework Core as the implementation of the repository and unit of work. 28 | /// 29 | /// The instance to configure. 30 | /// 31 | /// The lifetime with which to register the repository service in the service collection. 32 | /// 33 | /// 34 | /// The lifetime with which to register the unit of work service in the service collection. 35 | /// 36 | /// The instance so that additional calls can be chained. 37 | public static RepositoryPatternBuilder UseEntityFrameworkCore(this RepositoryPatternBuilder builder, 38 | ServiceLifetime repositoryServiceLifetime, ServiceLifetime unitOfWorkServiceLifetime) 39 | { 40 | builder.UseRepositoryImplementation(typeof(Repository<>)) 41 | .UseLifetime(repositoryServiceLifetime); 42 | 43 | builder.UseUnitOfWorkImplementation() 44 | .UseLifetime(unitOfWorkServiceLifetime); 45 | 46 | return builder; 47 | } 48 | 49 | /// 50 | /// Configures the repository pattern to use Entity Framework Core as the implementation of the repository and unit of work. 51 | /// 52 | /// The instance to configure. 53 | /// 54 | /// The lifetime with which to register the repository and unit of work services in the service collection. 55 | /// 56 | /// The instance so that additional calls can be chained. 57 | public static RepositoryPatternBuilder UseEntityFrameworkCore(this RepositoryPatternBuilder builder, 58 | ServiceLifetime serviceLifetime) 59 | { 60 | builder.UseRepositoryImplementation(typeof(Repository<>)) 61 | .UseLifetime(serviceLifetime); 62 | 63 | builder.UseUnitOfWorkImplementation() 64 | .UseLifetime(serviceLifetime); 65 | 66 | return builder; 67 | } 68 | } -------------------------------------------------------------------------------- /docs/docs/efcore/advanced-tutorials.md: -------------------------------------------------------------------------------- 1 | # Advanced Usage 2 | 3 | This section delves into advanced scenarios for utilizing the `UnitOfWork` class. Here, you will learn how to leverage 4 | the full potential of the `UnitOfWork` pattern, including comprehensive 5 | transaction management, the use of save points, and handling multiple repositories. 6 | By mastering these techniques, you can ensure robust and efficient data management in your applications, minimizing 7 | risks and optimizing performance. 8 | 9 | ## Advanced Transaction Management 10 | 11 | Efficient transaction management is crucial for maintaining data integrity and consistency. The `UnitOfWork` class 12 | provides robust support for starting, committing, and rolling back transactions. This ensures that a series of 13 | operations can either be completed successfully as a unit or reverted entirely if any operation fails. 14 | 15 | Here's how you can manage transactions effectively: 16 | 17 | ### Starting a Transaction 18 | 19 | To start a transaction, use the `BeginTransaction` method. This method opens a connection and starts a new transaction. 20 | 21 | ```csharp 22 | public void StartTransactionExample() 23 | { 24 | unitOfWork.BeginTransaction(); 25 | try 26 | { 27 | // Perform data operations 28 | unitOfWork.Repository().Add(new Product { Name = "Product1" }); 29 | unitOfWork.Save(); 30 | 31 | // Commit the transaction 32 | unitOfWork.Commit(); 33 | } 34 | catch (Exception) 35 | { 36 | // Roll back the transaction in case of an error 37 | unitOfWork.RollBack(); 38 | throw; 39 | } 40 | } 41 | ``` 42 | 43 | ### Asynchronous Transaction 44 | 45 | For asynchronous operations, use BeginTransactionAsync along with async/await patterns: 46 | 47 | ```csharp 48 | public async Task StartTransactionAsyncExample() 49 | { 50 | await unitOfWork.BeginTransactionAsync(); 51 | try 52 | { 53 | // Perform data operations 54 | await unitOfWork.Repository().AddAsync(new Product { Name = "Product1" }); 55 | await unitOfWork.SaveAsync(); 56 | 57 | // Commit the transaction 58 | await unitOfWork.CommitAsync(); 59 | } 60 | catch (Exception) 61 | { 62 | // Roll back the transaction in case of an error 63 | await unitOfWork.RollBackAsync(); 64 | throw; 65 | } 66 | } 67 | ``` 68 | 69 | ## Using Save Points 70 | 71 | Save points offer finer control within transactions, allowing partial rollbacks without affecting the entire 72 | transaction. This feature is particularly useful for complex business logic where certain steps may need to be retried 73 | or reversed without discarding all preceding successful operations. 74 | 75 | ### Creating and Rolling Back to Save Points: 76 | 77 | ```csharp 78 | public void SavePointExample() 79 | { 80 | unitOfWork.BeginTransaction(); 81 | try 82 | { 83 | unitOfWork.Repository().Add(new Product { Name = "Product1" }); 84 | unitOfWork.Save(); 85 | 86 | unitOfWork.CreateSavePoint("SavePoint1"); 87 | 88 | unitOfWork.Repository().Add(new Product { Name = "Product2" }); 89 | unitOfWork.Save(); 90 | 91 | unitOfWork.RollBackToSavePoint("SavePoint1"); 92 | 93 | unitOfWork.Commit(); 94 | } 95 | catch (Exception) 96 | { 97 | unitOfWork.RollBack(); 98 | throw; 99 | } 100 | } 101 | ``` 102 | 103 | ## Handling Multiple Repositories 104 | 105 | In scenarios where multiple entities need to be managed simultaneously, the `UnitOfWork` class facilitates the 106 | coordination of operations across different repositories within the same transaction. This ensures consistency and 107 | atomicity of related data changes. 108 | 109 | ```csharp 110 | public void MultipleRepositoriesExample() 111 | { 112 | unitOfWork.BeginTransaction(); 113 | try 114 | { 115 | var productRepository = unitOfWork.Repository(); 116 | var orderRepository = unitOfWork.Repository(); 117 | 118 | productRepository.Add(new Product { Name = "Product1" }); 119 | orderRepository.Add(new Order { OrderNumber = "Order1" }); 120 | 121 | unitOfWork.Save(); 122 | 123 | unitOfWork.Commit(); 124 | } 125 | catch (Exception) 126 | { 127 | unitOfWork.RollBack(); 128 | throw; 129 | } 130 | } 131 | ``` -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | _layout: landing 3 | --- 4 | 5 | # Introduction 6 | 7 | ![GitHub Repo stars](https://img.shields.io/github/stars/carlosjortiz/RepositoryPattern?style=flat&logo=github) 8 | 9 | **RepositoryPattern.Abstractions** 10 | 11 | ![NuGet Version](https://img.shields.io/nuget/v/Qrtix.RepositoryPattern.Abstractions?logo=nuget) 12 | ![NuGet Downloads](https://img.shields.io/nuget/dt/Qrtix.RepositoryPattern.Abstractions?style=flat&logo=nuget) 13 | 14 | **RepositoryPattern.EntityFrameworkCore** 15 | 16 | ![NuGet Version](https://img.shields.io/nuget/v/Qrtix.RepositoryPattern.EntityFrameworkCore?style=flat&logo=nuget) 17 | ![NuGet Downloads](https://img.shields.io/nuget/dt/Qrtix.RepositoryPattern.EntityFrameworkCore?style=flat&logo=nuget) 18 | 19 | 20 | 21 | 22 | Welcome to the documentation for Repository Pattern libraries! This documentation serves as a comprehensive guide to 23 | using these 24 | libraries effectively in your .NET projects. Whether you're a beginner looking to get started or 25 | an experienced developer seeking to streamline database operations with Repository Pattern, you'll find everything 26 | you need to know right here. 27 | 28 | Consult the online [documentation](https://carlosjortiz.github.io/RepositoryPattern/) for more details. 29 | 30 | - [About RepositoryPattern.Abstractions](#about-repositorypatternabstractions) 31 | - [What You'll Find in This Documentation](#what-youll-find-in-this-documentation) 32 | - [Contributing](#contributing) 33 | 34 | ## About `RepositoryPattern.Abstractions` 35 | 36 | `RepositoryPattern.Abstractions` is a library that create the repository pattern abstractions for the.NET ecosystem, 37 | providing a structured approach to database access and management. By decoupling your application's data access logic 38 | from the underlying data source, `RepositoryPattern.Abstractions` promotes modularity, testability, and maintainability. 39 | With 40 | this library, you can create and use repositories to perform CRUD (Create, Read, Update, Delete) operations, manage 41 | transactions, and execute custom queries, all while adhering to best practices in software architecture. 42 | 43 | [Getting Started](https://carlosjortiz.github.io/RepositoryPattern/docs/abstractions/getting-started.html) 44 | 45 | ## What You'll Find in This Documentation 46 | 47 | In this documentation, you'll discover detailed explanations, code samples, and practical tutorials covering various 48 | aspects. Here's what you can expect to find: 49 | 50 | - **Installation Guide:** Learn how to install and configure the libraries in your .NET projects. 51 | - **Getting Started:** Get up and running quickly with step-by-step instructions for basic usage. 52 | - **Advanced Usage and Tutorials:** Dive deeper into advanced features and scenarios, with hands-on tutorials and 53 | examples. 54 | - **API Reference:** Explore the complete API documentation for both libraries, including classes, methods, and 55 | properties. 56 | - **Best Practices:** Discover best practices, tips, and recommendations for optimizing your usage. 57 | 58 | Whether you're building a small application or a large-scale enterprise system, These libraries are powerful tools that 59 | can help you achieve your goals efficiently and effectively. Let's dive 60 | in and explore the capabilities of these libraries together! 61 | 62 | ## Contributing 63 | 64 | **Did you find a bug?** 65 | 66 | - Ensure the bug was not already reported by searching on GitHub 67 | under [Issues](https://github.com/carlosjortiz/RepositoryPattern/issues). 68 | - If you're unable to find an open issue addressing the 69 | problem, [open a new one](https://github.com/carlosjortiz/RepositoryPattern/issues/new). Be sure to include a title and clear 70 | description, as much relevant information as possible, and a code sample or an executable test case demonstrating the 71 | expected behavior that is not occurring. 72 | 73 | **Did you write a patch that fixes a bug?** 74 | 75 | - Open a new GitHub pull request with the patch. 76 | - Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 77 | 78 | **Do you intend to add a new feature or change an existing one?** 79 | 80 | - First suggest your change in the [RepositoryPattern ideas page](https://github.com/carlosjortiz/RepositoryPattern/discussions/categories/ideas) 81 | for discussion. 82 | - There are no fixed rules on what should and shouldn't be in this library, but some features are more valuable than 83 | others, and some require long-term maintenance that outweighs the value of the feature. So please get sign-off from 84 | the 85 | project leader (Carlos J. Ortiz) before putting in an excessive amount of work. 86 | 87 | **Do you have questions about the source code?** 88 | 89 | - Ask any question about how to use RepositoryPattern in 90 | the [RepositoryPattern discussion page](https://github.com/carlosjortiz/RepositoryPattern/discussions/new?category=q-a). 91 | 92 | If you have any questions or need further assistance, don't hesitate to reach out to us. 93 | 94 | Happy coding!n image. -------------------------------------------------------------------------------- /docs/docs/abstractions/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | A library for implementing generic repositories and unit of work with Entity Framework Core. 4 | This implementation uses a single instance of the DbContext for all repositories to avoid concurrency issues. 5 | 6 | > [!Tip] 7 | > While the `Qrtix.RepositoryPattern.Abstractions` provides the necessary abstractions for implementing generic 8 | > repositories and unit of work, it's recommended to use one of the specialized libraries that build upon these 9 | > abstractions for specific data access technologies. For Entity Framework Core, consider using 10 | > the `Qrtix.RepositoryPattern.EntityFrameworkCore` [library](https://carlosjortiz.github.io/RepositoryPattern/docs/efcore/getting-started.html), 11 | > which enhances compatibility and simplifies integration with 12 | > EF Core features. 13 | 14 | ## Table of Contents 15 | 16 | - [Installation](#installation) 17 | - [Creating the DbContext](#creating-the-dbcontext) 18 | - [Creating the Repository implementation](#creating-the-repository-implementation) 19 | - [Creating the Unit fo work implementation](#creating-the-unit-of-work-implementation) 20 | - [Injecting the Services](#injecting-the-repositorypatterns-services) 21 | 22 | ## Installation 23 | 24 | Using the NuGet package manager console within Visual Studio run the following command: 25 | 26 | ``` 27 | Install-Package Ortix.RepositoryPattern.Abstractions 28 | ``` 29 | 30 | Or using the .NET Core CLI from a terminal window: 31 | 32 | ``` 33 | dotnet add package Qrtix.RepositoryPattern.Abstractions 34 | ``` 35 | 36 | ## Creating the DbContext 37 | 38 | Create your DbContext inheriting from `Microsoft.EntityFrameworkCore.DbContext`: 39 | 40 | ```csharp 41 | public class MyDbContext : DbContext 42 | { 43 | public DbSet Products { get; set; } 44 | 45 | protected override void OnModelCreating(ModelBuilder modelBuilder) 46 | { 47 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 48 | base.OnModelCreating(modelBuilder); 49 | } 50 | } 51 | ``` 52 | 53 | Configure your DbContext in the `Program` class: 54 | 55 | ```csharp 56 | builder.Services.AddDbContext(opt => 57 | { 58 | opt.UseSqlServer(builder.Configuration.GetConnectionString("ConnectionString")); 59 | }); 60 | ``` 61 | 62 | ## Creating the Repository Implementation 63 | 64 | Create your repository implementation inheriting from `IRepository`: 65 | 66 | ```csharp 67 | public class ProductRepository : IRepository where TEntity : class 68 | { 69 | private readonly DbSet _dbSet; 70 | 71 | 72 | public IQueryable Data => _dbSet; 73 | 74 | 75 | public async Task> GetManyAsync(Expression>? filters = null, 76 | bool disableTracking = true, 77 | IEnumerable>>? includes = null, 78 | Func, IOrderedQueryable>? orderBy = null, 79 | CancellationToken cancellationToken = default) 80 | { 81 | // implementation 82 | } 83 | 84 | public IQueryable GetMany(Expression>? filters = null, 85 | bool disableTracking = false, 86 | IEnumerable>>? includes = null, 87 | Func, IOrderedQueryable>? orderBy = null) 88 | { 89 | // implementation 90 | } 91 | 92 | // others implementation 93 | } 94 | ``` 95 | 96 | ## Creating the Unit Of Work implementation 97 | 98 | Create your unit of work implementation inheriting from `IUnitOfWork`: 99 | 100 | ```csharp 101 | class UnitOfWork : IUnitOfWork 102 | { 103 | private readonly DbContext _context; 104 | private IDbContextTransaction? _transaction; 105 | private readonly Dictionary _repositories; 106 | 107 | public UnitOfWork(DbContext context) 108 | { 109 | _context = context; 110 | _repositories = new Dictionary(); 111 | } 112 | 113 | public void Dispose() 114 | { 115 | // implementation 116 | } 117 | 118 | 119 | public async ValueTask DisposeAsync() 120 | { 121 | // implementation 122 | } 123 | 124 | public IRepository Repository() where TEntity : class 125 | { 126 | // implementation 127 | } 128 | 129 | // others implementations 130 | ``` 131 | 132 | ## Injecting the RepositoryPattern's Services 133 | 134 | Add the `RepositoryPattern` services to the Service Collection: 135 | 136 | ```csharp 137 | builder.Services.AddRepositoryPattern(options => { 138 | options.UseRepositoryImplementation(typeof(Repository<>); 139 | options.UseUnitOfWorkImplementation(); 140 | }); 141 | ``` 142 | 143 | The default scope for injected services is scoped. If you want to change it, refer to the next example: 144 | 145 | ```csharp 146 | builder.Services.AddRepositoryPattern(options => { 147 | options.UseRepositoryImplementation(typeof(Repository<>) 148 | .UseLifeTime(ServiceLifetime.Singletor); 149 | 150 | options.UseUnitOfWorkImplementation() 151 | .UseLifetime(ServiceLifetime.Singleton); 152 | }); 153 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | ![GitHub Repo stars](https://img.shields.io/github/stars/carlosjortiz/RepositoryPattern?style=flat&logo=github) 4 | 5 | **RepositoryPattern.Abstractions** 6 | 7 | ![NuGet Version](https://img.shields.io/nuget/v/Qrtix.RepositoryPattern.Abstractions?logo=nuget) 8 | ![NuGet Downloads](https://img.shields.io/nuget/dt/Qrtix.RepositoryPattern.Abstractions?style=flat&logo=nuget) 9 | 10 | **RepositoryPattern.EntityFrameworkCore** 11 | 12 | ![NuGet Version](https://img.shields.io/nuget/v/Qrtix.RepositoryPattern.EntityFrameworkCore?style=flat&logo=nuget) 13 | ![NuGet Downloads](https://img.shields.io/nuget/dt/Qrtix.RepositoryPattern.EntityFrameworkCore?style=flat&logo=nuget) 14 | 15 | 16 | 17 | Welcome to the documentation for Repository Pattern libraries! This documentation serves as a comprehensive guide to 18 | using these 19 | libraries effectively in your .NET projects. Whether you're a beginner looking to get started or 20 | an experienced developer seeking to streamline database operations with Repository Pattern, you'll find everything 21 | you need to know right here. 22 | 23 | Consult the online [documentation](https://carlosjortiz.github.io/RepositoryPattern/) for more details. 24 | 25 | - [About RepositoryPattern.Converters](#about-repositorypatternabstractions) 26 | - [About RepositoryPattern.RepositoryPattern](#about-RepositoryPatternrepositorypattern) 27 | - [What You'll Find in This Documentation](#what-youll-find-in-this-documentation) 28 | - [Contributing](#contributing) 29 | 30 | ## About `RepositoryPattern.Abstractions` 31 | 32 | `RepositoryPattern.Abstractions` is a library that create the repository pattern abstractions for the.NET ecosystem, 33 | providing a structured approach to database access and management. By decoupling your application's data access logic 34 | from the underlying data source, `RepositoryPattern.Abstractions` promotes modularity, testability, and maintainability. 35 | With 36 | this library, you can create and use repositories to perform CRUD (Create, Read, Update, Delete) operations, manage 37 | transactions, and execute custom queries, all while adhering to best practices in software architecture. 38 | 39 | [README](src/RepositoryPattern.Abstractions/README.md) 40 | 41 | ## About `RepositoryPattern.EntityFrameworkCore` 42 | 43 | `RepositoryPattern.EntityFrameworkCore` is a library that implements the repository pattern on top of Entity Framework Core, 44 | providing a structured approach to database access and management. By decoupling your application's data access logic 45 | from the underlying data source, `RepositoryPattern.EntityFrameworkCore` promotes modularity, testability, and maintainability. With 46 | this library, you can create and use repositories to perform CRUD (Create, Read, Update, Delete) operations, manage 47 | transactions, and execute custom queries, all while adhering to best practices in software architecture. 48 | 49 | [README](src/RepositoryPattern.EntityFrameworkCore/README.md) 50 | 51 | ## What You'll Find in This Documentation 52 | 53 | In this documentation, you'll discover detailed explanations, code samples, and practical tutorials covering various 54 | aspects. Here's what you can expect to find: 55 | 56 | - **Installation Guide:** Learn how to install and configure the libraries in your .NET projects. 57 | - **Getting Started:** Get up and running quickly with step-by-step instructions for basic usage. 58 | - **Advanced Usage and Tutorials:** Dive deeper into advanced features and scenarios, with hands-on tutorials and 59 | examples. 60 | - **API Reference:** Explore the complete API documentation for both libraries, including classes, methods, and 61 | properties. 62 | - **Best Practices:** Discover best practices, tips, and recommendations for optimizing your usage. 63 | 64 | Whether you're building a small application or a large-scale enterprise system, These libraries are powerful tools that 65 | can help you achieve your goals efficiently and effectively. Let's dive 66 | in and explore the capabilities of these libraries together! 67 | 68 | ## Contributing 69 | 70 | **Did you find a bug?** 71 | 72 | - Ensure the bug was not already reported by searching on GitHub 73 | under [Issues](https://github.com/carlosjortiz/RepositoryPattern/issues). 74 | - If you're unable to find an open issue addressing the 75 | problem, [open a new one](https://github.com/carlosjortiz/RepositoryPattern/issues/new). Be sure to include a title and 76 | clear 77 | description, as much relevant information as possible, and a code sample or an executable test case demonstrating the 78 | expected behavior that is not occurring. 79 | 80 | **Did you write a patch that fixes a bug?** 81 | 82 | - Open a new GitHub pull request with the patch. 83 | - Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 84 | 85 | **Do you intend to add a new feature or change an existing one?** 86 | 87 | - First suggest your change in 88 | the [RepositoryPattern ideas page](https://github.com/carlosjortiz/RepositoryPattern/discussions/categories/ideas) 89 | for discussion. 90 | - There are no fixed rules on what should and shouldn't be in this library, but some features are more valuable than 91 | others, and some require long-term maintenance that outweighs the value of the feature. So please get sign-off from 92 | the 93 | project leader (Carlos J. Ortiz) before putting in an excessive amount of work. 94 | 95 | **Do you have questions about the source code?** 96 | 97 | - Ask any question about how to use RepositoryPattern in 98 | the [RepositoryPattern discussion page](https://github.com/carlosjortiz/RepositoryPattern/discussions/new?category=q-a). 99 | 100 | If you have any questions or need further assistance, don't hesitate to reach out to us. 101 | 102 | Happy coding! -------------------------------------------------------------------------------- /src/RepositoryPattern.EntityFrameworkCore/UnitsOfWork/UnitOfWorkImpl.cs: -------------------------------------------------------------------------------- 1 | using System.Data; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Storage; 4 | using RepositoryPattern.Abstractions.Repositories; 5 | using RepositoryPattern.Abstractions.UnitOfWork; 6 | using RepositoryPattern.EntityFrameworkCore.Repositories; 7 | 8 | namespace RepositoryPattern.EntityFrameworkCore.UnitsOfWork; 9 | 10 | internal class UnitOfWork : IUnitOfWork 11 | { 12 | private readonly DbContext _context; 13 | private IDbContextTransaction? _transaction; 14 | private readonly Dictionary _repositories; 15 | 16 | public UnitOfWork(DbContext context) 17 | { 18 | _context = context; 19 | _repositories = new Dictionary(); 20 | } 21 | 22 | public void Dispose() 23 | { 24 | TransactionDispose(); 25 | _context.Dispose(); 26 | } 27 | 28 | 29 | public async ValueTask DisposeAsync() 30 | { 31 | TransactionDisposeAsync(); 32 | await _context.DisposeAsync(); 33 | } 34 | 35 | public IRepository Repository() where TEntity : class 36 | { 37 | var entityType = typeof(TEntity); 38 | 39 | if (_repositories.TryGetValue(entityType.Name, out var repository)) 40 | return (IRepository)repository; 41 | 42 | var repoType = typeof(Repository<>).MakeGenericType(entityType); 43 | var repo = Activator.CreateInstance(repoType, _context); 44 | 45 | if (repo != null) 46 | _repositories.Add(entityType.Name, repo); 47 | 48 | return (IRepository)_repositories[entityType.Name]; 49 | } 50 | 51 | public void BeginTransaction(bool force = false) 52 | { 53 | if (_transaction is not null && !force) 54 | throw new InvalidOperationException("Cannot begin transaction. A transaction is already in progress."); 55 | 56 | var connection = _context.Database.GetDbConnection(); 57 | if (connection.State is not ConnectionState.Open) 58 | connection.Open(); 59 | 60 | _transaction = _context.Database.BeginTransaction(IsolationLevel.Unspecified); 61 | } 62 | 63 | 64 | public async Task BeginTransactionAsync(bool force = false, CancellationToken cancellationToken = default) 65 | { 66 | if (_transaction is not null && !force) 67 | throw new InvalidOperationException("Cannot begin transaction. A transaction is already in progress."); 68 | 69 | var connection = _context.Database.GetDbConnection(); 70 | if (connection.State is not ConnectionState.Open) 71 | await connection.OpenAsync(cancellationToken); 72 | 73 | _transaction = await _context.Database.BeginTransactionAsync(IsolationLevel.Unspecified, cancellationToken); 74 | } 75 | 76 | public void RollBack() 77 | { 78 | if (_transaction is null) 79 | throw new InvalidCastException("Cannot roll back. No active transaction exists."); 80 | 81 | _transaction.Rollback(); 82 | TransactionDispose(); 83 | } 84 | 85 | public async Task RollBackAsync(CancellationToken cancellationToken = default) 86 | { 87 | if (_transaction is null) 88 | throw new InvalidCastException("Cannot roll back. No active transaction exists."); 89 | 90 | await _transaction.RollbackAsync(cancellationToken); 91 | TransactionDisposeAsync(); 92 | } 93 | 94 | public void RollBackToSavePoint(string name) 95 | { 96 | if (_transaction is null) 97 | throw new InvalidCastException("Cannot roll back. No active transaction exists."); 98 | 99 | _transaction.RollbackToSavepoint(name); 100 | TransactionDispose(); 101 | } 102 | 103 | public async Task RollBackToSavePointAsync(string name, 104 | CancellationToken cancellationToken = default) 105 | { 106 | if (_transaction is null) 107 | throw new InvalidCastException("Cannot roll back. No active transaction exists."); 108 | 109 | await _transaction.RollbackToSavepointAsync(name, cancellationToken); 110 | TransactionDisposeAsync(); 111 | } 112 | 113 | public void Commit() 114 | { 115 | if (_transaction is null) 116 | throw new InvalidCastException("Cannot commit. No active transaction exists."); 117 | 118 | _transaction.Commit(); 119 | TransactionDispose(); 120 | } 121 | 122 | public async Task CommitAsync(CancellationToken cancellationToken = default) 123 | { 124 | if (_transaction is null) 125 | throw new InvalidCastException("Cannot commit. No active transaction exists."); 126 | 127 | await _transaction.CommitAsync(cancellationToken); 128 | TransactionDispose(); 129 | } 130 | 131 | public void CreateSavePoint(string name) 132 | { 133 | if (_transaction is null) 134 | throw new InvalidCastException("Cannot create a save point. No active transaction exists."); 135 | 136 | _transaction.CreateSavepoint(name); 137 | } 138 | 139 | public async Task CreateSavePointAsync(string name, 140 | CancellationToken cancellationToken = default) 141 | { 142 | if (_transaction is null) 143 | throw new InvalidCastException("Cannot create a save point. No active transaction exists."); 144 | 145 | await _transaction.CreateSavepointAsync(name, cancellationToken); 146 | } 147 | 148 | public void ReleaseSavePoint(string name) 149 | { 150 | if (_transaction is null) 151 | throw new InvalidCastException("Cannot release a save point. No active transaction exists."); 152 | 153 | _transaction.ReleaseSavepoint(name); 154 | TransactionDispose(); 155 | } 156 | 157 | public async Task ReleaseSavePointAsync(string name, 158 | CancellationToken cancellationToken = default) 159 | { 160 | if (_transaction is null) 161 | throw new InvalidCastException("Cannot release a save point. No active transaction exists."); 162 | 163 | await _transaction.ReleaseSavepointAsync(name, cancellationToken); 164 | TransactionDisposeAsync(); 165 | } 166 | 167 | public bool SupportsSavePoints => _transaction?.SupportsSavepoints ?? false; 168 | 169 | public async Task SaveAsync(CancellationToken cancellationToken = default) 170 | => await _context.SaveChangesAsync(cancellationToken); 171 | 172 | public int Save() => _context.SaveChanges(); 173 | 174 | private void TransactionDispose() 175 | { 176 | _transaction?.Dispose(); 177 | _transaction = null; 178 | } 179 | 180 | private ValueTask? TransactionDisposeAsync() 181 | { 182 | var result = _transaction?.DisposeAsync(); 183 | _transaction = null; 184 | return result; 185 | } 186 | } -------------------------------------------------------------------------------- /docs/docs/efcore/basic-tutorials.md: -------------------------------------------------------------------------------- 1 | # Basic Tutorials 2 | 3 | This guide provides examples of basic usage for the repository pattern implementation with Entity Framework Core. 4 | 5 | You are able to use the methods inherited from the `IQueryable` interface and can be used directly with 6 | the repository: 7 | - **Filtering Data**: Use methods like `Where` for filtering. 8 | - **Ordering Data**: Use methods like `OrderBy` for sorting. 9 | - **Projecting Data**: Use methods like `Select` for projection. 10 | 11 | These methods provide powerful querying capabilities and can be used in conjunction with other repository methods for 12 | more complex data operations. 13 | 14 | >[!Important] 15 | > To save changes into the database, use the `SaveChanges` method or the async version from the UnitOfWork. 16 | 17 | ## Table of Contents 18 | 19 | - [Retrieving Data](#retrieving-data) 20 | - [Saving Data](#saving-data) 21 | - [Updating Data](#updating-data) 22 | - [Deleting Data](#deleting-data) 23 | 24 | ## Retrieving Data 25 | 26 | ### Example: Retrieving a Single Entity 27 | 28 | You can use `GetOne` and `FirstOrDefault` or the async versions to retrieve a single entity from the repository. 29 | 30 | ```csharp 31 | public async Task GetProductByIdAsync(int productId) 32 | { 33 | // You can also use synchronous version of GetOne 34 | Product product = await _unitOfWork.Repository().GetOneAsync(p => p.Id == productId); 35 | 36 | // You can also use synchronous version of FirstOrDefault 37 | product = await _unitOfWork.Repository().FirstOrDefaultAsync(p => p.Id == productId); 38 | 39 | if (product == null) 40 | { 41 | throw new Exception($"Product with Id {productId} not found."); 42 | } 43 | 44 | return product; 45 | } 46 | ``` 47 | 48 | ### Example: Retrieving Multiple Entities 49 | 50 | You can use `GetMany`, `GetManyAsync` to retrieve multiple entities from the repository or use the IQueryable methods 51 | like `Where`, `OrderBy`, `Select` to filter, sort, and project data directly from the repository. 52 | 53 | ```csharp 54 | public async Task> GetProductsByCategoryAsync(string category) 55 | { 56 | // You can also use synchronous version of GetMany 57 | List products = await _unitOfWork.Repository().GetManyAsync(p => p.Category == category); 58 | 59 | // Also you can use the IQueryable methods like Where, OrderBy, Select directly on the repository 60 | products = await _unitOfWork.Repository().Where(p => p.Category == category).ToListAsync(); 61 | 62 | return products; 63 | } 64 | ``` 65 | 66 | ### Example: Using the 'includes' parameter in GetMany and GetOne methods 67 | 68 | You can use the `includes` parameter to include related entities in the query. 69 | 70 | ```csharp 71 | public async Task> GetProductsWithCategoryAsync() 72 | { 73 | List products = await _unitOfWork.Repository().GetManyAsync(includes: p => p.Category); 74 | return products; 75 | } 76 | ``` 77 | 78 | If you want to include multiple related entities, you can use the `includes` parameter with multiple expressions. 79 | 80 | ```csharp 81 | public async Task> GetProductsWithCategoryAndSupplierAsync() 82 | { 83 | var includes = new Expression>[] 84 | { 85 | p => p.Category, 86 | p => p.Suppliers 87 | }; 88 | List products = await _unitOfWork.Repository().GetManyAsync(includes: includes); 89 | return products; 90 | } 91 | ``` 92 | 93 | Now you want access to the contact info of the supplier, you can use the `Then` method if the suppliers is a collection to provide the navigation property. 94 | 95 | ```csharp 96 | public async Task> GetProductsWithCategoryAndSupplierContactInfoAsync() 97 | { 98 | var includes = new Expression>[] 99 | { 100 | p => p.Category, 101 | p => p.Suppliers.Then(s => s.ContactInfo) 102 | }; 103 | List products = await _unitOfWork.Repository().GetManyAsync(includes: includes); 104 | return products; 105 | } 106 | ``` 107 | 108 | ## Saving Data 109 | 110 | ### Example: Saving an Entity 111 | 112 | You can use `AddOne` or the async versions to save an entity to the repository. 113 | 114 | ```csharp 115 | public async Task AddProductAsync(Product product) 116 | { 117 | await _unitOfWork.Repository().AddOneAsync(product); 118 | await _unitOfWork.SaveAsync(); 119 | } 120 | ``` 121 | 122 | ### Example: Saving Multiple Entities 123 | 124 | You can use `AddMany` or the async versions to save multiple entities to the repository. 125 | 126 | ```csharp 127 | public async Task AddProductsAsync(List products) 128 | { 129 | await _unitOfWork.Repository().AddManyAsync(products); 130 | await _unitOfWork.SaveAsync(); 131 | } 132 | ``` 133 | 134 | ## Updating Data 135 | 136 | ### Example: Updating an Entity 137 | 138 | You can use `UpdateOne` or the async versions to update an entity in the repository. 139 | 140 | ```csharp 141 | public async Task UpdateProductAsync(Product product) 142 | { 143 | await _unitOfWork.Repository().UpdateOneAsync(product); 144 | await _unitOfWork.SaveAsync(); 145 | } 146 | ``` 147 | 148 | ### Example: Updating Multiple Entities 149 | 150 | You can use `UpdateMany` or the async versions to update multiple entities in the repository. 151 | 152 | ```csharp 153 | public async Task UpdateProductsAsync(List products) 154 | { 155 | await _unitOfWork.Repository().UpdateManyAsync(products); 156 | await _unitOfWork.SaveAsync(); 157 | } 158 | ``` 159 | 160 | ## Deleting Data 161 | 162 | ### Example: Deleting an Entity 163 | 164 | You can use `RemoveOne` or the async versions to delete an entity from the repository. 165 | 166 | ```csharp 167 | public async Task RemoveProductAsync(Product product) 168 | { 169 | await _unitOfWork.Repository().RemoveOneAsync(product); 170 | await _unitOfWork.SaveAsync(); 171 | } 172 | ``` 173 | 174 | ### Example: Deleting Multiple Entities 175 | 176 | You can use `RemoveMany` or the async versions to delete multiple entities from the repository. 177 | 178 | ```csharp 179 | public async Task RemoveProductsAsync(List products) 180 | { 181 | await _unitOfWork.Repository().RemoveManyAsync(products); 182 | await _unitOfWork.SaveAsync(); 183 | } 184 | ``` -------------------------------------------------------------------------------- /src/RepositoryPattern.Abstractions/README.md: -------------------------------------------------------------------------------- 1 | # Repository Pattern Abstractions 2 | 3 | ![NuGet Version](https://img.shields.io/nuget/v/Qrtix.RepositoryPattern.Abstractions?style=flat&logo=nuget) 4 | ![NuGet Downloads](https://img.shields.io/nuget/dt/Qrtix.RepositoryPattern.Abstractions?style=flat&logo=nuget) 5 | ![GitHub Repo stars](https://img.shields.io/github/stars/carlosjortiz/RepositoryPattern?style=flat&logo=github) 6 | 7 | A library for implementing generic repositories and unit of work with Entity Framework Core. 8 | This implementation uses a single instance of the DbContext for all repositories to avoid concurrency issues. 9 | 10 | Consult the online [documentation](https://carlosjortiz.github.io/RepositoryPattern/) for more details. 11 | 12 | > [!Tip] 13 | > While the `Qrtix.RepositoryPattern.Abstractions` provides the necessary abstractions for implementing generic 14 | > repositories and unit of work, it's recommended to use one of the specialized libraries that build upon these 15 | > abstractions for specific data access technologies. For Entity Framework Core, consider using 16 | > the `Qrtix.RepositoryPattern.EntityFrameworkCore` [library](https://carlosjortiz.github.io/RepositoryPattern/docs/efcore/getting-started.html), 17 | > which enhances compatibility and simplifies integration with 18 | > EF Core features. 19 | 20 | ## Table of Contents 21 | 22 | - [Installation](#installation) 23 | - [Creating the DbContext](#creating-the-dbcontext) 24 | - [Creating the Repository Implementation](#creating-the-repository-implementation) 25 | - [Creating the Unit Of Work implementation](#creating-the-unit-of-work-implementation) 26 | - [Injecting the RepositoryPattern's Services](#injecting-the-repositorypatterns-services) 27 | - [Repository Pattern Implementations](#repository-pattern-implementations) 28 | 29 | ## Installation 30 | 31 | Using the NuGet package manager console within Visual Studio run the following command: 32 | 33 | ``` 34 | Install-Package Ortix.RepositoryPattern.Abstractions 35 | ``` 36 | 37 | Or using the .NET Core CLI from a terminal window: 38 | 39 | ``` 40 | dotnet add package Qrtix.RepositoryPattern.Abstractions 41 | ``` 42 | 43 | ## Creating the DbContext 44 | 45 | Create your DbContext inheriting from `Microsoft.EntityFrameworkCore.DbContext`: 46 | 47 | ```csharp 48 | public class MyDbContext : DbContext 49 | { 50 | public DbSet Products { get; set; } 51 | 52 | protected override void OnModelCreating(ModelBuilder modelBuilder) 53 | { 54 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 55 | base.OnModelCreating(modelBuilder); 56 | } 57 | } 58 | ``` 59 | 60 | Configure your DbContext in the `Program` class: 61 | 62 | ```csharp 63 | builder.Services.AddDbContext(opt => 64 | { 65 | opt.UseSqlServer(builder.Configuration.GetConnectionString("ConnectionString")); 66 | }); 67 | ``` 68 | 69 | ## Creating the Repository Implementation 70 | 71 | Create your repository implementation inheriting from `IRepository`: 72 | 73 | ```csharp 74 | public class ProductRepository : IRepository where TEntity : class 75 | { 76 | private readonly DbSet _dbSet; 77 | 78 | 79 | public IQueryable Data => _dbSet; 80 | 81 | 82 | public async Task> GetManyAsync(Expression>? filters = null, 83 | bool disableTracking = true, 84 | IEnumerable>>? includes = null, 85 | Func, IOrderedQueryable>? orderBy = null, 86 | CancellationToken cancellationToken = default) 87 | { 88 | // implementation 89 | } 90 | 91 | public IQueryable GetMany(Expression>? filters = null, 92 | bool disableTracking = false, 93 | IEnumerable>>? includes = null, 94 | Func, IOrderedQueryable>? orderBy = null) 95 | { 96 | // implementation 97 | } 98 | 99 | // others implementation 100 | } 101 | ``` 102 | 103 | ## Creating the Unit Of Work implementation 104 | 105 | Create your unit of work implementation inheriting from `IUnitOfWork`: 106 | 107 | ```csharp 108 | class UnitOfWork : IUnitOfWork 109 | { 110 | private readonly DbContext _context; 111 | private IDbContextTransaction? _transaction; 112 | private readonly Dictionary _repositories; 113 | 114 | public UnitOfWork(DbContext context) 115 | { 116 | _context = context; 117 | _repositories = new Dictionary(); 118 | } 119 | 120 | public void Dispose() 121 | { 122 | // implementation 123 | } 124 | 125 | 126 | public async ValueTask DisposeAsync() 127 | { 128 | // implementation 129 | } 130 | 131 | public IRepository Repository() where TEntity : class 132 | { 133 | // implementation 134 | } 135 | 136 | // others implementations 137 | ``` 138 | 139 | ## Injecting the RepositoryPattern's Services 140 | 141 | Add the `RepositoryPattern` services to the Service Collection: 142 | 143 | ```csharp 144 | builder.Services.AddRepositoryPattern(options => { 145 | options.UseRepositoryImplementation(typeof(Repository<>); 146 | options.UseUnitOfWorkImplementation(); 147 | }); 148 | ``` 149 | 150 | The default scope for injected services is scoped. If you want to change it, refer to the next example: 151 | 152 | ```csharp 153 | builder.Services.AddRepositoryPattern(options => { 154 | options.UseRepositoryImplementation(typeof(Repository<>) 155 | .UseLifeTime(ServiceLifetime.Singletor); 156 | 157 | options.UseUnitOfWorkImplementation() 158 | .UseLifetime(ServiceLifetime.Singleton); 159 | }); 160 | ``` 161 | 162 | ## Repository Pattern Implementations 163 | 164 | While the `RepositoryPattern.Abstractions` provides the necessary abstractions for implementing generic repositories and 165 | unit of work, it's recommended to use one of the specialized libraries that build upon these abstractions for specific 166 | data access technologies. 167 | 168 | For Entity Framework Core, consider using the `RepositoryPattern.EntityFrameworkCore` library, which enhances 169 | compatibility and simplifies integration with EF Core features. For more information consult 170 | the [Readme file](../RepositoryPattern.EntityFrameworkCore/README.md) or 171 | the [online documentation](https://carlosjortiz.github.io/RepositoryPattern/docs/efcore/introduction.html). 172 | -------------------------------------------------------------------------------- /src/RepositoryPattern.Abstractions/Repositories/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace RepositoryPattern.Abstractions.Repositories; 4 | 5 | /// 6 | /// Represents a repository for managing entities of type 7 | /// within an Entity Framework Core context. 8 | /// 9 | /// The type of entities managed by this repository. 10 | public interface IRepository : IQueryable where TEntity : class 11 | { 12 | /// 13 | /// Synchronously retrieves multiple entities from the repository based on filters criteria. 14 | /// 15 | /// 16 | /// (Optional) A lambda expression to test each entity for a condition. If null, all entities are retrieved. 17 | /// 18 | /// 19 | /// (Optional) to disable change tracking; otherwise, . The default is 20 | /// . 21 | /// 22 | /// 23 | /// (Optional) A lambda expression to specify the order of the retrieved entities. 24 | /// 25 | /// 26 | /// (Optional) A list with the expressions for related entities to be included with the retrieved entities. 27 | /// 28 | /// 29 | /// An with all existing 30 | /// elements if the is null. Otherwise, it only contains elements 31 | /// that satisfy the condition specified by . 32 | /// 33 | IQueryable GetMany(IEnumerable>>? includes = null, 34 | bool disableTracking = false, 35 | Func, IOrderedQueryable>? orderBy = null, 36 | params Expression>[]? filters); 37 | 38 | /// 39 | /// Asynchronously retrieves a single entity from the repository based on filters criteria. 40 | /// 41 | /// 42 | /// (Optional) A list with the expressions for related entities to be included with the retrieved entity. 43 | /// 44 | /// 45 | /// (Optional) to disable change tracking; otherwise, . The default is 46 | /// . 47 | /// 48 | /// 49 | /// (Optional) A to observe while waiting for the task to complete. 50 | /// 51 | /// 52 | /// A lambda expression to test each entity for a condition. 53 | /// 54 | /// 55 | /// A task that represents the asynchronous operation. 56 | /// The task result is the retrieved entity, or null if not found. 57 | /// 58 | Task GetOneAsync( 59 | IEnumerable>>? includes = null, 60 | bool disableTracking = false, 61 | CancellationToken cancellationToken = default, 62 | params Expression>[] filters 63 | ); 64 | 65 | /// 66 | /// Synchronously retrieves a single entity from the repository based on filters criteria. 67 | /// 68 | /// 69 | /// (Optional) A list with the expressions for related entities to be included with the retrieved entity. 70 | /// 71 | /// 72 | /// (Optional) to disable change tracking; otherwise, . The default is 73 | /// . 74 | /// 75 | /// 76 | /// A lambda expressions to test each entity for a condition. 77 | /// 78 | /// 79 | /// Retrieved the entity that satisfies the condition specified by , or null if not found. 80 | /// 81 | TEntity? GetOne( 82 | IEnumerable>>? includes = null, 83 | bool disableTracking = false, 84 | params Expression>[] filters 85 | ); 86 | 87 | /// 88 | /// Asynchronously creates a single entity into the repository. 89 | /// 90 | /// 91 | /// The entity to be created. 92 | /// 93 | /// 94 | /// (Optional) A to observe while waiting for the task to complete. 95 | /// 96 | /// 97 | /// A task that represents the asynchronous operation. 98 | /// 99 | Task AddOneAsync(TEntity entity, 100 | CancellationToken cancellationToken = default); 101 | 102 | /// 103 | /// Synchronously creates a single entity into the repository. 104 | /// 105 | /// 106 | /// The entity to be created. 107 | /// 108 | void AddOne(TEntity entity); 109 | 110 | 111 | /// 112 | /// Synchronously creates multiple entities into the repository. 113 | /// 114 | /// 115 | /// A collection of entities to be created. 116 | /// 117 | void AddMany(IEnumerable entities); 118 | 119 | /// 120 | /// Synchronously updates a single entity in the repository. 121 | /// 122 | /// 123 | /// The entity to be updated. 124 | /// 125 | void UpdateOne(TEntity entity); 126 | 127 | /// 128 | /// Synchronously updates multiple entities in the repository. 129 | /// 130 | /// 131 | /// A collection of entities to be updated. 132 | /// 133 | void UpdateMany(IEnumerable entities); 134 | 135 | /// 136 | /// Synchronously removes a single entity from the repository. 137 | /// 138 | /// 139 | /// The entity to be removed. 140 | /// 141 | void RemoveOne(TEntity entity); 142 | 143 | /// 144 | /// Synchronously removes a single entity from the repository based on filters criteria. 145 | /// 146 | /// 147 | /// A lambda expression to test each entity for a condition. 148 | /// 149 | /// 150 | /// Thrown when no entity matching the filters criteria is found in the repository. 151 | /// 152 | void RemoveOne(Expression>[] filters); 153 | 154 | /// 155 | /// Synchronously removes multiple entities from the repository. 156 | /// 157 | /// 158 | /// A collection of entities to be removed. 159 | /// 160 | void RemoveMany(IEnumerable entities); 161 | 162 | /// 163 | /// Synchronously removes multiple entities from the repository based on filters criteria. 164 | /// 165 | /// 166 | /// A lambda expression to test each entity for a condition. 167 | /// 168 | void RemoveMany(Expression>[] filters); 169 | } -------------------------------------------------------------------------------- /src/RepositoryPattern.Abstractions/UnitOfWork/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using RepositoryPattern.Abstractions.Repositories; 2 | 3 | namespace RepositoryPattern.Abstractions.UnitOfWork; 4 | 5 | /// 6 | /// Represents a unit of work that coordinates and manages database operations using Entity Framework Core. 7 | /// The `IUnitOfWork` interface serves as a central hub for working with the database context, repositories, and 8 | /// transactions. 9 | /// 10 | public interface IUnitOfWork : IDisposable, IAsyncDisposable 11 | { 12 | /// 13 | /// Determines whether the current transaction supports save points. 14 | /// 15 | /// 16 | /// true if the transaction supports save points; otherwise, false. 17 | /// 18 | /// 19 | /// This property returns true if the current transaction supports savepoints, 20 | /// otherwise false. 21 | /// 22 | bool SupportsSavePoints { get; } 23 | 24 | /// 25 | /// Retrieves a repository for managing entities of type . 26 | /// 27 | /// The type of entities to be managed by the repository. 28 | /// 29 | /// An instance of for managing entities of the specified type. 30 | /// 31 | IRepository Repository() where TEntity : class; 32 | 33 | /// 34 | /// Initiates a new database transaction. 35 | /// 36 | /// 37 | /// (Optional) Indicates whether to force a new transaction even when another transaction is in progress. 38 | /// true to force a new transaction; false otherwise. Default is false. 39 | /// 40 | /// 41 | /// Thrown when attempting to begin a transaction while another transaction is already in progress. 42 | /// 43 | void BeginTransaction(bool force = false); 44 | 45 | /// 46 | /// Asynchronously initiates a new database transaction. 47 | /// 48 | /// 49 | /// (Optional) Indicates whether to force a new transaction even when another transaction is in progress. 50 | /// true to force a new transaction; false otherwise. Default is false. 51 | /// 52 | /// 53 | /// A to observe while waiting for the operation to 54 | /// complete. 55 | /// 56 | /// 57 | /// Thrown when attempting to begin a transaction while another transaction is already in progress. 58 | /// 59 | Task BeginTransactionAsync(bool force = false, CancellationToken cancellationToken = default); 60 | 61 | /// 62 | /// Rolls back the current transaction. 63 | /// 64 | /// 65 | /// Thrown when attempting to roll back a transaction that is not in progress. 66 | /// 67 | void RollBack(); 68 | 69 | /// 70 | /// Asynchronously rolls back the current transaction. 71 | /// 72 | /// 73 | /// A to observe while waiting for the operation to 74 | /// complete. 75 | /// 76 | /// 77 | /// Thrown when attempting to roll back a transaction that is not in progress. 78 | /// 79 | Task RollBackAsync(CancellationToken cancellationToken = default); 80 | 81 | /// 82 | /// Rolls back the transaction to a specific savepoint. 83 | /// 84 | /// The name of the savepoint to which the transaction should be rolled back. 85 | /// 86 | /// The transaction is not in progress. 87 | /// 88 | /// 89 | /// Thrown when attempting to roll back a transaction that is not in progress. 90 | /// 91 | void RollBackToSavePoint(string name); 92 | 93 | /// 94 | /// Asynchronously rolls back the transaction to a specific savepoint. 95 | /// 96 | /// The name of the savepoint to which the transaction should be rolled back. 97 | /// 98 | /// A to observe while waiting for the operation to 99 | /// complete. 100 | /// 101 | /// 102 | /// Thrown when attempting to roll back a transaction that is not in progress. 103 | /// 104 | Task RollBackToSavePointAsync(string name, CancellationToken cancellationToken = default); 105 | 106 | /// 107 | /// Commits the current transaction. 108 | /// 109 | /// 110 | /// Thrown when attempting to commit a transaction that is not in progress. 111 | /// 112 | void Commit(); 113 | 114 | /// 115 | /// Asynchronously commits the current transaction. 116 | /// 117 | /// 118 | /// A to observe while waiting for the operation to 119 | /// complete. 120 | /// 121 | /// 122 | /// Thrown when attempting to commit a transaction that is not in progress. 123 | /// 124 | Task CommitAsync(CancellationToken cancellationToken = default); 125 | 126 | /// 127 | /// Creates a savepoint within the current transaction. 128 | /// 129 | /// The name of the savepoint to create. 130 | /// 131 | /// Thrown when attempting to create a save point within a transaction that is not in progress. 132 | /// 133 | void CreateSavePoint(string name); 134 | 135 | /// 136 | /// Asynchronously creates a savepoint within the current transaction. 137 | /// 138 | /// The name of the savepoint to create. 139 | /// 140 | /// A to observe while waiting for the operation to 141 | /// complete. 142 | /// 143 | /// 144 | /// Thrown when attempting to create a save point within a transaction that is not in progress. 145 | /// 146 | Task CreateSavePointAsync(string name, CancellationToken cancellationToken = default); 147 | 148 | /// 149 | /// Releases a previously created savepoint within the current transaction. 150 | /// 151 | /// The name of the savepoint to release. 152 | /// 153 | /// Thrown when attempting to release a save point within a transaction that is not in progress. 154 | /// 155 | void ReleaseSavePoint(string name); 156 | 157 | /// 158 | /// Releases a previously created savepoint within the current transaction. 159 | /// 160 | /// The name of the savepoint to release. 161 | /// 162 | /// A to observe while waiting for the operation to 163 | /// complete. 164 | /// 165 | /// 166 | /// Thrown when attempting to release a save point within a transaction that is not in progress. 167 | /// 168 | Task ReleaseSavePointAsync(string name, CancellationToken cancellationToken = default); 169 | 170 | /// 171 | /// Asynchronously saves changes to the database. 172 | /// 173 | /// 174 | /// A to observe while waiting for the operation to 175 | /// complete. 176 | /// 177 | /// 178 | /// A representing the asynchronous operation. The task result is the number of entities affected 179 | /// by the save operation. 180 | /// 181 | Task SaveAsync(CancellationToken cancellationToken = default); 182 | 183 | /// 184 | /// Synchronously saves changes to the database. 185 | /// 186 | /// 187 | /// The number of entities affected by the save operation. 188 | /// 189 | int Save(); 190 | } -------------------------------------------------------------------------------- /tests/RepositoryPattern.EntityFrameworkCore.Tests/UnitsOfWork/UnitOfWorkTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using JetBrains.Annotations; 3 | using Microsoft.EntityFrameworkCore; 4 | using RepositoryPattern.EntityFrameworkCore.Repositories; 5 | using RepositoryPattern.EntityFrameworkCore.Tests.Laboratory; 6 | using RepositoryPattern.EntityFrameworkCore.UnitsOfWork; 7 | 8 | namespace RepositoryPattern.EntityFrameworkCore.Tests.UnitsOfWork; 9 | 10 | [TestSubject(typeof(UnitOfWork))] 11 | public class UnitOfWorkTest 12 | { 13 | private readonly UnitOfWork _unitOfWork; 14 | private readonly TestDbContext _context; 15 | 16 | public UnitOfWorkTest() 17 | { 18 | var options = new DbContextOptionsBuilder() 19 | .UseInMemoryDatabase("TestDB") 20 | .Options; 21 | 22 | _context = new TestDbContext(options); 23 | 24 | _unitOfWork = new UnitOfWork(_context); 25 | } 26 | 27 | [Fact] 28 | public void Repository_ShouldReturnNewRepositoryInstance_WhenCalled() 29 | { 30 | // Act 31 | var repository = _unitOfWork.Repository(); 32 | 33 | // Assert 34 | repository.Should().NotBeNull(); 35 | repository.Should().BeOfType>(); 36 | } 37 | 38 | /*[Fact] 39 | public void Commit_ShouldThrowAnException_WhenNotExistsAnActiveTransaction() 40 | { 41 | // Act 42 | var act = () => _unitOfWork.Commit(); 43 | 44 | // Assert 45 | act.Should().Throw(); 46 | _context.Database.CurrentTransaction.Should().NotBeNull(); 47 | } 48 | 49 | [Fact] 50 | public void SaveAsync_ShouldSaveChanges_WhenCalled() 51 | { 52 | // Act 53 | var result = _unitOfWork.SaveAsync().Result; 54 | 55 | // Assert 56 | result.Should().BeGreaterThan(0); 57 | } 58 | 59 | [Fact] 60 | public void Save_ShouldSaveChanges_WhenCalled() 61 | { 62 | // Act 63 | var result = _unitOfWork.Save(); 64 | 65 | // Assert 66 | result.Should().BeGreaterThan(0); 67 | } 68 | 69 | [Fact] 70 | public void BeginTransaction_ShouldBeginTransaction_WhenCalled() 71 | { 72 | // Act 73 | _unitOfWork.BeginTransaction(); 74 | 75 | // Assert 76 | _context.Database.CurrentTransaction.Should().NotBeNull(); 77 | _context.Database.CurrentTransaction!.Commit(); 78 | } 79 | 80 | [Fact] 81 | public void BeginTransaction_ShouldThrowAnException_WhenExistsAnActiveTransaction() 82 | { 83 | // Arrange 84 | _unitOfWork.BeginTransaction(); 85 | 86 | // Act 87 | var act = () => _unitOfWork.BeginTransaction(); 88 | 89 | 90 | // Assert 91 | act.Should().Throw(); 92 | _context.Database.CurrentTransaction.Should().NotBeNull(); 93 | _context.Database.CurrentTransaction!.Commit(); 94 | } 95 | 96 | [Fact] 97 | public void BeginTransaction_ShouldCreateANewTransaction_WhenExistsAnActiveTransactionAndForceIsTrue() 98 | { 99 | // Arrange 100 | _unitOfWork.BeginTransaction(); 101 | var oldTransactionId = _context.Database.CurrentTransaction!.TransactionId; 102 | 103 | // Act 104 | _unitOfWork.BeginTransaction(true); 105 | 106 | 107 | // Assert 108 | _context.Database.CurrentTransaction.Should().NotBeNull(); 109 | _context.Database.CurrentTransaction!.TransactionId.Should().NotBe(oldTransactionId); 110 | _context.Database.CurrentTransaction!.Commit(); 111 | } 112 | 113 | [Fact] 114 | public async Task BeginTransactionAsync_ShouldBeginTransaction_WhenCalled() 115 | { 116 | // Act 117 | await _unitOfWork.BeginTransactionAsync(); 118 | 119 | // Assert 120 | _context.Database.CurrentTransaction.Should().NotBeNull(); 121 | await _context.Database.CurrentTransaction!.CommitAsync(); 122 | } 123 | 124 | [Fact] 125 | public async Task BeginTransactionAsync_ShouldThrowAnException_WhenExistsAnActiveTransaction() 126 | { 127 | // Arrange 128 | await _unitOfWork.BeginTransactionAsync(); 129 | 130 | // Act 131 | var act = async () => await _unitOfWork.BeginTransactionAsync(); 132 | 133 | 134 | // Assert 135 | await act.Should().ThrowAsync(); 136 | _context.Database.CurrentTransaction.Should().NotBeNull(); 137 | await _context.Database.CurrentTransaction!.CommitAsync(); 138 | } 139 | 140 | [Fact] 141 | public async Task BeginTransactionAsync_ShouldCreateANewTransaction_WhenExistsAnActiveTransactionAndForceIsTrue() 142 | { 143 | // Arrange 144 | await _unitOfWork.BeginTransactionAsync(); 145 | var oldTransactionId = _context.Database.CurrentTransaction!.TransactionId; 146 | 147 | // Act 148 | await _unitOfWork.BeginTransactionAsync(true); 149 | 150 | 151 | // Assert 152 | _context.Database.CurrentTransaction.Should().NotBeNull(); 153 | _context.Database.CurrentTransaction!.TransactionId.Should().NotBe(oldTransactionId); 154 | await _context.Database.CurrentTransaction!.CommitAsync(); 155 | } 156 | 157 | [Fact] 158 | public void Commit_ShouldCommitTransaction_WhenCalled() 159 | { 160 | // Arrange 161 | _unitOfWork.BeginTransaction(); 162 | 163 | // Act 164 | _unitOfWork.Commit(); 165 | 166 | // Assert 167 | _context.Database.CurrentTransaction.Should().BeNull(); 168 | } 169 | 170 | 171 | [Fact] 172 | public async Task CommitAsync_ShouldCommitTransaction_WhenCalled() 173 | { 174 | // Arrange 175 | await _unitOfWork.BeginTransactionAsync(); 176 | 177 | // Act 178 | await _unitOfWork.CommitAsync(); 179 | 180 | // Assert 181 | _context.Database.CurrentTransaction.Should().BeNull(); 182 | } 183 | 184 | [Fact] 185 | public async Task CommitAsync_ShouldThrowAnException_WhenNotExistsAnActiveTransaction() 186 | { 187 | // Act 188 | var act = async () => await _unitOfWork.CommitAsync(); 189 | 190 | // Assert 191 | await act.Should().ThrowAsync(); 192 | _context.Database.CurrentTransaction.Should().NotBeNull(); 193 | } 194 | 195 | [Fact] 196 | public void RollBack_ShouldRollBackTransaction_WhenCalled() 197 | { 198 | // Arrange 199 | _unitOfWork.BeginTransaction(); 200 | 201 | // Act 202 | _unitOfWork.RollBack(); 203 | 204 | // Assert 205 | _context.Database.CurrentTransaction.Should().BeNull(); 206 | } 207 | 208 | [Fact] 209 | public void RollBack_ShouldThrowAnException_WhenNotExistsAnActiveTransaction() 210 | { 211 | // Act 212 | var act = () => _unitOfWork.RollBack(); 213 | 214 | // Assert 215 | act.Should().Throw(); 216 | _context.Database.CurrentTransaction.Should().NotBeNull(); 217 | } 218 | 219 | [Fact] 220 | public async Task RollBackAsync_ShouldRollBackTransaction_WhenCalled() 221 | { 222 | // Arrange 223 | await _unitOfWork.BeginTransactionAsync(); 224 | 225 | // Act 226 | await _unitOfWork.RollBackAsync(); 227 | 228 | // Assert 229 | _context.Database.CurrentTransaction.Should().BeNull(); 230 | } 231 | 232 | [Fact] 233 | public async Task RollBackAsync_ShouldThrowAnException_WhenNotExistsAnActiveTransaction() 234 | { 235 | // Act 236 | var act = async () => await _unitOfWork.RollBackAsync(); 237 | 238 | // Assert 239 | await act.Should().ThrowAsync(); 240 | _context.Database.CurrentTransaction.Should().BeNull(); 241 | } 242 | 243 | [Fact] 244 | public void RollBackToSavePoint_ShouldRollBackTransaction_WhenCalled() 245 | { 246 | // Arrange 247 | _unitOfWork.BeginTransaction(); 248 | _unitOfWork.CreateSavePoint("test"); 249 | 250 | // Act 251 | _unitOfWork.RollBackToSavePoint("test"); 252 | 253 | // Assert 254 | _context.Database.CurrentTransaction.Should().BeNull(); 255 | } 256 | 257 | [Fact] 258 | public void RollBackToSavePoint_ShouldThrowAnException_WhenNotExistsAnActiveTransaction() 259 | { 260 | // Act 261 | var act = () => _unitOfWork.RollBackToSavePoint("test"); 262 | 263 | // Assert 264 | _context.Database.CurrentTransaction.Should().NotBeNull(); 265 | } 266 | 267 | [Fact] 268 | public async Task RollBackToSavePointAsync_ShouldRollBackTransaction_WhenCalled() 269 | { 270 | // Arrange 271 | await _unitOfWork.BeginTransactionAsync(); 272 | await _unitOfWork.CreateSavePointAsync("test"); 273 | 274 | // Act 275 | await _unitOfWork.RollBackToSavePointAsync("test"); 276 | 277 | // Assert 278 | _context.Database.CurrentTransaction.Should().BeNull(); 279 | } 280 | 281 | [Fact] 282 | public async Task RollBackToSavePointAsync_ShouldThrowAnException_WhenNotExistsAnActiveTransaction() 283 | { 284 | // Act 285 | var act = async () => await _unitOfWork.RollBackToSavePointAsync("test"); 286 | 287 | // Assert 288 | await act.Should().ThrowAsync(); 289 | _context.Database.CurrentTransaction.Should().BeNull(); 290 | } 291 | 292 | [Fact] 293 | public void CreateSavePoint_ShouldCreateSavePoint_WhenCalled() 294 | { 295 | // Arrange 296 | _unitOfWork.BeginTransaction(); 297 | 298 | // Act 299 | _unitOfWork.CreateSavePoint("test"); 300 | 301 | // Assert 302 | _context.Database.CurrentTransaction.Should().NotBeNull(); 303 | } 304 | 305 | [Fact] 306 | public void CreateSavePoint_ShouldThrowAnException_WhenNotExistsAnActiveTransaction() 307 | { 308 | // Act 309 | var act = () => _unitOfWork.CreateSavePoint("test"); 310 | 311 | // Assert 312 | act.Should().Throw(); 313 | _context.Database.CurrentTransaction.Should().BeNull(); 314 | } 315 | 316 | [Fact] 317 | public async Task CreateSavePointAsync_ShouldCreateSavePoint_WhenCalled() 318 | { 319 | // Arrange 320 | await _unitOfWork.BeginTransactionAsync(); 321 | 322 | // Act 323 | await _unitOfWork.CreateSavePointAsync("test"); 324 | 325 | // Assert 326 | _context.Database.CurrentTransaction.Should().NotBeNull(); 327 | } 328 | 329 | [Fact] 330 | public async Task CreateSavePointAsync_ShouldThrowAnException_WhenNotExistsAnActiveTransaction() 331 | { 332 | // Act 333 | var act = async () => await _unitOfWork.CreateSavePointAsync("test"); 334 | 335 | // Assert 336 | await act.Should().ThrowAsync(); 337 | _context.Database.CurrentTransaction.Should().NotBeNull(); 338 | } 339 | 340 | [Fact] 341 | public void ReleaseSavePoint_ShouldReleaseSavePoint_WhenCalled() 342 | { 343 | // Arrange 344 | _unitOfWork.BeginTransaction(); 345 | _unitOfWork.CreateSavePoint("test"); 346 | 347 | // Act 348 | _unitOfWork.ReleaseSavePoint("test"); 349 | 350 | // Assert 351 | _context.Database.CurrentTransaction.Should().BeNull(); 352 | } 353 | 354 | [Fact] 355 | public void ReleaseSavePoint_ShouldThrowAnException_WhenNotExistsAnActiveTransaction() 356 | { 357 | // Act 358 | var act = () => _unitOfWork.ReleaseSavePoint("test"); 359 | 360 | // Assert 361 | act.Should().Throw(); 362 | _context.Database.CurrentTransaction.Should().BeNull(); 363 | } 364 | 365 | [Fact] 366 | public async Task ReleaseSavePointAsync_ShouldReleaseSavePoint_WhenCalled() 367 | { 368 | // Arrange 369 | await _unitOfWork.BeginTransactionAsync(); 370 | await _unitOfWork.CreateSavePointAsync("test"); 371 | 372 | // Act 373 | await _unitOfWork.ReleaseSavePointAsync("test"); 374 | 375 | // Assert 376 | _context.Database.CurrentTransaction.Should().BeNull(); 377 | } 378 | 379 | [Fact] 380 | public async Task ReleaseSavePointAsync_ShouldThrowAnException_WhenNotExistsAnActiveTransaction() 381 | { 382 | // Act 383 | var act = async () => await _unitOfWork.ReleaseSavePointAsync("test"); 384 | 385 | // Assert 386 | await act.Should().ThrowAsync(); 387 | _context.Database.CurrentTransaction.Should().BeNull(); 388 | }*/ 389 | } -------------------------------------------------------------------------------- /tests/RepositoryPattern.EntityFrameworkCore.Tests/Repositories/RepositoryOfTEntityTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | using FluentAssertions; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Internal; 5 | using RepositoryPattern.Abstractions.Extensions; 6 | using RepositoryPattern.EntityFrameworkCore.Extensions; 7 | using RepositoryPattern.EntityFrameworkCore.Repositories; 8 | using RepositoryPattern.EntityFrameworkCore.Tests.Laboratory; 9 | 10 | namespace RepositoryPattern.EntityFrameworkCore.Tests.Repositories; 11 | 12 | public class RepositoryOfTEntityTests 13 | { 14 | private readonly TestDbContext _context; 15 | private readonly Repository _repository; 16 | 17 | public RepositoryOfTEntityTests() 18 | { 19 | var options = new DbContextOptionsBuilder() 20 | .UseInMemoryDatabase("TestDB") 21 | .Options; 22 | 23 | _context = new TestDbContext(options); 24 | 25 | _repository = new Repository(_context); 26 | } 27 | 28 | [Fact] 29 | public void Where_ShouldReturnCorrectEntities() 30 | { 31 | // Arrange 32 | _context.TestEntities.Add(new TestEntity { Id = 1121, Name = "Test 111" }); 33 | _context.TestEntities.Add(new TestEntity { Id = 2212, Name = "Test 222" }); 34 | _context.SaveChanges(); 35 | 36 | // Act 37 | var result = _repository 38 | .Where(e => e.Id == 1121) 39 | .ToList(); 40 | 41 | // Assert 42 | result.Should().ContainSingle(e => e.Id == 1121); 43 | } 44 | 45 | [Fact] 46 | public void FirstOrDefault_ShouldReturnCorrectEntity() 47 | { 48 | // Arrange 49 | _context.TestEntities.Add(new TestEntity { Id = 111, Name = "Test 111" }); 50 | _context.TestEntities.Add(new TestEntity { Id = 222, Name = "Test 222" }); 51 | _context.SaveChanges(); 52 | 53 | // Act 54 | var result = _repository 55 | .FirstOrDefault(e => e.Id == 111); 56 | 57 | // Assert 58 | result.Should().BeEquivalentTo(new TestEntity { Id = 111, Name = "Test 111" }); 59 | } 60 | 61 | [Fact] 62 | public async Task FirstOrDefaultAsync_ShouldReturnCorrectEntity() 63 | { 64 | // Arrange 65 | _context.TestEntities.Add(new TestEntity { Id = 1211, Name = "Test 111" }); 66 | _context.TestEntities.Add(new TestEntity { Id = 2122, Name = "Test 222" }); 67 | _context.SaveChanges(); 68 | 69 | // Act 70 | var result = await _repository 71 | .FirstOrDefaultAsync(e => e.Id == 1211); 72 | 73 | // Assert 74 | result.Should().BeEquivalentTo(new TestEntity { Id = 1211, Name = "Test 111" }); 75 | } 76 | 77 | [Fact] 78 | public void GetMany_NoFilters_ReturnsAll() 79 | { 80 | // Arrange 81 | var entities = _context.TestEntities.ToList(); 82 | 83 | // Act 84 | var result = _repository.GetMany(); 85 | 86 | // Assert 87 | result.Should().BeEquivalentTo(entities); 88 | 89 | _context.ChangeTracker.Clear(); 90 | } 91 | 92 | [Fact] 93 | public void GetMany_WithFilter_FiltersCorrectly() 94 | { 95 | // Arrange 96 | _context.TestEntities.Add(new TestEntity { Id = 1111, Name = "Test 111" }); 97 | _context.SaveChanges(); 98 | 99 | // Act 100 | var result = _repository.GetMany(filters: e => e.Id == 1111); 101 | 102 | // Assert 103 | result.Should().ContainSingle(e => e.Id == 1111); 104 | 105 | _context.ChangeTracker.Clear(); 106 | } 107 | 108 | [Fact] 109 | public void GetMany_WithOrderBy_OrdersCorrectly() 110 | { 111 | // Arrange 112 | var entities = _repository.ToList(); 113 | 114 | // Act 115 | var result = _repository.GetMany(orderBy: q => q.OrderBy(e => e.Id)); 116 | 117 | // Assert 118 | result.Should().BeInAscendingOrder(e => e.Id); 119 | 120 | _context.ChangeTracker.Clear(); 121 | } 122 | 123 | [Fact] 124 | public void GetMany_DisableTracking_DoesNotTrackEntities() 125 | { 126 | // Act 127 | var result = _repository.GetMany(disableTracking: true); 128 | 129 | // Assert 130 | result.Should().NotBeNull(); 131 | result.AsEnumerable().All(e => _context.Entry(e).State == EntityState.Detached).Should().BeTrue(); 132 | 133 | _context.ChangeTracker.Clear(); 134 | } 135 | 136 | [Fact] 137 | public void GetMany_WithIncludes_IncludesRelatedEntities() 138 | { 139 | // Arrange 140 | List relatedTestEntities = 141 | [ 142 | new RelatedTestEntity { Id = 10, Name = "Related 1", TestEntityId = 10 }, 143 | new RelatedTestEntity { Id = 20, Name = "Related 2", TestEntityId = 20 }, 144 | new RelatedTestEntity { Id = 30, Name = "Related 3", TestEntityId = 20 }, 145 | new RelatedTestEntity { Id = 40, Name = "Related 4", TestEntityId = 30 }, 146 | new RelatedTestEntity { Id = 50, Name = "Related 5", TestEntityId = 30 }, 147 | new RelatedTestEntity { Id = 60, Name = "Related 6", TestEntityId = 30 } 148 | ]; 149 | IEnumerable testEntities = 150 | [ 151 | new TestEntity { Id = 10, Name = "Test 1", RelatedTestEntities = [relatedTestEntities[0]] }, 152 | new TestEntity 153 | { Id = 20, Name = "Test 2", RelatedTestEntities = [relatedTestEntities[1], relatedTestEntities[2]] }, 154 | new TestEntity 155 | { 156 | Id = 30, Name = "Test 3", 157 | RelatedTestEntities = [relatedTestEntities[3], relatedTestEntities[4], relatedTestEntities[5]] 158 | }, 159 | ]; 160 | _context.AddRange(relatedTestEntities); 161 | _context.AddRange(testEntities); 162 | _context.SaveChanges(); 163 | var entities = _repository.Include(e => e.RelatedTestEntities).ToList(); 164 | var includes = new Expression>[] 165 | { 166 | e => e.RelatedTestEntities 167 | .Then(r => r.InsideRelatedTestEntity) 168 | }; 169 | 170 | // Act 171 | var result = _repository.GetMany(includes: includes); 172 | 173 | // Assert 174 | result.Should().BeEquivalentTo(entities); 175 | result.AsEnumerable().Any(e => e.RelatedTestEntities.Count != 0).Should().BeTrue(); 176 | 177 | _context.ChangeTracker.Clear(); 178 | } 179 | 180 | [Fact] 181 | public void GetMany_WithThenIncludes_IncludesRelatedEntities() 182 | { 183 | // Arrange 184 | List relatedTestEntities = 185 | [ 186 | new RelatedTestEntity { Id = 1, Name = "Related 1", TestEntityId = 1, InsideRelatedTestEntityId = 1}, 187 | new RelatedTestEntity { Id = 2, Name = "Related 2", TestEntityId = 2 }, 188 | new RelatedTestEntity { Id = 3, Name = "Related 3", TestEntityId = 2, InsideRelatedTestEntityId = 1 }, 189 | new RelatedTestEntity { Id = 4, Name = "Related 4", TestEntityId = 3, InsideRelatedTestEntityId = 1 }, 190 | new RelatedTestEntity { Id = 5, Name = "Related 5", TestEntityId = 3 }, 191 | new RelatedTestEntity { Id = 6, Name = "Related 6", TestEntityId = 3, InsideRelatedTestEntityId = 1 } 192 | ]; 193 | InsideRelatedTestEntity insideRelatedEntity = new() 194 | { 195 | Id = 1, 196 | RelatedTestEntities = [ 197 | relatedTestEntities[0], 198 | relatedTestEntities[2], 199 | relatedTestEntities[3], 200 | relatedTestEntities[5], 201 | ] 202 | }; 203 | IEnumerable testEntities = 204 | [ 205 | new TestEntity { Id = 1, Name = "Test 1", RelatedTestEntities = [relatedTestEntities[0]] }, 206 | new TestEntity 207 | { Id = 2, Name = "Test 2", RelatedTestEntities = [relatedTestEntities[1], relatedTestEntities[2]] }, 208 | new TestEntity 209 | { 210 | Id = 3, Name = "Test 3", 211 | RelatedTestEntities = [relatedTestEntities[3], relatedTestEntities[4], relatedTestEntities[5]] 212 | }, 213 | ]; 214 | _context.Add(insideRelatedEntity); 215 | _context.AddRange(relatedTestEntities); 216 | _context.AddRange(testEntities); 217 | _context.SaveChanges(); 218 | var entities = _repository.Include(e => e.RelatedTestEntities) 219 | .ThenInclude(r => r.InsideRelatedTestEntity).ToList(); 220 | 221 | var includes = new Expression>[] { 222 | e => e.RelatedTestEntities.Then(r => r.InsideRelatedTestEntity) 223 | }; 224 | 225 | // Act 226 | var result = _repository.GetMany(includes: includes); 227 | 228 | // Assert 229 | result.Should().BeEquivalentTo(entities); 230 | result.AsEnumerable().Any(e => e.RelatedTestEntities.Count != 0).Should().BeTrue(); 231 | 232 | _context.ChangeTracker.Clear(); 233 | } 234 | 235 | [Fact] 236 | public async Task GetOneAsync_ExistingEntity_ReturnsEntity() 237 | { 238 | // Arrange 239 | var entity = _repository.First(); 240 | 241 | // Act 242 | var result = await _repository.GetOneAsync(); 243 | 244 | // Assert 245 | result.Should().BeEquivalentTo(entity); 246 | 247 | _context.ChangeTracker.Clear(); 248 | } 249 | 250 | [Fact] 251 | public async Task GetOneAsync_NonExistingEntity_ReturnsNull() 252 | { 253 | // Arrange 254 | var nonExistingId = _repository.Max(e => e.Id) + 1; 255 | 256 | // Act 257 | var result = await _repository.GetOneAsync(filters: e => e.Id == nonExistingId); 258 | 259 | // Assert 260 | result.Should().BeNull(); 261 | 262 | _context.ChangeTracker.Clear(); 263 | } 264 | 265 | [Fact] 266 | public async Task GetOneAsync_DisableTracking_DoesNotTrackEntity() 267 | { 268 | // Arrange 269 | var entity = _repository.First(); 270 | 271 | // Act 272 | var result = await _repository.GetOneAsync(disableTracking: true, filters: e => e.Id == entity.Id); 273 | 274 | // Assert 275 | result.Should().BeEquivalentTo(entity); 276 | (_context.Entry(result).State == EntityState.Detached).Should().BeTrue(); 277 | 278 | _context.ChangeTracker.Clear(); 279 | } 280 | 281 | [Fact] 282 | public async Task GetOneAsync_WithIncludes_IncludesRelatedEntities() 283 | { 284 | // Arrange 285 | var entity = _repository.Include(e => e.RelatedTestEntities).First(); 286 | var includes = new Expression>[] { e => e.RelatedTestEntities }; 287 | 288 | // Act 289 | var result = await _repository.GetOneAsync(includes: includes, filters: e => e.Id == entity.Id); 290 | 291 | // Assert 292 | result.Should().BeEquivalentTo(entity); 293 | result.RelatedTestEntities.Should().NotBeNull(); 294 | 295 | _context.ChangeTracker.Clear(); 296 | } 297 | 298 | [Fact] 299 | public async Task GetOneAsync_WithCancellationToken_HonorsCancellation() 300 | { 301 | // Arrange 302 | var cts = new CancellationTokenSource(); 303 | cts.Cancel(); 304 | 305 | // Act 306 | Func action = async () => 307 | await _repository.GetOneAsync(cancellationToken: cts.Token, filters: e => e.Id == 1); 308 | 309 | // Assert 310 | await action.Should().ThrowAsync(); 311 | 312 | _context.ChangeTracker.Clear(); 313 | } 314 | 315 | [Fact] 316 | public void GetOne_ExistingEntity_ReturnsEntity() 317 | { 318 | // Arrange 319 | var entity = _repository.First(); 320 | 321 | // Act 322 | var result = _repository.GetOne(filters: e => e.Id == entity.Id); 323 | 324 | // Assert 325 | result.Should().BeEquivalentTo(entity); 326 | 327 | _context.ChangeTracker.Clear(); 328 | } 329 | 330 | [Fact] 331 | public void GetOne_NonExistingEntity_ReturnsNull() 332 | { 333 | // Act 334 | var result = _repository.GetOne(filters: e => e.Id == 999); 335 | 336 | // Assert 337 | result.Should().BeNull(); 338 | 339 | _context.ChangeTracker.Clear(); 340 | } 341 | 342 | [Fact] 343 | public void GetOne_DisableTracking_DoesNotTrackEntity() 344 | { 345 | // Arrange 346 | var entity = _repository.First(); 347 | 348 | // Act 349 | var result = _repository.GetOne(disableTracking: true, filters: e => e.Id == entity.Id); 350 | 351 | // Assert 352 | result.Should().BeEquivalentTo(entity); 353 | (_context.Entry(result).State == EntityState.Detached).Should().BeTrue(); 354 | 355 | _context.ChangeTracker.Clear(); 356 | } 357 | 358 | [Fact] 359 | public void GetOne_WithIncludes_IncludesRelatedEntities() 360 | { 361 | // Arrange 362 | var entity = _repository.Include(e => e.RelatedTestEntities).First(); 363 | var includes = new Expression>[] { e => e.RelatedTestEntities }; 364 | 365 | // Act 366 | var result = _repository.GetOne(includes: includes, filters: e => e.Id == entity.Id); 367 | 368 | // Assert 369 | result.Should().BeEquivalentTo(entity); 370 | result.RelatedTestEntities.Should().NotBeNull(); 371 | 372 | _context.ChangeTracker.Clear(); 373 | } 374 | 375 | [Fact] 376 | public async Task AddOneAsync_AddsEntityToContext() 377 | { 378 | // Arrange 379 | var entity = new TestEntity { Id = 5, Name = "Test 4" }; 380 | 381 | // Act 382 | await _repository.AddOneAsync(entity); 383 | 384 | // Assert 385 | _context.Entry(entity).State.Should().Be(EntityState.Added); 386 | 387 | _context.ChangeTracker.Clear(); 388 | } 389 | 390 | [Fact] 391 | public void AddOne_ShouldAddEntityToContext() 392 | { 393 | // Arrange 394 | var entity = new TestEntity { Id = 612, Name = "Test Entity" }; 395 | 396 | // Act 397 | _repository.AddOne(entity); 398 | 399 | // Assert 400 | _context.Entry(entity).State.Should().Be(EntityState.Added); 401 | 402 | _context.ChangeTracker.Clear(); 403 | } 404 | 405 | [Fact] 406 | public async Task AddManyAsync_ShouldAddEntitiesToContext() 407 | { 408 | // Arrange 409 | var entities = new List 410 | { 411 | new() { Id = 64, Name = "Test Entity 1" }, 412 | new() { Id = 74, Name = "Test Entity 2" } 413 | }; 414 | 415 | // Act 416 | await _repository.AddManyAsync(entities); 417 | 418 | // Assert 419 | _context.Entry(entities[0]).State.Should().Be(EntityState.Added); 420 | _context.Entry(entities[1]).State.Should().Be(EntityState.Added); 421 | 422 | _context.ChangeTracker.Clear(); 423 | } 424 | 425 | [Fact] 426 | public void AddMany_ShouldAddEntitiesToContext() 427 | { 428 | // Arrange 429 | var entities = new List 430 | { 431 | new() { Id = 62, Name = "Test Entity 1" }, 432 | new() { Id = 72, Name = "Test Entity 2" } 433 | }; 434 | 435 | // Act 436 | _repository.AddMany(entities); 437 | 438 | // Assert 439 | _context.Entry(entities[0]).State.Should().Be(EntityState.Added); 440 | _context.Entry(entities[1]).State.Should().Be(EntityState.Added); 441 | 442 | _context.ChangeTracker.Clear(); 443 | } 444 | 445 | [Fact] 446 | public void UpdateOne_ShouldUpdateEntityInContext() 447 | { 448 | // Arrange 449 | var entity = new TestEntity { Id = 776, Name = "Test Entity" }; 450 | _repository.AddOne(entity); 451 | 452 | entity.Name = "Updated Test Entity"; 453 | 454 | // Act 455 | _repository.UpdateOne(entity); 456 | 457 | // Assert 458 | _context.Entry(entity).State.Should().Be(EntityState.Modified); 459 | 460 | _context.ChangeTracker.Clear(); 461 | } 462 | 463 | [Fact] 464 | public void UpdateMany_ShouldSaveChangesToContext() 465 | { 466 | // Arrange 467 | var entities = new List 468 | { 469 | new() { Id = 456, Name = "Test Entity 1" }, 470 | new() { Id = 457, Name = "Test Entity 2" } 471 | }; 472 | _repository.AddMany(entities); 473 | 474 | entities[0].Name = "Updated Test Entity 1"; 475 | entities[1].Name = "Updated Test Entity 2"; 476 | 477 | // Act 478 | _repository.UpdateMany(entities); 479 | 480 | // Assert 481 | _context.Entry(entities[0]).State.Should().Be(EntityState.Modified); 482 | _context.Entry(entities[1]).State.Should().Be(EntityState.Modified); 483 | 484 | _context.ChangeTracker.Clear(); 485 | } 486 | 487 | [Fact] 488 | public void RemoveOne_ShouldRemoveEntityFromContext() 489 | { 490 | // Arrange 491 | var entity = new TestEntity { Id = 608, Name = "Test Entity" }; 492 | _repository.AddOne(entity); 493 | _context.SaveChanges(); 494 | 495 | // Act 496 | _repository.RemoveOne(entity); 497 | 498 | // Assert 499 | _context.Entry(entity).State.Should().Be(EntityState.Deleted); 500 | 501 | _context.ChangeTracker.Clear(); 502 | } 503 | 504 | [Fact] 505 | public void RemoveOne_UsingFilters_ShouldRemoveEntityFromContext() 506 | { 507 | // Arrange 508 | var entity = new TestEntity { Id = 657, Name = "Test Entity" }; 509 | _repository.AddOne(entity); 510 | _context.SaveChanges(); 511 | 512 | // Act 513 | _repository.RemoveOne([e => e.Id == entity.Id]); 514 | 515 | // Assert 516 | _context.Entry(entity).State.Should().Be(EntityState.Deleted); 517 | 518 | _context.ChangeTracker.Clear(); 519 | } 520 | 521 | [Fact] 522 | public void RemoveMany_UsingFilters_ShouldRemoveEntitiesFromDbSet_WhenEntitiesExist() 523 | { 524 | // Arrange 525 | var entities = new List 526 | { 527 | new() { Id = 81, Name = "Entity 1" }, 528 | new() { Id = 82, Name = "Entity 2" } 529 | }; 530 | _repository.AddMany(entities); 531 | _context.SaveChanges(); 532 | 533 | // Act 534 | _repository.RemoveMany([e => entities.Select(testEntity => testEntity.Id).Contains(e.Id)]); 535 | 536 | // Assert 537 | _context.TestEntities.Where(e => e.Id == 81 || e.Id == 82) 538 | .ToList() 539 | .All(e => _context.Entry(e).State == EntityState.Deleted).Should().BeTrue(); 540 | 541 | _context.ChangeTracker.Clear(); 542 | } 543 | } --------------------------------------------------------------------------------