├── .editorconfig ├── .gitattributes ├── .gitignore ├── ConfigureMSDTC.ps1 ├── LICENSE.txt ├── MR.Patterns.Repository.sln ├── README.md ├── appveyor.yml ├── build.cake ├── build.ps1 ├── build ├── index.cake ├── util.cake ├── version.cake └── version.props ├── samples ├── Basic.Tests │ ├── Basic.Tests.csproj │ ├── Services │ │ └── BlogsManagerTest.cs │ └── TestHost.cs └── Basic │ ├── Basic.csproj │ ├── DIConfiguration.cs │ ├── Models │ ├── AppDbContext.cs │ ├── Blog.cs │ └── Post.cs │ ├── Program.cs │ └── Services │ ├── BlogsManager.cs │ ├── IRepository.Default.cs │ ├── IRepository.InMemory.cs │ ├── IRepository.cs │ └── Math.cs ├── src └── MR.Patterns.Repository │ ├── EqualityComparers.cs │ ├── IEntity.cs │ ├── IRepositoryCore.Default.cs │ ├── IRepositoryCore.Extensions.cs │ ├── IRepositoryCore.InMemory.cs │ ├── IRepositoryCore.cs │ ├── Internal │ └── ReflectionHelper.cs │ ├── MR.Patterns.Repository.csproj │ ├── PKGenerator.cs │ ├── QueryableExtensions.cs │ └── UnitTestDetector.cs └── test ├── MR.Patterns.Repository.IntegrationTests ├── DatabaseTestHost.cs ├── MR.Patterns.Repository.IntegrationTests.csproj ├── Models.cs ├── ReentranceTest.cs ├── TestDbContext.cs ├── TestRepository.cs └── xunit.runner.json └── MR.Patterns.Repository.Tests ├── InMemoryRepositoryCoreTest.cs ├── MR.Patterns.Repository.Tests.csproj └── PKGeneratorTest.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{cs,js,cake}] 4 | indent_style = tab 5 | indent_size = 4 6 | trim_trailing_whitespace = true 7 | 8 | [*.json] 9 | indent_style = space 10 | indent_size = 2 11 | trim_trailing_whitespace = true 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.cs diff=csharp 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.suo 2 | *.user 3 | *.lock.json 4 | 5 | .vs/ 6 | [Bb]in/ 7 | [Oo]bj/ 8 | artifacts/ 9 | node_modules/ 10 | **/wwwroot/lib/ 11 | 12 | tools/ 13 | -------------------------------------------------------------------------------- /ConfigureMSDTC.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mrahhal/MR.Patterns.Repository/724469672c3e51762163bbf5c107d0af62df908b/ConfigureMSDTC.ps1 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016, Mohammad Rahhal @mrahhal 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. -------------------------------------------------------------------------------- /MR.Patterns.Repository.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26430.6 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{28C15C68-D034-4247-BD6C-780A23F6CD4D}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{57D55E7F-20D4-4FEA-B491-B24783A34271}" 9 | ProjectSection(SolutionItems) = preProject 10 | .editorconfig = .editorconfig 11 | .gitattributes = .gitattributes 12 | .gitignore = .gitignore 13 | appveyor.yml = appveyor.yml 14 | README.md = README.md 15 | EndProjectSection 16 | EndProject 17 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{D0DF305B-A160-4C34-B787-AA08D41178E6}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7E928912-BFFF-470E-8FB8-56FC754F8716}" 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{1DC79ED7-7E9E-4ABE-9203-44046420F144}" 22 | ProjectSection(SolutionItems) = preProject 23 | build.cake = build.cake 24 | build.ps1 = build.ps1 25 | build\index.cake = build\index.cake 26 | build\util.cake = build\util.cake 27 | build\version.cake = build\version.cake 28 | build\version.props = build\version.props 29 | EndProjectSection 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MR.Patterns.Repository", "src\MR.Patterns.Repository\MR.Patterns.Repository.csproj", "{277BD246-4427-4F7B-A331-B62C6D72EC62}" 32 | EndProject 33 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basic", "samples\Basic\Basic.csproj", "{8DE5A0B8-ACBD-4C68-BE73-422974DC4AE0}" 34 | EndProject 35 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Basic.Tests", "samples\Basic.Tests\Basic.Tests.csproj", "{650A5A85-5956-491E-9312-5E25A27D1108}" 36 | EndProject 37 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MR.Patterns.Repository.Tests", "test\MR.Patterns.Repository.Tests\MR.Patterns.Repository.Tests.csproj", "{6C76FCF1-1790-477F-A279-B4B889BB90BE}" 38 | EndProject 39 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MR.Patterns.Repository.IntegrationTests", "test\MR.Patterns.Repository.IntegrationTests\MR.Patterns.Repository.IntegrationTests.csproj", "{2104EFF7-72D8-4CAF-A44A-8B6D481C8D1B}" 40 | EndProject 41 | Global 42 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 43 | Debug|Any CPU = Debug|Any CPU 44 | Release|Any CPU = Release|Any CPU 45 | EndGlobalSection 46 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 47 | {277BD246-4427-4F7B-A331-B62C6D72EC62}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {277BD246-4427-4F7B-A331-B62C6D72EC62}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {277BD246-4427-4F7B-A331-B62C6D72EC62}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {277BD246-4427-4F7B-A331-B62C6D72EC62}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {8DE5A0B8-ACBD-4C68-BE73-422974DC4AE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {8DE5A0B8-ACBD-4C68-BE73-422974DC4AE0}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {8DE5A0B8-ACBD-4C68-BE73-422974DC4AE0}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {8DE5A0B8-ACBD-4C68-BE73-422974DC4AE0}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {650A5A85-5956-491E-9312-5E25A27D1108}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {650A5A85-5956-491E-9312-5E25A27D1108}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {650A5A85-5956-491E-9312-5E25A27D1108}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {650A5A85-5956-491E-9312-5E25A27D1108}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {6C76FCF1-1790-477F-A279-B4B889BB90BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {6C76FCF1-1790-477F-A279-B4B889BB90BE}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {6C76FCF1-1790-477F-A279-B4B889BB90BE}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {6C76FCF1-1790-477F-A279-B4B889BB90BE}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {2104EFF7-72D8-4CAF-A44A-8B6D481C8D1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 64 | {2104EFF7-72D8-4CAF-A44A-8B6D481C8D1B}.Debug|Any CPU.Build.0 = Debug|Any CPU 65 | {2104EFF7-72D8-4CAF-A44A-8B6D481C8D1B}.Release|Any CPU.ActiveCfg = Release|Any CPU 66 | {2104EFF7-72D8-4CAF-A44A-8B6D481C8D1B}.Release|Any CPU.Build.0 = Release|Any CPU 67 | EndGlobalSection 68 | GlobalSection(SolutionProperties) = preSolution 69 | HideSolutionNode = FALSE 70 | EndGlobalSection 71 | GlobalSection(NestedProjects) = preSolution 72 | {277BD246-4427-4F7B-A331-B62C6D72EC62} = {28C15C68-D034-4247-BD6C-780A23F6CD4D} 73 | {8DE5A0B8-ACBD-4C68-BE73-422974DC4AE0} = {D0DF305B-A160-4C34-B787-AA08D41178E6} 74 | {650A5A85-5956-491E-9312-5E25A27D1108} = {D0DF305B-A160-4C34-B787-AA08D41178E6} 75 | {6C76FCF1-1790-477F-A279-B4B889BB90BE} = {7E928912-BFFF-470E-8FB8-56FC754F8716} 76 | {2104EFF7-72D8-4CAF-A44A-8B6D481C8D1B} = {7E928912-BFFF-470E-8FB8-56FC754F8716} 77 | EndGlobalSection 78 | EndGlobal 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MR.Patterns.Repository 2 | 3 | [![Build status](https://img.shields.io/appveyor/ci/mrahhal/mr-patterns-repository/master.svg)](https://ci.appveyor.com/project/mrahhal/mr-patterns-repository) 4 | [![NuGet version](https://badge.fury.io/nu/MR.Patterns.Repository.svg)](https://www.nuget.org/packages/MR.Patterns.Repository) 5 | [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://opensource.org/licenses/MIT) 6 | 7 | Provides a base when implementing the repository pattern with EF6. 8 | 9 | (Version 3.1 and above supports EF 6.4) 10 | 11 | ## Objectives 12 | 13 | - Quickly implement the pattern with little to no plumbing or boilerplate on your end. 14 | - Ease out unit testing by providing some kind of an in memory repository store. 15 | 16 | To that end: 17 | 18 | - Have a base repository class that handles most of the plumbing (like implementing Add, Remove, Update, SaveChanges, Transactions, ...). 19 | - Have another base repository class that in memory implementations should inherit from. It should basically be an in memory store for entities that also provides things like ensuring (int, long) PKs get a proper incrementing value when entities are added. 20 | 21 | ## The basics 22 | 23 | `IRepositoryCore`, `RepositoryCore`, and `InMemoryRepositoryCore` are provided for your (real) Repository classes to inherit from. They do most of the work. 24 | 25 | ## A simple example 26 | 27 | ```cs 28 | // This is your service. 29 | public interface IRepository : IRepositoryCore 30 | { 31 | IQueryable Blogs { get; } 32 | } 33 | ``` 34 | 35 | ```cs 36 | // This is the real repository that stores entities in a database. 37 | public class Repository : RepositoryCore, IRepository 38 | { 39 | public Repository(ApplicationDbContext context) : base(context) { } 40 | 41 | IQueryable Blogs => Context.Blogs; 42 | 43 | // Add, Remove, Update, SaveChangesAsync are all implemented in RepositoryCore. 44 | } 45 | ``` 46 | 47 | ```cs 48 | // This is the in memory repository that will depend on InMemoryRepositoryCore to store entities in memory. 49 | public class InMemoryRepository : InMemoryRepositoryCore, IRepository 50 | { 51 | // For() is a method on InMemoryRepositoryCore 52 | IQueryable Blogs => For(); 53 | 54 | // Add, Remove, Update, SaveChangesAsync are all implemented in InMemoryRepositoryCore. 55 | } 56 | ``` 57 | 58 | Note: The `InMemoryRepository` requires the PK property to be called "Id" for auto incrementing to work. 59 | 60 | ## Nice improvements to have 61 | 62 | - Relationship fixups for the in memory store. 63 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: '{build}' 2 | os: Visual Studio 2019 3 | environment: 4 | BUILDING_ON_PLATFORM: win 5 | BuildEnvironment: appveyor 6 | Jobs_SqlServer_ConnectionStringTemplate: Server=.\SQL2012SP1;Database={0};User ID=sa;Password=Password12! 7 | services: 8 | - mssql2012sp1 9 | build_script: 10 | - ps: ./ConfigureMSDTC.ps1 11 | - ps: ./build.ps1 12 | test: off 13 | artifacts: 14 | - path: artifacts/packages/*.nupkg 15 | deploy: 16 | provider: NuGet 17 | on: 18 | appveyor_repo_tag: true 19 | api_key: 20 | secure: PWCeOQ12fgAhPiBjgkBAqHbvaPQArarsO2sI+8KCeXjBjE+AHaMIMW8b7efmDzQ6 21 | skip_symbols: true 22 | artifact: /artifacts\/packages\/.+\.nupkg/ 23 | -------------------------------------------------------------------------------- /build.cake: -------------------------------------------------------------------------------- 1 | #addin "nuget:https://www.nuget.org/api/v2?package=Newtonsoft.Json&version=9.0.1" 2 | 3 | #load "./build/index.cake" 4 | 5 | var target = Argument("target", "Default"); 6 | 7 | var build = BuildParameters.Create(Context); 8 | var util = new Util(Context, build); 9 | 10 | Task("Clean") 11 | .Does(() => 12 | { 13 | if (DirectoryExists("./artifacts")) 14 | { 15 | DeleteDirectory("./artifacts", true); 16 | } 17 | }); 18 | 19 | Task("Restore") 20 | .IsDependentOn("Clean") 21 | .Does(() => 22 | { 23 | DotNetCoreRestore(); 24 | }); 25 | 26 | Task("Build") 27 | .IsDependentOn("Restore") 28 | .Does(() => 29 | { 30 | var settings = new DotNetCoreBuildSettings 31 | { 32 | Configuration = build.Configuration, 33 | VersionSuffix = build.Version.Suffix, 34 | ArgumentCustomization = args => 35 | { 36 | args.Append($"/p:InformationalVersion={build.Version.VersionWithSuffix()}"); 37 | return args; 38 | } 39 | }; 40 | foreach (var project in build.ProjectFiles) 41 | { 42 | DotNetCoreBuild(project.FullPath, settings); 43 | } 44 | }); 45 | 46 | Task("Test") 47 | .IsDependentOn("Build") 48 | .Does(() => 49 | { 50 | foreach (var testProject in build.TestProjectFiles) 51 | { 52 | DotNetCoreTest(testProject.FullPath); 53 | } 54 | }); 55 | 56 | Task("Pack") 57 | .Does(() => 58 | { 59 | var settings = new DotNetCorePackSettings 60 | { 61 | Configuration = build.Configuration, 62 | VersionSuffix = build.Version.Suffix, 63 | OutputDirectory = "./artifacts/packages" 64 | }; 65 | foreach (var project in build.ProjectFiles) 66 | { 67 | DotNetCorePack(project.FullPath, settings); 68 | } 69 | }); 70 | 71 | Task("Default") 72 | .IsDependentOn("Build") 73 | //.IsDependentOn("Test") 74 | .IsDependentOn("Pack") 75 | .Does(() => 76 | { 77 | util.PrintInfo(); 78 | }); 79 | 80 | Task("Version") 81 | .Does(() => 82 | { 83 | Information($"{build.FullVersion()}"); 84 | }); 85 | 86 | Task("Print") 87 | .Does(() => 88 | { 89 | util.PrintInfo(); 90 | }); 91 | 92 | RunTarget(target); 93 | -------------------------------------------------------------------------------- /build.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | 3 | .SYNOPSIS 4 | This is a Powershell script to bootstrap a Cake build. 5 | 6 | .DESCRIPTION 7 | This Powershell script will download NuGet if missing, restore NuGet tools (including Cake) 8 | and execute your Cake build script with the parameters you provide. 9 | 10 | .PARAMETER Script 11 | The build script to execute. 12 | .PARAMETER Target 13 | The build script target to run. 14 | .PARAMETER Configuration 15 | The build configuration to use. 16 | .PARAMETER Verbosity 17 | Specifies the amount of information to be displayed. 18 | .PARAMETER Experimental 19 | Tells Cake to use the latest Roslyn release. 20 | .PARAMETER WhatIf 21 | Performs a dry run of the build script. 22 | No tasks will be executed. 23 | .PARAMETER Mono 24 | Tells Cake to use the Mono scripting engine. 25 | .PARAMETER SkipToolPackageRestore 26 | Skips restoring of packages. 27 | .PARAMETER ScriptArgs 28 | Remaining arguments are added here. 29 | 30 | .LINK 31 | http://cakebuild.net 32 | 33 | #> 34 | 35 | [CmdletBinding()] 36 | Param( 37 | [string]$Script = "build.cake", 38 | [string]$Target = "Default", 39 | [ValidateSet("Release", "Debug")] 40 | [string]$Configuration = "Debug", 41 | [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] 42 | [string]$Verbosity = "Normal", 43 | [switch]$Experimental = $true, 44 | [Alias("DryRun","Noop")] 45 | [switch]$WhatIf, 46 | [switch]$Mono, 47 | [switch]$SkipToolPackageRestore, 48 | [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] 49 | [string[]]$ScriptArgs 50 | ) 51 | 52 | [Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null 53 | function MD5HashFile([string] $filePath) 54 | { 55 | if ([string]::IsNullOrEmpty($filePath) -or !(Test-Path $filePath -PathType Leaf)) 56 | { 57 | return $null 58 | } 59 | 60 | [System.IO.Stream] $file = $null; 61 | [System.Security.Cryptography.MD5] $md5 = $null; 62 | try 63 | { 64 | $md5 = [System.Security.Cryptography.MD5]::Create() 65 | $file = [System.IO.File]::OpenRead($filePath) 66 | return [System.BitConverter]::ToString($md5.ComputeHash($file)) 67 | } 68 | finally 69 | { 70 | if ($file -ne $null) 71 | { 72 | $file.Dispose() 73 | } 74 | } 75 | } 76 | 77 | Write-Host "Preparing to run build script..." 78 | 79 | if(!$PSScriptRoot){ 80 | $PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent 81 | } 82 | 83 | $TOOLS_DIR = Join-Path $PSScriptRoot "tools" 84 | $NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" 85 | $CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" 86 | $NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" 87 | $PACKAGES_CONFIG = Join-Path $TOOLS_DIR "packages.config" 88 | $PACKAGES_CONFIG_MD5 = Join-Path $TOOLS_DIR "packages.config.md5sum" 89 | 90 | # Should we use mono? 91 | $UseMono = ""; 92 | if($Mono.IsPresent) { 93 | Write-Verbose -Message "Using the Mono based scripting engine." 94 | $UseMono = "-mono" 95 | } 96 | 97 | # Should we use the new Roslyn? 98 | $UseExperimental = ""; 99 | if($Experimental.IsPresent -and !($Mono.IsPresent)) { 100 | Write-Verbose -Message "Using experimental version of Roslyn." 101 | $UseExperimental = "-experimental" 102 | } 103 | 104 | # Is this a dry run? 105 | $UseDryRun = ""; 106 | if($WhatIf.IsPresent) { 107 | $UseDryRun = "-dryrun" 108 | } 109 | 110 | # Make sure tools folder exists 111 | if ((Test-Path $PSScriptRoot) -and !(Test-Path $TOOLS_DIR)) { 112 | Write-Verbose -Message "Creating tools directory..." 113 | New-Item -Path $TOOLS_DIR -Type directory | out-null 114 | } 115 | 116 | # Make sure that packages.config exist. 117 | if (!(Test-Path $PACKAGES_CONFIG)) { 118 | Write-Verbose -Message "Downloading packages.config..." 119 | try { (New-Object System.Net.WebClient).DownloadFile("http://cakebuild.net/download/bootstrapper/packages", $PACKAGES_CONFIG) } catch { 120 | Throw "Could not download packages.config." 121 | } 122 | } 123 | 124 | # Try find NuGet.exe in path if not exists 125 | if (!(Test-Path $NUGET_EXE)) { 126 | Write-Verbose -Message "Trying to find nuget.exe in PATH..." 127 | $existingPaths = $Env:Path -Split ';' | Where-Object { (![string]::IsNullOrEmpty($_)) -and (Test-Path $_) } 128 | $NUGET_EXE_IN_PATH = Get-ChildItem -Path $existingPaths -Filter "nuget.exe" | Select -First 1 129 | if ($NUGET_EXE_IN_PATH -ne $null -and (Test-Path $NUGET_EXE_IN_PATH.FullName)) { 130 | Write-Verbose -Message "Found in PATH at $($NUGET_EXE_IN_PATH.FullName)." 131 | $NUGET_EXE = $NUGET_EXE_IN_PATH.FullName 132 | } 133 | } 134 | 135 | # Try download NuGet.exe if not exists 136 | if (!(Test-Path $NUGET_EXE)) { 137 | Write-Verbose -Message "Downloading NuGet.exe..." 138 | try { 139 | (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) 140 | } catch { 141 | Throw "Could not download NuGet.exe." 142 | } 143 | } 144 | 145 | # Save nuget.exe path to environment to be available to child processed 146 | $ENV:NUGET_EXE = $NUGET_EXE 147 | 148 | # Restore tools from NuGet? 149 | if(-Not $SkipToolPackageRestore.IsPresent) { 150 | Push-Location 151 | Set-Location $TOOLS_DIR 152 | 153 | # Check for changes in packages.config and remove installed tools if true. 154 | [string] $md5Hash = MD5HashFile($PACKAGES_CONFIG) 155 | if((!(Test-Path $PACKAGES_CONFIG_MD5)) -Or 156 | ($md5Hash -ne (Get-Content $PACKAGES_CONFIG_MD5 ))) { 157 | Write-Verbose -Message "Missing or changed package.config hash..." 158 | Remove-Item * -Recurse -Exclude packages.config,nuget.exe 159 | } 160 | 161 | Write-Verbose -Message "Restoring tools from NuGet..." 162 | $NuGetOutput = Invoke-Expression "&`"$NUGET_EXE`" install -ExcludeVersion -OutputDirectory `"$TOOLS_DIR`"" 163 | 164 | if ($LASTEXITCODE -ne 0) { 165 | Throw "An error occured while restoring NuGet tools." 166 | } 167 | else 168 | { 169 | $md5Hash | Out-File $PACKAGES_CONFIG_MD5 -Encoding "ASCII" 170 | } 171 | Write-Verbose -Message ($NuGetOutput | out-string) 172 | Pop-Location 173 | } 174 | 175 | # Make sure that Cake has been installed. 176 | if (!(Test-Path $CAKE_EXE)) { 177 | Throw "Could not find Cake.exe at $CAKE_EXE" 178 | } 179 | 180 | # Start Cake 181 | Write-Host "Running build script..." 182 | Invoke-Expression "& `"$CAKE_EXE`" `"$Script`" -target=`"$Target`" -configuration=`"$Configuration`" -verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" 183 | exit $LASTEXITCODE 184 | -------------------------------------------------------------------------------- /build/index.cake: -------------------------------------------------------------------------------- 1 | #load "./util.cake" 2 | #load "./version.cake" 3 | -------------------------------------------------------------------------------- /build/util.cake: -------------------------------------------------------------------------------- 1 | public class Util 2 | { 3 | public Util(ICakeContext context, BuildParameters build) 4 | { 5 | Context = context; 6 | Build = build; 7 | } 8 | 9 | public ICakeContext Context { get; set; } 10 | public BuildParameters Build { get; set; } 11 | 12 | public void PrintInfo() 13 | { 14 | Context.Information($@" 15 | Version: {Build.FullVersion()} 16 | Configuration: {Build.Configuration} 17 | "); 18 | } 19 | 20 | public static string CreateStamp() 21 | { 22 | var seconds = (long)(DateTime.UtcNow - new DateTime(2017, 1, 1)).TotalSeconds; 23 | return seconds.ToString().PadLeft(11, (char)'0'); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /build/version.cake: -------------------------------------------------------------------------------- 1 | using System.Xml; 2 | 3 | public class BuildParameters 4 | { 5 | public BuildParameters(ICakeContext context) 6 | { 7 | Context = context; 8 | } 9 | 10 | public ICakeContext Context { get; } 11 | public BuildVersion Version { get; private set; } 12 | public string Configuration { get; private set; } 13 | public bool IsTagged { get; private set; } 14 | public bool IsCI { get; private set; } 15 | public DirectoryPathCollection Projects { get; set; } 16 | public DirectoryPathCollection TestProjects { get; set; } 17 | public FilePathCollection ProjectFiles { get; set; } 18 | public FilePathCollection TestProjectFiles { get; set; } 19 | 20 | public static BuildParameters Create(ICakeContext context) 21 | { 22 | var buildParameters = new BuildParameters(context); 23 | buildParameters.Initialize(); 24 | return buildParameters; 25 | } 26 | 27 | public string FullVersion() 28 | { 29 | return Version.VersionWithSuffix(); 30 | } 31 | 32 | private void Initialize() 33 | { 34 | InitializeCore(); 35 | InitializeVersion(); 36 | } 37 | 38 | private void InitializeCore() 39 | { 40 | Projects = Context.GetDirectories("./src/*"); 41 | TestProjects = Context.GetDirectories("./test/*"); 42 | ProjectFiles = Context.GetFiles("./src/*/*.csproj"); 43 | TestProjectFiles = Context.GetFiles("./test/*/*.csproj"); 44 | 45 | var buildSystem = Context.BuildSystem(); 46 | if (!buildSystem.IsLocalBuild) 47 | { 48 | IsCI = true; 49 | if ((buildSystem.IsRunningOnAppVeyor && buildSystem.AppVeyor.Environment.Repository.Tag.IsTag) || 50 | (buildSystem.IsRunningOnTravisCI && string.IsNullOrWhiteSpace(buildSystem.TravisCI.Environment.Build.Tag))) 51 | { 52 | IsTagged = true; 53 | } 54 | } 55 | 56 | Configuration = Context.Argument("Configuration", "Debug"); 57 | if (IsCI) 58 | { 59 | Configuration = "Release"; 60 | } 61 | } 62 | 63 | private void InitializeVersion() 64 | { 65 | var versionFile = Context.File("./build/version.props"); 66 | var content = System.IO.File.ReadAllText(versionFile.Path.FullPath); 67 | 68 | XmlDocument doc = new XmlDocument(); 69 | doc.LoadXml(content); 70 | 71 | var versionMajor = doc.DocumentElement.SelectSingleNode("/Project/PropertyGroup/VersionMajor").InnerText; 72 | var versionMinor = doc.DocumentElement.SelectSingleNode("/Project/PropertyGroup/VersionMinor").InnerText; 73 | var versionPatch = doc.DocumentElement.SelectSingleNode("/Project/PropertyGroup/VersionPatch").InnerText; 74 | var versionQuality = doc.DocumentElement.SelectSingleNode("/Project/PropertyGroup/VersionQuality").InnerText; 75 | versionQuality = string.IsNullOrWhiteSpace(versionQuality) ? null : versionQuality; 76 | 77 | var suffix = versionQuality; 78 | if (!IsTagged) 79 | { 80 | suffix += (IsCI ? "ci-" : "dv-") + Util.CreateStamp(); 81 | } 82 | suffix = string.IsNullOrWhiteSpace(suffix) ? null : suffix; 83 | 84 | Version = 85 | new BuildVersion(int.Parse(versionMajor), int.Parse(versionMinor), int.Parse(versionPatch), versionQuality); 86 | Version.Suffix = suffix; 87 | } 88 | } 89 | 90 | public class BuildVersion 91 | { 92 | public BuildVersion(int major, int minor, int patch, string quality) 93 | { 94 | Major = major; 95 | Minor = minor; 96 | Patch = patch; 97 | Quality = quality; 98 | } 99 | 100 | public int Major { get; set; } 101 | public int Minor { get; set; } 102 | public int Patch { get; set; } 103 | public string Quality { get; set; } 104 | public string Suffix { get; set; } 105 | 106 | public string VersionWithoutQuality() 107 | { 108 | return $"{Major}.{Minor}.{Patch}"; 109 | } 110 | 111 | public string Version() 112 | { 113 | return VersionWithoutQuality() + (Quality == null ? string.Empty : $"-{Quality}"); 114 | } 115 | 116 | public string VersionWithSuffix() 117 | { 118 | return Version() + (Suffix == null ? string.Empty : $"-{Suffix}"); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /build/version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 3 4 | 1 5 | 0 6 | 7 | $(VersionMajor).$(VersionMinor).$(VersionPatch) 8 | 9 | 10 | -------------------------------------------------------------------------------- /samples/Basic.Tests/Basic.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | Basic.Tests 6 | Basic.Tests 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /samples/Basic.Tests/Services/BlogsManagerTest.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using System; 6 | 7 | namespace Basic.Services 8 | { 9 | public class BlogsManagerTest : TestHost 10 | { 11 | [Fact] 12 | public async Task GetBlogs_Empty() 13 | { 14 | var provider = CreateProvider(); 15 | var manager = provider.GetService(); 16 | 17 | var blogs = await manager.GetBlogs(); 18 | 19 | blogs.Should().BeEmpty(); 20 | } 21 | 22 | [Fact] 23 | public void CreateNewBlog_ArgumentNullCheck() 24 | { 25 | var provider = CreateProvider(); 26 | var manager = provider.GetService(); 27 | 28 | Assert.Throws(() => manager.CreateNewBlog(null).GetAwaiter().GetResult()); 29 | } 30 | 31 | [Fact] 32 | public async Task CreateNewBlog() 33 | { 34 | var provider = CreateProvider(); 35 | var repo = provider.GetService(); 36 | var manager = provider.GetService(); 37 | 38 | var blogId = await manager.CreateNewBlog("Foo"); 39 | 40 | blogId.Should().NotBe(0); 41 | } 42 | 43 | [Fact] 44 | public async Task CreateNewBlog_StoresBlog() 45 | { 46 | var provider = CreateProvider(); 47 | var repo = provider.GetService(); 48 | var manager = provider.GetService(); 49 | 50 | var blogId = await manager.CreateNewBlog("Foo"); 51 | 52 | (await manager.GetBlog(blogId)).Should().NotBeNull(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /samples/Basic.Tests/TestHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Basic 4 | { 5 | public class TestHost 6 | { 7 | static TestHost() 8 | { 9 | } 10 | 11 | public IServiceProvider CreateProvider() 12 | { 13 | var configuration = new DIConfiguration(); 14 | configuration.ConfigureCommon(); 15 | configuration.ConfigureForTests(); 16 | return configuration.Build(); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/Basic/Basic.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | Basic 6 | Exe 7 | Basic 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /samples/Basic/DIConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Entity; 3 | using Basic.Models; 4 | using Basic.Services; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Basic 8 | { 9 | public class DIConfiguration 10 | { 11 | private IServiceCollection _services = new ServiceCollection(); 12 | 13 | public IServiceCollection Services => _services; 14 | 15 | // Services that don't need to be mocked or changed between normal runs and tests goes here. 16 | public void ConfigureCommon() 17 | { 18 | _services.AddTransient(); 19 | _services.AddTransient(); 20 | } 21 | 22 | public void ConfigureDefaults() 23 | { 24 | // Typically, the DbContext should be scope registered in a web environment. 25 | // But this does it for a simple console app. 26 | _services.AddTransient(_ => 27 | new AppDbContext("Server=(localdb)\\mssqllocaldb;Database=MR.Patterns.Repository-Basic;Trusted_Connection=True;MultipleActiveResultSets=true")); 28 | Database.SetInitializer(new DropCreateDatabaseIfModelChanges()); 29 | 30 | // Repository asks for DbContext so we should also do transient here. 31 | _services.AddTransient(); 32 | } 33 | 34 | public void ConfigureForTests() 35 | { 36 | // We can't do scoped here, so singleton will 37 | // suffice in tests since we're always recreating the provider. 38 | _services.AddSingleton(); 39 | } 40 | 41 | public IServiceProvider Build() => _services.BuildServiceProvider(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/Basic/Models/AppDbContext.cs: -------------------------------------------------------------------------------- 1 | using System.Data.Entity; 2 | 3 | namespace Basic.Models 4 | { 5 | public class AppDbContext : DbContext 6 | { 7 | public AppDbContext(string nameOrConnectionString) : base(nameOrConnectionString) 8 | { 9 | } 10 | 11 | public DbSet Blogs { get; set; } 12 | 13 | public DbSet Posts { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /samples/Basic/Models/Blog.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MR.Patterns.Repository; 3 | 4 | namespace Basic.Models 5 | { 6 | public class Blog : IEntity 7 | { 8 | public int Id { get; set; } 9 | 10 | public string Name { get; set; } 11 | 12 | public List Posts { get; set; } = new List(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/Basic/Models/Post.cs: -------------------------------------------------------------------------------- 1 | using MR.Patterns.Repository; 2 | 3 | namespace Basic.Models 4 | { 5 | public class Post : IEntity 6 | { 7 | public int Id { get; set; } 8 | 9 | public string Title { get; set; } 10 | 11 | public string Content { get; set; } 12 | 13 | public int BlogId { get; set; } 14 | public Blog Blog { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /samples/Basic/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Basic.Models; 4 | using Basic.Services; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using MR.Patterns.Repository; 7 | 8 | namespace Basic 9 | { 10 | public class Program 11 | { 12 | private IServiceProvider Provider { get; set; } 13 | 14 | public static void Main(string[] args) 15 | { 16 | new Program().Run(); 17 | } 18 | 19 | private void Run() 20 | { 21 | Initialize(); 22 | } 23 | 24 | private void Initialize() 25 | { 26 | Provider = ConfigureDI(); 27 | using (var context = Provider.GetService()) 28 | { 29 | TryInitializeDatabaseAsync(context).GetAwaiter().GetResult(); 30 | } 31 | GoAsync().GetAwaiter().GetResult(); 32 | } 33 | 34 | private async Task GoAsync() 35 | { 36 | using (var repository = Provider.GetService()) 37 | { 38 | var posts = await repository.Posts.ToListAsync(); 39 | Console.WriteLine($"There are {posts.Count} posts."); 40 | foreach (var post in posts) 41 | { 42 | Console.WriteLine($"Post: {post.Title}"); 43 | } 44 | } 45 | } 46 | 47 | private async Task TryInitializeDatabaseAsync(AppDbContext context) 48 | { 49 | if (!await context.Blogs.AnyAsync()) 50 | { 51 | context.Blogs.Add(new Blog() 52 | { 53 | Name = "My blog" 54 | }); 55 | await context.SaveChangesAsync(); 56 | } 57 | var blog = await context.Blogs.FirstAsync(); 58 | if (!await context.Posts.AnyAsync()) 59 | { 60 | blog.Posts.Add(new Post() 61 | { 62 | Title = "Some post title", 63 | Content = "Some post content" 64 | }); 65 | await context.SaveChangesAsync(); 66 | } 67 | } 68 | 69 | private IServiceProvider ConfigureDI() 70 | { 71 | var configuration = new DIConfiguration(); 72 | configuration.ConfigureCommon(); 73 | configuration.ConfigureDefaults(); 74 | return configuration.Build(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /samples/Basic/Services/BlogsManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Basic.Models; 5 | using MR.Patterns.Repository; 6 | 7 | namespace Basic.Services 8 | { 9 | public class BlogsManager 10 | { 11 | private IRepository _repository; 12 | 13 | public BlogsManager(IRepository repository) 14 | { 15 | _repository = repository; 16 | } 17 | 18 | public Task GetBlog(int id) 19 | { 20 | return _repository.Blogs.FindByIdAsync(id); 21 | } 22 | 23 | public Task> GetBlogs() 24 | { 25 | return _repository.Blogs.ToListAsync(); 26 | } 27 | 28 | public async Task CreateNewBlog(string name) 29 | { 30 | if (name == null) 31 | { 32 | throw new ArgumentNullException(nameof(name)); 33 | } 34 | 35 | var blog = new Blog 36 | { 37 | Name = name 38 | }; 39 | await _repository.AddAsync(blog); 40 | return blog.Id; 41 | } 42 | 43 | public async Task CreateNewPostInBlog(int blogId, string postTitle, string postContent) 44 | { 45 | if (postTitle == null) 46 | { 47 | throw new ArgumentNullException(nameof(postTitle)); 48 | } 49 | 50 | var blog = await _repository.Blogs.FindByIdAsync(blogId); 51 | if (blog == null) 52 | { 53 | throw new ArgumentException($"Could not find the blog with blog id: {blogId}."); 54 | } 55 | 56 | var post = new Post 57 | { 58 | Blog = blog, 59 | Title = postTitle, 60 | Content = postContent 61 | }; 62 | await _repository.AddAsync(post); 63 | return post.Id; 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /samples/Basic/Services/IRepository.Default.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Basic.Models; 3 | using MR.Patterns.Repository; 4 | 5 | namespace Basic.Services 6 | { 7 | public class Repository : RepositoryCore, IRepository 8 | { 9 | public Repository(AppDbContext context) 10 | : base(context) 11 | { 12 | } 13 | 14 | public IQueryable Blogs => Context.Blogs; 15 | 16 | public IQueryable Posts => Context.Posts; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /samples/Basic/Services/IRepository.InMemory.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Basic.Models; 3 | using MR.Patterns.Repository; 4 | 5 | namespace Basic.Services 6 | { 7 | public class InMemoryRepository : InMemoryRepositoryCore, IRepository 8 | { 9 | public IQueryable Blogs => For(); 10 | 11 | public IQueryable Posts => For(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /samples/Basic/Services/IRepository.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Basic.Models; 4 | using MR.Patterns.Repository; 5 | 6 | namespace Basic.Services 7 | { 8 | public interface IRepository : IRepositoryCore 9 | { 10 | IQueryable Blogs { get; } 11 | 12 | IQueryable Posts { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/Basic/Services/Math.cs: -------------------------------------------------------------------------------- 1 | namespace Basic.Services 2 | { 3 | public interface IMath 4 | { 5 | int Add(int x, int y); 6 | } 7 | 8 | public class DefaultMath : IMath 9 | { 10 | public virtual int Add(int x, int y) => x + y; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/EqualityComparers.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MR.Patterns.Repository 4 | { 5 | /// 6 | /// An that compares the property of int entities. 7 | /// 8 | public class EntityIntEqualityComparer : IEqualityComparer> 9 | { 10 | public static readonly EntityIntEqualityComparer Instance = new EntityIntEqualityComparer(); 11 | 12 | public bool Equals(IEntity x, IEntity y) 13 | { 14 | return x.Id == y.Id; 15 | } 16 | 17 | public int GetHashCode(IEntity obj) 18 | { 19 | return obj.Id.GetHashCode(); 20 | } 21 | } 22 | 23 | /// 24 | /// An that compares the property of string entities. 25 | /// 26 | public class EntityStringEqualityComparer : IEqualityComparer> 27 | { 28 | public static readonly EntityStringEqualityComparer Instance = new EntityStringEqualityComparer(); 29 | 30 | public bool Equals(IEntity x, IEntity y) 31 | { 32 | return x.Id == y.Id; 33 | } 34 | 35 | public int GetHashCode(IEntity obj) 36 | { 37 | return obj.Id.GetHashCode(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/IEntity.cs: -------------------------------------------------------------------------------- 1 | namespace MR.Patterns.Repository 2 | { 3 | public interface IEntity 4 | { 5 | TKey Id { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/IRepositoryCore.Default.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Data.Entity; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using System.Threading.Tasks; 9 | 10 | namespace MR.Patterns.Repository 11 | { 12 | public abstract class RepositoryCore : IRepositoryCore 13 | where TContext : DbContext 14 | { 15 | public RepositoryCore(TContext context) 16 | { 17 | Context = context; 18 | } 19 | 20 | public TContext Context { get; } 21 | 22 | public Database Database => Context.Database; 23 | 24 | public DbConnection Connection => Context.Database.Connection; 25 | 26 | public virtual void Add(TEntity entity) 27 | where TEntity : class 28 | { 29 | Context.Set().Add(entity); 30 | } 31 | 32 | public virtual void Update(TEntity entity) 33 | where TEntity : class 34 | { 35 | Context.Entry(entity).State = EntityState.Modified; 36 | } 37 | 38 | public virtual void Remove(TEntity entity) 39 | where TEntity : class 40 | { 41 | Context.Set().Remove(entity); 42 | } 43 | 44 | public virtual IQueryable Set() 45 | where TEntity : class 46 | { 47 | return Context.Set(); 48 | } 49 | 50 | public virtual Task SaveChangesAsync() => Context.SaveChangesAsync(); 51 | 52 | public void RunInTransaction( 53 | Action action, IsolationLevel? isolationLevel = null) 54 | { 55 | RunInTransaction((connection, transaction) => 56 | { 57 | action(connection, transaction); 58 | return true; 59 | }, isolationLevel); 60 | } 61 | 62 | public T RunInTransaction( 63 | Func func, IsolationLevel? isolationLevel = null) 64 | { 65 | T result; 66 | 67 | if (Database.CurrentTransaction != null) 68 | { 69 | result = func(Database.Connection, Database.CurrentTransaction.UnderlyingTransaction); 70 | } 71 | else 72 | { 73 | using (var contextTransaction = CreateTransaction(isolationLevel)) 74 | { 75 | try 76 | { 77 | result = func(Database.Connection, contextTransaction.UnderlyingTransaction); 78 | contextTransaction.Commit(); 79 | } 80 | catch 81 | { 82 | // Ignore rollback exceptions from the client side. 83 | try 84 | { 85 | contextTransaction.Rollback(); 86 | } 87 | catch 88 | { 89 | // No need to do anything here. 90 | } 91 | throw; 92 | } 93 | } 94 | } 95 | 96 | return result; 97 | } 98 | 99 | public Task RunInTransactionAsync( 100 | Func func, IsolationLevel? isolationLevel = null) 101 | { 102 | return RunInTransactionAsync(async (connection, transaction) => 103 | { 104 | await func(connection, transaction); 105 | return true; 106 | }, isolationLevel); 107 | } 108 | 109 | public async Task RunInTransactionAsync( 110 | Func> func, IsolationLevel? isolationLevel = null) 111 | { 112 | T result; 113 | 114 | if (Database.CurrentTransaction != null) 115 | { 116 | result = await func(Database.Connection, Database.CurrentTransaction.UnderlyingTransaction); 117 | } 118 | else 119 | { 120 | using (var contextTransaction = CreateTransaction(isolationLevel)) 121 | { 122 | try 123 | { 124 | result = await func(Database.Connection, contextTransaction.UnderlyingTransaction); 125 | contextTransaction.Commit(); 126 | } 127 | catch 128 | { 129 | // Ignore rollback exceptions from the client side. 130 | try 131 | { 132 | contextTransaction.Rollback(); 133 | } 134 | catch 135 | { 136 | // No need to do anything here. 137 | } 138 | throw; 139 | } 140 | } 141 | } 142 | 143 | return result; 144 | } 145 | 146 | public void Reload(T entity) 147 | where T : class 148 | { 149 | Context.Entry(entity).Reload(); 150 | } 151 | 152 | public Task ReloadAsync(T entity) 153 | where T : class 154 | { 155 | return Context.Entry(entity).ReloadAsync(); 156 | } 157 | 158 | public void SetState(T entity, EntityState state) 159 | where T : class 160 | { 161 | Context.Entry(entity).State = state; 162 | } 163 | 164 | public void SetPropertyModified(T entity, string propertyName) 165 | where T : class 166 | { 167 | Context.Entry(entity).Property(propertyName).IsModified = true; 168 | } 169 | 170 | public void SetPropertyModified(T entity, Expression> property) 171 | where T : class 172 | { 173 | Context.Entry(entity).Property(property).IsModified = true; 174 | } 175 | 176 | public void Load(T entity, Expression> property) 177 | where T : class 178 | where TProperty : class 179 | { 180 | Context.Entry(entity).Reference(property).Load(); 181 | } 182 | 183 | public Task LoadAsync(T entity, Expression> property) 184 | where T : class 185 | where TProperty : class 186 | { 187 | return Context.Entry(entity).Reference(property).LoadAsync(); 188 | } 189 | 190 | public void Load(T entity, Expression>> property) 191 | where T : class 192 | where TElement : class 193 | { 194 | Context.Entry(entity).Collection(property).Load(); 195 | } 196 | 197 | public Task LoadAsync(T entity, Expression>> property) 198 | where T : class 199 | where TElement : class 200 | { 201 | return Context.Entry(entity).Collection(property).LoadAsync(); 202 | } 203 | 204 | public IQueryable Query(T entity, Expression>> property) 205 | where T : class 206 | where TElement : class 207 | { 208 | return Context.Entry(entity).Collection(property).Query(); 209 | } 210 | 211 | public virtual void Dispose() 212 | { 213 | Context.Dispose(); 214 | } 215 | 216 | private DbContextTransaction CreateTransaction(IsolationLevel? isolationLevel) 217 | { 218 | return 219 | isolationLevel == null ? 220 | Database.BeginTransaction() : 221 | Database.BeginTransaction(isolationLevel.Value); 222 | } 223 | } 224 | } 225 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/IRepositoryCore.Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Data.Entity; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace MR.Patterns.Repository 7 | { 8 | public static class RepositoryExtensions 9 | { 10 | public static Task AddAsync(this IRepositoryCore @this, TEntity entity) 11 | where TEntity : class 12 | { 13 | @this.Add(entity); 14 | return @this.SaveChangesAsync(); 15 | } 16 | 17 | public static void AddRange(this IRepositoryCore @this, IEnumerable entities) 18 | where TEntity : class 19 | { 20 | foreach (var entity in entities) 21 | { 22 | @this.Add(entity); 23 | } 24 | } 25 | 26 | public static Task AddRangeAsync(this IRepositoryCore @this, IEnumerable entities) 27 | where TEntity : class 28 | { 29 | @this.AddRange(entities); 30 | return @this.SaveChangesAsync(); 31 | } 32 | 33 | public static Task UpdateAsync(this IRepositoryCore @this, TEntity entity) 34 | where TEntity : class 35 | { 36 | @this.Update(entity); 37 | return @this.SaveChangesAsync(); 38 | } 39 | 40 | public static void UpdateRange(this IRepositoryCore @this, IEnumerable entities) 41 | where TEntity : class 42 | { 43 | foreach (var entity in entities) 44 | { 45 | @this.Update(entity); 46 | } 47 | } 48 | 49 | public static Task UpdateRangeAsync(this IRepositoryCore @this, IEnumerable entities) 50 | where TEntity : class 51 | { 52 | @this.UpdateRange(entities); 53 | return @this.SaveChangesAsync(); 54 | } 55 | 56 | public static Task RemoveAsync(this IRepositoryCore @this, TEntity entity) 57 | where TEntity : class 58 | { 59 | @this.Remove(entity); 60 | return @this.SaveChangesAsync(); 61 | } 62 | 63 | public static void RemoveRange(this IRepositoryCore @this, IEnumerable entities) 64 | where TEntity : class 65 | { 66 | foreach (var entity in entities) 67 | { 68 | @this.Remove(entity); 69 | } 70 | } 71 | 72 | public static Task RemoveRangeAsync(this IRepositoryCore @this, IEnumerable entities) 73 | where TEntity : class 74 | { 75 | @this.RemoveRange(entities); 76 | return @this.SaveChangesAsync(); 77 | } 78 | 79 | public static void SetState(this IRepositoryCore @this, IEnumerable entities, EntityState state) 80 | where TEntity : class 81 | { 82 | foreach (var entity in entities) 83 | { 84 | @this.SetState(entity, state); 85 | } 86 | } 87 | 88 | public static void Detach(this IRepositoryCore @this, TEntity entity) 89 | where TEntity : class 90 | { 91 | @this.SetState(entity, EntityState.Detached); 92 | } 93 | 94 | public static void Detach(this IRepositoryCore @this, IEnumerable entities) 95 | where TEntity : class 96 | { 97 | foreach (var entity in entities) 98 | { 99 | @this.Detach(entity); 100 | } 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/IRepositoryCore.InMemory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Linq; 6 | using System.Linq.Expressions; 7 | using System.Reflection; 8 | using System.Threading.Tasks; 9 | using MR.Patterns.Repository.Internal; 10 | using System.Data.Entity; 11 | 12 | namespace MR.Patterns.Repository 13 | { 14 | public abstract class InMemoryRepositoryCore : IRepositoryCore 15 | { 16 | private Dictionary _tables = new Dictionary(); 17 | 18 | public virtual void Add(TEntity entity) 19 | where TEntity : class 20 | { 21 | var table = EnsureTable(); 22 | EnsurePK(table, entity); 23 | var entities = table.Entities as List; 24 | if (!entities.Contains(entity)) 25 | { 26 | entities.Add(entity); 27 | } 28 | } 29 | 30 | public virtual void Update(TEntity entity) 31 | where TEntity : class 32 | { 33 | } 34 | 35 | public virtual void Remove(TEntity entity) 36 | where TEntity : class 37 | { 38 | var table = EnsureTable(); 39 | var entities = table.Entities as List; 40 | if (entities.Contains(entity)) 41 | { 42 | entities.Remove(entity); 43 | } 44 | } 45 | 46 | public virtual IQueryable Set() 47 | where TEntity : class 48 | { 49 | return For(); 50 | } 51 | 52 | protected IQueryable For() 53 | where TEntity : class 54 | { 55 | var table = EnsureTable(); 56 | var entities = table.Entities as List; 57 | return entities.AsQueryable(); 58 | } 59 | 60 | public virtual Task SaveChangesAsync() => Task.FromResult(0); 61 | 62 | //------------------------------------------------------------------------------ 63 | 64 | private void EnsurePK(Table table, object entity) 65 | { 66 | table.EnsurePK(entity); 67 | } 68 | 69 | private Table FindTable() 70 | where TEntity : class 71 | { 72 | var entityType = typeof(TEntity); 73 | if (_tables.ContainsKey(entityType)) 74 | { 75 | return _tables[entityType]; 76 | } 77 | return null; 78 | } 79 | 80 | private Table EnsureTable() 81 | where TEntity : class 82 | { 83 | var entityType = typeof(TEntity); 84 | var table = FindTable(); 85 | if (table == null) 86 | { 87 | table = _tables[entityType] = 88 | Activator.CreateInstance(typeof(Table<>).MakeGenericType(entityType)) as Table; 89 | } 90 | return table; 91 | } 92 | 93 | public void RunInTransaction( 94 | Action action, IsolationLevel? isolationLevel) 95 | { 96 | RunInTransaction((_1, _2) => 97 | { 98 | action(_1, _2); 99 | return true; 100 | }, isolationLevel); 101 | } 102 | 103 | public T RunInTransaction( 104 | Func func, IsolationLevel? isolationLevel) 105 | { 106 | return func(null, null); 107 | } 108 | 109 | public Task RunInTransactionAsync( 110 | Func func, IsolationLevel? isolationLevel) 111 | { 112 | return RunInTransactionAsync((_1, _2) => 113 | { 114 | func(_1, _2).GetAwaiter().GetResult(); 115 | return Task.FromResult(true); 116 | }, isolationLevel); 117 | } 118 | 119 | public Task RunInTransactionAsync( 120 | Func> func, IsolationLevel? isolationLevel) 121 | { 122 | return func(null, null); 123 | } 124 | 125 | public void Reload(T entity) 126 | where T : class 127 | { 128 | } 129 | 130 | public Task ReloadAsync(T entity) 131 | where T : class 132 | { 133 | return Task.FromResult(0); 134 | } 135 | 136 | public void SetState(T entity, EntityState state) 137 | where T : class 138 | { 139 | } 140 | 141 | public void SetPropertyModified(T entity, string propertyName) 142 | where T : class 143 | { 144 | } 145 | 146 | public void SetPropertyModified(T entity, Expression> property) 147 | where T : class 148 | { 149 | } 150 | 151 | public void Load(T entity, Expression> property) 152 | where T : class 153 | where TProperty : class 154 | { 155 | } 156 | 157 | public Task LoadAsync(T entity, Expression> property) 158 | where T : class 159 | where TProperty : class 160 | { 161 | return Task.FromResult(0); 162 | } 163 | 164 | public void Load(T entity, Expression>> property) 165 | where T : class 166 | where TElement : class 167 | { 168 | } 169 | 170 | public Task LoadAsync(T entity, Expression>> property) 171 | where T : class 172 | where TElement : class 173 | { 174 | return Task.FromResult(0); 175 | } 176 | 177 | public IQueryable Query(T entity, Expression>> expression) 178 | where T : class 179 | where TElement : class 180 | { 181 | var pi = (expression.Body as MemberExpression).Member as PropertyInfo; 182 | var result = (ICollection)pi.GetMethod.Invoke(entity, new object[0]); 183 | return result.AsQueryable(); 184 | } 185 | 186 | public virtual void Dispose() 187 | { 188 | } 189 | 190 | private class Table 191 | { 192 | private Type _entityType; 193 | private PropertyInfo _idPI; 194 | private object _pkGenerator; 195 | 196 | public object Entities { get; } 197 | 198 | public Table(Type entityType) 199 | { 200 | _entityType = entityType; 201 | _idPI = ReflectionHelper.GetIdProperty(entityType); 202 | if (_idPI != null) 203 | { 204 | if (ReflectionHelper.IsCountType(_idPI.PropertyType)) 205 | { 206 | _pkGenerator = Activator.CreateInstance( 207 | typeof(PKGenerator<>).MakeGenericType(_idPI.PropertyType)); 208 | } 209 | } 210 | Entities = Activator.CreateInstance(typeof(List<>).MakeGenericType(_entityType)); 211 | } 212 | 213 | public object NextKey() 214 | { 215 | if (_pkGenerator == null) 216 | { 217 | throw new InvalidOperationException("The PK's type is not int or long, so no need to generate a PK ourselves."); 218 | } 219 | return typeof(PKGenerator<>) 220 | .MakeGenericType(_idPI.PropertyType) 221 | .GetMethod("Next") 222 | .Invoke(_pkGenerator, new object[0]); 223 | } 224 | 225 | public void EnsurePK(object entity) 226 | { 227 | if (_pkGenerator != null && ReflectionHelper.IsDefaultCountValue(_idPI, entity)) 228 | { 229 | _idPI.SetValue(entity, NextKey()); 230 | } 231 | } 232 | } 233 | 234 | private class Table : Table 235 | where TEntity : class 236 | { 237 | public Table() 238 | : base(typeof(TEntity)) 239 | { 240 | } 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/IRepositoryCore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Data; 4 | using System.Data.Common; 5 | using System.Data.Entity; 6 | using System.Linq; 7 | using System.Linq.Expressions; 8 | using System.Threading.Tasks; 9 | 10 | namespace MR.Patterns.Repository 11 | { 12 | public interface IRepositoryCore : IDisposable 13 | { 14 | void Add(TEntity entity) where TEntity : class; 15 | 16 | void Update(TEntity entity) where TEntity : class; 17 | 18 | void Remove(TEntity entity) where TEntity : class; 19 | 20 | IQueryable Set() where TEntity : class; 21 | 22 | Task SaveChangesAsync(); 23 | 24 | void RunInTransaction( 25 | Action action, IsolationLevel? isolationLevel = null); 26 | 27 | T RunInTransaction( 28 | Func func, IsolationLevel? isolationLevel = null); 29 | 30 | Task RunInTransactionAsync( 31 | Func func, IsolationLevel? isolationLevel = null); 32 | 33 | Task RunInTransactionAsync( 34 | Func> func, IsolationLevel? isolationLevel = null); 35 | 36 | void Reload(T entity) 37 | where T : class; 38 | 39 | Task ReloadAsync(T entity) 40 | where T : class; 41 | 42 | void SetState(T entity, EntityState state) 43 | where T : class; 44 | 45 | void SetPropertyModified(T entity, string propertyName) 46 | where T : class; 47 | 48 | void SetPropertyModified(T entity, Expression> property) 49 | where T : class; 50 | 51 | void Load(T entity, Expression> property) 52 | where T : class 53 | where TProperty : class; 54 | 55 | Task LoadAsync(T entity, Expression> property) 56 | where T : class 57 | where TProperty : class; 58 | 59 | void Load(T entity, Expression>> property) 60 | where T : class 61 | where TElement : class; 62 | 63 | Task LoadAsync(T entity, Expression>> property) 64 | where T : class 65 | where TElement : class; 66 | 67 | IQueryable Query(T entity, Expression>> property) 68 | where T : class 69 | where TElement : class; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/Internal/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace MR.Patterns.Repository.Internal 5 | { 6 | internal static class ReflectionHelper 7 | { 8 | public static bool IsDefaultCountValue(PropertyInfo property, object obj) 9 | { 10 | if (property.PropertyType == typeof(int)) 11 | return property.GetValue(obj).Equals((int)0); 12 | else if (property.PropertyType == typeof(long)) 13 | return property.GetValue(obj).Equals((long)0); 14 | else 15 | throw new InvalidOperationException("The property's type is expected to be either an int or a long."); 16 | } 17 | 18 | public static PropertyInfo GetIdProperty(Type type) => type.GetProperty("Id"); 19 | 20 | public static bool IsCountType(Type type) 21 | => 22 | type == typeof(int) || 23 | type == typeof(long); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/MR.Patterns.Repository.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Provides a base when implementing the repository pattern with EF6. 7 | Mohammad Rahhal 8 | netstandard2.1;net45 9 | MR.Patterns.Repository 10 | MR.Patterns.Repository 11 | ef;repository;pattern;extensions 12 | https://github.com/mrahhal/MR.Patterns.Repository 13 | https://github.com/mrahhal/MR.Patterns.Repository/blob/master/LICENSE.txt 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/PKGenerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | 4 | namespace MR.Patterns.Repository 5 | { 6 | public class PKGenerator 7 | { 8 | private long _current; 9 | 10 | public TValue Next() 11 | => (TValue)Convert.ChangeType(Interlocked.Increment(ref _current), typeof(TValue)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/QueryableExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Linq.Expressions; 5 | using System.Threading.Tasks; 6 | using QE = System.Data.Entity.QueryableExtensions; 7 | 8 | namespace MR.Patterns.Repository 9 | { 10 | public static class QueryableExtensions 11 | { 12 | public static IQueryable ById(this IQueryable @this, int id) 13 | where T : class, IEntity 14 | => @this.Where(e => e.Id == id); 15 | 16 | public static IQueryable ById(this IQueryable @this, long id) 17 | where T : class, IEntity 18 | => @this.Where(e => e.Id == id); 19 | 20 | public static IQueryable ById(this IQueryable @this, string id) 21 | where T : class, IEntity 22 | => @this.Where(e => e.Id == id); 23 | 24 | public static Task FindByIdAsync(this IQueryable @this, int id) 25 | where T : class, IEntity 26 | => @this.ById(id).FirstOrDefaultAsync(); 27 | 28 | public static Task FindByIdAsync(this IQueryable @this, long id) 29 | where T : class, IEntity 30 | => @this.ById(id).FirstOrDefaultAsync(); 31 | 32 | public static Task FindByIdAsync(this IQueryable @this, string id) 33 | where T : class, IEntity 34 | => @this.ById(id).FirstOrDefaultAsync(); 35 | 36 | //------------------------------------------------------------------------------ 37 | // Shims over System.Data.Entity.QueryableExtensions to ease out testing 38 | //------------------------------------------------------------------------------ 39 | 40 | private static bool IsInUnitTest => UnitTestDetector.IsInUnitTest; 41 | 42 | public static IQueryable Include(this IQueryable source, Expression> path) 43 | { 44 | if (IsInUnitTest) 45 | return source; 46 | return QE.Include(source, path); 47 | } 48 | 49 | public static IQueryable AsNoTracking(this IQueryable source) 50 | where T : class 51 | { 52 | if (IsInUnitTest) 53 | return source; 54 | return QE.AsNoTracking(source); 55 | } 56 | 57 | public static Task CountAsync(this IQueryable source) 58 | { 59 | if (IsInUnitTest) 60 | return Task.FromResult(source.Count()); 61 | return QE.CountAsync(source); 62 | } 63 | 64 | public static Task FirstAsync(this IQueryable source) 65 | { 66 | if (IsInUnitTest) 67 | return Task.FromResult(source.First()); 68 | return QE.FirstAsync(source); 69 | } 70 | 71 | public static Task FirstAsync(this IQueryable source, Expression> predicate) 72 | { 73 | if (IsInUnitTest) 74 | return Task.FromResult(source.First(predicate)); 75 | return QE.FirstAsync(source, predicate); 76 | } 77 | 78 | public static Task FirstOrDefaultAsync(this IQueryable source) 79 | { 80 | if (IsInUnitTest) 81 | return Task.FromResult(source.FirstOrDefault()); 82 | return QE.FirstOrDefaultAsync(source); 83 | } 84 | 85 | public static Task FirstOrDefaultAsync(this IQueryable source, Expression> predicate) 86 | { 87 | if (IsInUnitTest) 88 | return Task.FromResult(source.FirstOrDefault(predicate)); 89 | return QE.FirstOrDefaultAsync(source, predicate); 90 | } 91 | 92 | public static Task AnyAsync(this IQueryable source) 93 | { 94 | if (IsInUnitTest) 95 | return Task.FromResult(source.Any()); 96 | return QE.AnyAsync(source); 97 | } 98 | 99 | public static Task AnyAsync(this IQueryable source, Expression> predicate) 100 | { 101 | if (IsInUnitTest) 102 | return Task.FromResult(source.Any(predicate)); 103 | return QE.AnyAsync(source, predicate); 104 | } 105 | 106 | public static Task ToArrayAsync(this IQueryable source) 107 | { 108 | if (IsInUnitTest) 109 | return Task.FromResult(source.ToArray()); 110 | return QE.ToArrayAsync(source); 111 | } 112 | 113 | public static Task> ToListAsync(this IQueryable source) 114 | { 115 | if (IsInUnitTest) 116 | return Task.FromResult(source.ToList()); 117 | return QE.ToListAsync(source); 118 | } 119 | 120 | public static Task LoadAsync(this IQueryable source) 121 | { 122 | if (IsInUnitTest) 123 | return Task.FromResult(source); 124 | return QE.LoadAsync(source); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/MR.Patterns.Repository/UnitTestDetector.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | namespace MR.Patterns.Repository 6 | { 7 | public static class UnitTestDetector 8 | { 9 | private static readonly string[] TestAssemblies = new[] 10 | { 11 | "xunit.core", 12 | "nunit.framework" 13 | }; 14 | 15 | private static bool _set = IsTestAssemblyLoaded(); 16 | 17 | public static bool IsInUnitTest => _set; 18 | 19 | public static void SetIsInUnitTest(bool value = true) => _set = value; 20 | 21 | private static bool IsTestAssemblyLoaded() 22 | { 23 | foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) 24 | { 25 | if (TestAssemblies 26 | .Any(testAssemblyName => assembly.FullName.ToLowerInvariant().StartsWith(testAssemblyName))) 27 | { 28 | return true; 29 | } 30 | } 31 | return false; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/MR.Patterns.Repository.IntegrationTests/DatabaseTestHost.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data; 3 | using Dapper; 4 | 5 | namespace MR.Patterns.Repository 6 | { 7 | public abstract class DatabaseTestHost : IDisposable 8 | { 9 | private static bool _sqlObjectInstalled; 10 | 11 | public DatabaseTestHost() 12 | { 13 | UnitTestDetector.SetIsInUnitTest(false); 14 | InitializeDatabase(); 15 | } 16 | 17 | public virtual void Dispose() 18 | { 19 | DeleteAllData(); 20 | } 21 | 22 | protected TestDbContext CreateContext() => new TestDbContext(); 23 | 24 | protected TestRepository CreateRepository(TestDbContext context = null) 25 | => new TestRepository(context ?? CreateContext()); 26 | 27 | private void InitializeDatabase() 28 | { 29 | if (!_sqlObjectInstalled) 30 | { 31 | using (var context = CreateContext()) 32 | { 33 | context.Database.Delete(); 34 | context.Database.CreateIfNotExists(); 35 | _sqlObjectInstalled = true; 36 | } 37 | } 38 | } 39 | 40 | private void DeleteAllData() 41 | { 42 | using (var context = CreateContext()) 43 | { 44 | var commands = new[] 45 | { 46 | "DISABLE TRIGGER ALL ON ?", 47 | "ALTER TABLE ? NOCHECK CONSTRAINT ALL", 48 | "DELETE FROM ?", 49 | "ALTER TABLE ? CHECK CONSTRAINT ALL", 50 | "ENABLE TRIGGER ALL ON ?" 51 | }; 52 | foreach (var command in commands) 53 | { 54 | context.Database.Connection.Execute( 55 | "sp_MSforeachtable", 56 | new { command1 = command }, 57 | commandType: CommandType.StoredProcedure); 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /test/MR.Patterns.Repository.IntegrationTests/MR.Patterns.Repository.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | MR.Patterns.Repository 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | PreserveNewest 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /test/MR.Patterns.Repository.IntegrationTests/Models.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace MR.Patterns.Repository 4 | { 5 | public class Blog : IEntity 6 | { 7 | public int Id { get; set; } 8 | 9 | public string Name { get; set; } 10 | 11 | public List Posts = new List(); 12 | } 13 | 14 | public class Post : IEntity 15 | { 16 | public int Id { get; set; } 17 | 18 | public string Title { get; set; } 19 | 20 | public Blog Blog { get; set; } 21 | public int BlogId { get; set; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/MR.Patterns.Repository.IntegrationTests/ReentranceTest.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using FluentAssertions; 4 | using Xunit; 5 | 6 | namespace MR.Patterns.Repository 7 | { 8 | public class ReentranceTest : DatabaseTestHost 9 | { 10 | [Fact] 11 | public async Task RunInTransaction_IsReentrant() 12 | { 13 | using (var repo = CreateRepository()) 14 | { 15 | await repo.AddAsync(new Blog()); 16 | } 17 | 18 | using (var repo = CreateRepository()) 19 | { 20 | repo.RunInTransaction((c, t) => 21 | { 22 | repo.RunInTransaction((c2, t2) => 23 | { 24 | var count = repo.Blogs.Count(); 25 | count.Should().Be(1); 26 | }); 27 | }); 28 | } 29 | } 30 | 31 | [Fact] 32 | public async Task RunInTransactionAsync_IsReentrant() 33 | { 34 | using (var repo = CreateRepository()) 35 | { 36 | await repo.AddAsync(new Blog()); 37 | } 38 | 39 | using (var repo = CreateRepository()) 40 | { 41 | await repo.RunInTransactionAsync((c, t) => 42 | { 43 | return repo.RunInTransactionAsync(async (c2, t2) => 44 | { 45 | var count = await repo.Blogs.CountAsync(); 46 | count.Should().Be(1); 47 | }); 48 | }); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/MR.Patterns.Repository.IntegrationTests/TestDbContext.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Data.Entity; 3 | using System.Data.SqlClient; 4 | 5 | namespace MR.Patterns.Repository 6 | { 7 | public class TestDbContext : DbContext 8 | { 9 | public static string ConnectionString { get; set; } = ConnectionUtil.GetConnectionString(); 10 | 11 | public TestDbContext() : base(ConnectionString) 12 | { 13 | } 14 | 15 | public DbSet Blogs { get; set; } 16 | 17 | public DbSet Posts { get; set; } 18 | } 19 | 20 | public static class ConnectionUtil 21 | { 22 | private const string ConnectionStringTemplateVariable = "Jobs_SqlServer_ConnectionStringTemplate"; 23 | 24 | private const string DefaultDatabaseName = @"MR.Patterns.Repository.IntegrationTests"; 25 | 26 | private const string DefaultConnectionStringTemplate = "Server=(localdb)\\mssqllocaldb;Database={0};Trusted_Connection=True;"; 27 | 28 | public static string GetConnectionString() 29 | { 30 | return string.Format(GetConnectionStringTemplate(), DefaultDatabaseName); 31 | } 32 | 33 | private static string GetConnectionStringTemplate() 34 | { 35 | return 36 | Environment.GetEnvironmentVariable(ConnectionStringTemplateVariable) ?? 37 | DefaultConnectionStringTemplate; 38 | } 39 | 40 | public static SqlConnection CreateConnection(string connectionString = null) 41 | { 42 | connectionString = connectionString ?? GetConnectionString(); 43 | var connection = new SqlConnection(connectionString); 44 | connection.Open(); 45 | return connection; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/MR.Patterns.Repository.IntegrationTests/TestRepository.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | 3 | namespace MR.Patterns.Repository 4 | { 5 | public class TestRepository : RepositoryCore 6 | { 7 | public TestRepository(TestDbContext context) : base(context) 8 | { 9 | } 10 | 11 | public IQueryable Blogs => Context.Blogs; 12 | 13 | public IQueryable Posts => Context.Posts; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/MR.Patterns.Repository.IntegrationTests/xunit.runner.json: -------------------------------------------------------------------------------- 1 | { 2 | "parallelizeTestCollections": false 3 | } 4 | -------------------------------------------------------------------------------- /test/MR.Patterns.Repository.Tests/InMemoryRepositoryCoreTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Xunit; 6 | 7 | namespace MR.Patterns.Repository 8 | { 9 | public class InMemoryRepositoryCoreTest 10 | { 11 | private class Blog : IEntity 12 | { 13 | public int Id { get; set; } 14 | public string Name { get; set; } 15 | 16 | public List Posts { get; set; } 17 | } 18 | 19 | private class Post : IEntity 20 | { 21 | public long Id { get; set; } 22 | public string Title { get; set; } 23 | 24 | public Blog Blog { get; set; } 25 | public int BlogId { get; set; } 26 | } 27 | 28 | public class Some 29 | { 30 | public int Foo { get; set; } 31 | } 32 | 33 | private class InMemoryRepository : InMemoryRepositoryCore 34 | { 35 | public IQueryable Blogs => For(); 36 | 37 | public IQueryable Posts => For(); 38 | 39 | public IQueryable Somes => For(); 40 | } 41 | 42 | [Fact] 43 | public void Empty() 44 | { 45 | var repo = Create(); 46 | 47 | repo.Blogs.ToList().Should().BeEmpty(); 48 | } 49 | 50 | [Fact] 51 | public async Task Add() 52 | { 53 | var repo = Create(); 54 | var blog = new Blog(); 55 | 56 | repo.Add(blog); 57 | 58 | blog.Id.Should().NotBe(0); 59 | 60 | (await repo.Blogs.FindByIdAsync(blog.Id)).Should().NotBeNull(); 61 | } 62 | 63 | [Fact] 64 | public void Update() 65 | { 66 | var repo = Create(); 67 | var blog = new Blog(); 68 | 69 | repo.Add(blog); 70 | repo.Update(blog); 71 | 72 | blog.Id.Should().NotBe(0); 73 | } 74 | 75 | [Fact] 76 | public async Task Remove() 77 | { 78 | var repo = Create(); 79 | var blog = new Blog(); 80 | repo.Add(blog); 81 | 82 | repo.Remove(blog); 83 | 84 | (await repo.Blogs.FindByIdAsync(blog.Id)).Should().BeNull(); 85 | } 86 | 87 | [Fact] 88 | public async Task Add_Some() 89 | { 90 | var repo = Create(); 91 | var some = new Some() 92 | { 93 | Foo = 1 94 | }; 95 | repo.Add(some); 96 | (await repo.Somes.Where(s => s.Foo == 1).FirstOrDefaultAsync()).Should().NotBeNull(); 97 | } 98 | 99 | [Fact] 100 | public async Task Query() 101 | { 102 | var repo = Create(); 103 | var blog = new Blog() 104 | { 105 | Posts = new List { new Post(), new Post() } 106 | }; 107 | 108 | repo.Add(blog); 109 | 110 | (await repo.Query(blog, b => b.Posts).ToListAsync()).Should().HaveCount(2); 111 | } 112 | 113 | private InMemoryRepository Create() => new InMemoryRepository(); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /test/MR.Patterns.Repository.Tests/MR.Patterns.Repository.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | MR.Patterns.Repository 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/MR.Patterns.Repository.Tests/PKGeneratorTest.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Xunit; 3 | 4 | namespace MR.Patterns.Repository 5 | { 6 | public class PKGeneratorTest 7 | { 8 | [Fact] 9 | public void Int() 10 | { 11 | var generator = new PKGenerator(); 12 | 13 | generator.Next().Should().Be(1); 14 | generator.Next().Should().Be(2); 15 | generator.Next().Should().Be(3); 16 | } 17 | 18 | [Fact] 19 | public void Long() 20 | { 21 | var generator = new PKGenerator(); 22 | 23 | generator.Next().Should().Be(1L); 24 | generator.Next().Should().Be(2L); 25 | generator.Next().Should().Be(3L); 26 | } 27 | } 28 | } 29 | --------------------------------------------------------------------------------