├── .gitignore
├── .runsettings
├── EzDev.sln
├── GenerateCodeCoverageReport.ps1
├── azure-build.yml
├── readme.md
├── samples
└── EzDev.Examples.GenericRepository.SimpleImplementations
│ ├── EzDev.Examples.GenericRepository.SimpleImplementations.csproj
│ ├── Implementations.cs
│ ├── Models.cs
│ └── Samples.cs
├── src
└── EzDev.GenericRepository
│ ├── EntityRepository.cs
│ ├── EzDev.GenericRepository.csproj
│ ├── RepositoryEvents.cs
│ └── ServiceCollectionExtensions.cs
└── tests
└── EzDev.GenericRepositoryTests
├── EntityRepositoryShould.cs
├── EzDev.GenericRepositoryTests.csproj
├── Models
├── Employee.cs
└── EmployeeTestRepository.cs
└── ServiceExtensionsShould.cs
/.gitignore:
--------------------------------------------------------------------------------
1 | ### VisualStudioCode template
2 | .vscode/*
3 | !.vscode/settings.json
4 | !.vscode/tasks.json
5 | !.vscode/launch.json
6 | !.vscode/extensions.json
7 | *.code-workspace
8 |
9 | # Local History for Visual Studio Code
10 | .history/
11 |
12 | ### JetBrains template
13 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
14 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
15 |
16 | # User-specific stuff
17 | .idea
18 |
19 | # Gradle and Maven with auto-import
20 | # When using Gradle or Maven with auto-import, you should exclude module files,
21 | # since they will be recreated, and may cause churn. Uncomment if using
22 | # auto-import.
23 | # .idea/artifacts
24 | # .idea/compiler.xml
25 | # .idea/jarRepositories.xml
26 | # .idea/modules.xml
27 | # .idea/*.iml
28 | # .idea/modules
29 | # *.iml
30 | # *.ipr
31 |
32 | # CMake
33 | cmake-build-*/
34 |
35 | # Mongo Explorer plugin
36 | .idea/**/mongoSettings.xml
37 |
38 | # File-based project format
39 | *.iws
40 |
41 | # IntelliJ
42 | out/
43 |
44 | # mpeltonen/sbt-idea plugin
45 | .idea_modules/
46 |
47 | # JIRA plugin
48 | atlassian-ide-plugin.xml
49 |
50 | # Cursive Clojure plugin
51 | .idea/replstate.xml
52 |
53 | # Crashlytics plugin (for Android Studio and IntelliJ)
54 | com_crashlytics_export_strings.xml
55 | crashlytics.properties
56 | crashlytics-build.properties
57 | fabric.properties
58 |
59 | # Editor-based Rest Client
60 | .idea/httpRequests
61 |
62 | # Android studio 3.1+ serialized cache file
63 | .idea/caches/build_file_checksums.ser
64 |
65 | ### VisualStudio template
66 | ## Ignore Visual Studio temporary files, build results, and
67 | ## files generated by popular Visual Studio add-ons.
68 | ##
69 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
70 |
71 | # User-specific files
72 | *.rsuser
73 | *.suo
74 | *.user
75 | *.userosscache
76 | *.sln.docstates
77 |
78 | # User-specific files (MonoDevelop/Xamarin Studio)
79 | *.userprefs
80 |
81 | # Mono auto generated files
82 | mono_crash.*
83 |
84 | # Build results
85 | [Dd]ebug/
86 | [Dd]ebugPublic/
87 | [Rr]elease/
88 | [Rr]eleases/
89 | x64/
90 | x86/
91 | [Ww][Ii][Nn]32/
92 | [Aa][Rr][Mm]/
93 | [Aa][Rr][Mm]64/
94 | bld/
95 | [Bb]in/
96 | [Oo]bj/
97 | [Ll]og/
98 | [Ll]ogs/
99 |
100 | # Visual Studio 2015/2017 cache/options directory
101 | .vs/
102 | # Uncomment if you have tasks that create the project's static files in wwwroot
103 | #wwwroot/
104 |
105 | # Visual Studio 2017 auto generated files
106 | Generated\ Files/
107 |
108 | # MSTest test Results
109 | [Tt]est[Rr]esult*/
110 | [Bb]uild[Ll]og.*
111 |
112 | # NUnit
113 | *.VisualState.xml
114 | TestResult.xml
115 | nunit-*.xml
116 |
117 | # Build Results of an ATL Project
118 | [Dd]ebugPS/
119 | [Rr]eleasePS/
120 | dlldata.c
121 |
122 | # Benchmark Results
123 | BenchmarkDotNet.Artifacts/
124 |
125 | # .NET Core
126 | project.lock.json
127 | project.fragment.lock.json
128 | artifacts/
129 |
130 | # ASP.NET Scaffolding
131 | ScaffoldingReadMe.txt
132 |
133 | # StyleCop
134 | StyleCopReport.xml
135 |
136 | # Files built by Visual Studio
137 | *_i.c
138 | *_p.c
139 | *_h.h
140 | *.ilk
141 | *.meta
142 | *.obj
143 | *.iobj
144 | *.pch
145 | *.pdb
146 | *.ipdb
147 | *.pgc
148 | *.pgd
149 | *.rsp
150 | *.sbr
151 | *.tlb
152 | *.tli
153 | *.tlh
154 | *.tmp
155 | *.tmp_proj
156 | *_wpftmp.csproj
157 | *.log
158 | *.vspscc
159 | *.vssscc
160 | .builds
161 | *.pidb
162 | *.svclog
163 | *.scc
164 |
165 | # Chutzpah Test files
166 | _Chutzpah*
167 |
168 | # Visual C++ cache files
169 | ipch/
170 | *.aps
171 | *.ncb
172 | *.opendb
173 | *.opensdf
174 | *.sdf
175 | *.cachefile
176 | *.VC.db
177 | *.VC.VC.opendb
178 |
179 | # Visual Studio profiler
180 | *.psess
181 | *.vsp
182 | *.vspx
183 | *.sap
184 |
185 | # Visual Studio Trace Files
186 | *.e2e
187 |
188 | # TFS 2012 Local Workspace
189 | $tf/
190 |
191 | # Guidance Automation Toolkit
192 | *.gpState
193 |
194 | # ReSharper is a .NET coding add-in
195 | _ReSharper*/
196 | *.[Rr]e[Ss]harper
197 | *.DotSettings.user
198 |
199 | # TeamCity is a build add-in
200 | _TeamCity*
201 |
202 | # DotCover is a Code Coverage Tool
203 | *.dotCover
204 |
205 | # AxoCover is a Code Coverage Tool
206 | .axoCover/*
207 | !.axoCover/settings.json
208 |
209 | # Coverlet is a free, cross platform Code Coverage Tool
210 | coverage*.json
211 | coverage*.xml
212 | coverage*.info
213 |
214 | # Visual Studio code coverage results
215 | *.coverage
216 | *.coveragexml
217 |
218 | # NCrunch
219 | _NCrunch_*
220 | .*crunch*.local.xml
221 | nCrunchTemp_*
222 |
223 | # MightyMoose
224 | *.mm.*
225 | AutoTest.Net/
226 |
227 | # Web workbench (sass)
228 | .sass-cache/
229 |
230 | # Installshield output folder
231 | [Ee]xpress/
232 |
233 | # DocProject is a documentation generator add-in
234 | DocProject/buildhelp/
235 | DocProject/Help/*.HxT
236 | DocProject/Help/*.HxC
237 | DocProject/Help/*.hhc
238 | DocProject/Help/*.hhk
239 | DocProject/Help/*.hhp
240 | DocProject/Help/Html2
241 | DocProject/Help/html
242 |
243 | # Click-Once directory
244 | publish/
245 |
246 | # Publish Web Output
247 | *.[Pp]ublish.xml
248 | *.azurePubxml
249 | # Note: Comment the next line if you want to checkin your web deploy settings,
250 | # but database connection strings (with potential passwords) will be unencrypted
251 | *.pubxml
252 | *.publishproj
253 |
254 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
255 | # checkin your Azure Web App publish settings, but sensitive information contained
256 | # in these scripts will be unencrypted
257 | PublishScripts/
258 |
259 | # NuGet Packages
260 | *.nupkg
261 | # NuGet Symbol Packages
262 | *.snupkg
263 | # The packages folder can be ignored because of Package Restore
264 | **/[Pp]ackages/*
265 | # except build/, which is used as an MSBuild target.
266 | !**/[Pp]ackages/build/
267 | # Uncomment if necessary however generally it will be regenerated when needed
268 | #!**/[Pp]ackages/repositories.config
269 | # NuGet v3's project.json files produces more ignorable files
270 | *.nuget.props
271 | *.nuget.targets
272 |
273 | # Microsoft Azure Build Output
274 | csx/
275 | *.build.csdef
276 |
277 | # Microsoft Azure Emulator
278 | ecf/
279 | rcf/
280 |
281 | # Windows Store app package directories and files
282 | AppPackages/
283 | BundleArtifacts/
284 | Package.StoreAssociation.xml
285 | _pkginfo.txt
286 | *.appx
287 | *.appxbundle
288 | *.appxupload
289 |
290 | # Visual Studio cache files
291 | # files ending in .cache can be ignored
292 | *.[Cc]ache
293 | # but keep track of directories ending in .cache
294 | !?*.[Cc]ache/
295 |
296 | # Others
297 | ClientBin/
298 | ~$*
299 | *~
300 | *.dbmdl
301 | *.dbproj.schemaview
302 | *.jfm
303 | *.pfx
304 | *.publishsettings
305 | orleans.codegen.cs
306 |
307 | # Including strong name files can present a security risk
308 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
309 | #*.snk
310 |
311 | # Since there are multiple workflows, uncomment next line to ignore bower_components
312 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
313 | #bower_components/
314 |
315 | # RIA/Silverlight projects
316 | Generated_Code/
317 |
318 | # Backup & report files from converting an old project file
319 | # to a newer Visual Studio version. Backup files are not needed,
320 | # because we have git ;-)
321 | _UpgradeReport_Files/
322 | Backup*/
323 | UpgradeLog*.XML
324 | UpgradeLog*.htm
325 | ServiceFabricBackup/
326 | *.rptproj.bak
327 |
328 | # SQL Server files
329 | *.mdf
330 | *.ldf
331 | *.ndf
332 |
333 | # Business Intelligence projects
334 | *.rdl.data
335 | *.bim.layout
336 | *.bim_*.settings
337 | *.rptproj.rsuser
338 | *- [Bb]ackup.rdl
339 | *- [Bb]ackup ([0-9]).rdl
340 | *- [Bb]ackup ([0-9][0-9]).rdl
341 |
342 | # Microsoft Fakes
343 | FakesAssemblies/
344 |
345 | # GhostDoc plugin setting file
346 | *.GhostDoc.xml
347 |
348 | # Node.js Tools for Visual Studio
349 | .ntvs_analysis.dat
350 | node_modules/
351 |
352 | # Visual Studio 6 build log
353 | *.plg
354 |
355 | # Visual Studio 6 workspace options file
356 | *.opt
357 |
358 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
359 | *.vbw
360 |
361 | # Visual Studio LightSwitch build output
362 | **/*.HTMLClient/GeneratedArtifacts
363 | **/*.DesktopClient/GeneratedArtifacts
364 | **/*.DesktopClient/ModelManifest.xml
365 | **/*.Server/GeneratedArtifacts
366 | **/*.Server/ModelManifest.xml
367 | _Pvt_Extensions
368 |
369 | # Paket dependency manager
370 | .paket/paket.exe
371 | paket-files/
372 |
373 | # FAKE - F# Make
374 | .fake/
375 |
376 | # CodeRush personal settings
377 | .cr/personal
378 |
379 | # Python Tools for Visual Studio (PTVS)
380 | __pycache__/
381 | *.pyc
382 |
383 | # Cake - Uncomment if you are using it
384 | # tools/**
385 | # !tools/packages.config
386 |
387 | # Tabs Studio
388 | *.tss
389 |
390 | # Telerik's JustMock configuration file
391 | *.jmconfig
392 |
393 | # BizTalk build output
394 | *.btp.cs
395 | *.btm.cs
396 | *.odx.cs
397 | *.xsd.cs
398 |
399 | # OpenCover UI analysis results
400 | OpenCover/
401 |
402 | # Azure Stream Analytics local run output
403 | ASALocalRun/
404 |
405 | # MSBuild Binary and Structured Log
406 | *.binlog
407 |
408 | # NVidia Nsight GPU debugger configuration file
409 | *.nvuser
410 |
411 | # MFractors (Xamarin productivity tool) working folder
412 | .mfractor/
413 |
414 | # Local History for Visual Studio
415 | .localhistory/
416 |
417 | # BeatPulse healthcheck temp database
418 | healthchecksdb
419 |
420 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
421 | MigrationBackup/
422 |
423 | # Ionide (cross platform F# VS Code tools) working folder
424 | .ionide/
425 |
426 | # Fody - auto-generated XML schema
427 | FodyWeavers.xsd
428 |
429 |
--------------------------------------------------------------------------------
/.runsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ./CodeCoverage
5 |
6 |
7 |
8 |
9 |
10 | cobertura
11 | Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverage
12 | false
13 | true
14 | true
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/EzDev.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{5ED69FD0-AB5E-4A62-BEBA-FC959E831847}"
4 | EndProject
5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{35B28F0C-2B09-4DE5-A203-181F7877A158}"
6 | ProjectSection(SolutionItems) = preProject
7 | readme.md = readme.md
8 | azure-build.yml = azure-build.yml
9 | EndProjectSection
10 | EndProject
11 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EzDev.GenericRepository", "src\EzDev.GenericRepository\EzDev.GenericRepository.csproj", "{D8B79D69-7080-4C4F-8B46-EF251A9287CE}"
12 | EndProject
13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EzDev.Examples.GenericRepository.SimpleImplementations", "samples\EzDev.Examples.GenericRepository.SimpleImplementations\EzDev.Examples.GenericRepository.SimpleImplementations.csproj", "{BA860158-44AE-4071-AEC9-3DD4CB29DF1C}"
14 | EndProject
15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{BDA1E039-67F7-49DA-9DF6-63B5773BBA01}"
16 | EndProject
17 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EzDev.GenericRepositoryTests", "tests\EzDev.GenericRepositoryTests\EzDev.GenericRepositoryTests.csproj", "{BED53C44-CE7F-4711-A6F8-02C2462F401C}"
18 | EndProject
19 | Global
20 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
21 | Debug|Any CPU = Debug|Any CPU
22 | Release|Any CPU = Release|Any CPU
23 | EndGlobalSection
24 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
25 | {D8B79D69-7080-4C4F-8B46-EF251A9287CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
26 | {D8B79D69-7080-4C4F-8B46-EF251A9287CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
27 | {D8B79D69-7080-4C4F-8B46-EF251A9287CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
28 | {D8B79D69-7080-4C4F-8B46-EF251A9287CE}.Release|Any CPU.Build.0 = Release|Any CPU
29 | {BA860158-44AE-4071-AEC9-3DD4CB29DF1C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {BA860158-44AE-4071-AEC9-3DD4CB29DF1C}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {BA860158-44AE-4071-AEC9-3DD4CB29DF1C}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {BA860158-44AE-4071-AEC9-3DD4CB29DF1C}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {BED53C44-CE7F-4711-A6F8-02C2462F401C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {BED53C44-CE7F-4711-A6F8-02C2462F401C}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {BED53C44-CE7F-4711-A6F8-02C2462F401C}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {BED53C44-CE7F-4711-A6F8-02C2462F401C}.Release|Any CPU.Build.0 = Release|Any CPU
37 | EndGlobalSection
38 | GlobalSection(NestedProjects) = preSolution
39 | {BA860158-44AE-4071-AEC9-3DD4CB29DF1C} = {5ED69FD0-AB5E-4A62-BEBA-FC959E831847}
40 | {BED53C44-CE7F-4711-A6F8-02C2462F401C} = {BDA1E039-67F7-49DA-9DF6-63B5773BBA01}
41 | EndGlobalSection
42 | EndGlobal
43 |
--------------------------------------------------------------------------------
/GenerateCodeCoverageReport.ps1:
--------------------------------------------------------------------------------
1 |
2 | <#
3 | .SYNOPSIS
4 | Run the reportgenerator tool to combine and generate a code coverage report
5 | #>
6 |
7 | Set-Variable -Name ReportPath -Value ./CodeCoverage/Report
8 |
9 | if(Test-Path $ReportPath) { Remove-Item $ReportPath -Recurse }
10 | Write-Output "Executing from path $PWD"
11 | reportgenerator -reports:./**/*cobertura.xml -targetdir:$ReportPath -reporttypes:HtmlInline_AzurePipelines
--------------------------------------------------------------------------------
/azure-build.yml:
--------------------------------------------------------------------------------
1 | trigger:
2 | - master
3 |
4 | pool:
5 | vmImage: windows-latest
6 |
7 | variables:
8 | buildConfiguration: 'Release'
9 |
10 | steps:
11 | - task: UseDotNet@2
12 | displayName: "Use .NET Core SDK"
13 | inputs:
14 | packageType: sdk
15 | version: '6.0.x'
16 | - task: DotNetCoreCLI@2
17 | displayName: 'Install report generator tool'
18 | inputs:
19 | command: custom
20 | custom: tool
21 | arguments: 'install dotnet-reportgenerator-globaltool -g'
22 | - task: DotNetCoreCLI@2
23 | displayName: 'Restore packages'
24 | inputs:
25 | command: restore
26 | - task: DotNetCoreCLI@2
27 | displayName: 'Build solution'
28 | inputs:
29 | command: build
30 | projects: '**/*.csproj'
31 | arguments: '-c $(buildConfiguration) --no-restore'
32 | - task: DotNetCoreCLI@2
33 | displayName: 'Run tests'
34 | inputs:
35 | command: test
36 | projects: 'tests/**/*.csproj'
37 | arguments: '-c $(buildConfiguration) --no-build --settings ".runsettings"'
38 | - task: PublishCodeCoverageResults@1
39 | displayName: 'Publish code coverage results'
40 | inputs:
41 | codeCoverageTool: Cobertura
42 | reportDirectory: $(Agent.TempDirectory)/Result
43 | summaryFileLocation: $(Agent.TempDirectory)/**/*cobertura.xml
44 | - task: DotNetCoreCLI@2
45 | displayName: "Pack project"
46 | inputs:
47 | command: 'pack'
48 | packagesToPack: 'src/EzDev.GenericRepository/*.csproj'
49 | includesymbols: true
50 | includesource: true
51 | versioningScheme: 'off'
52 | outputDir: "$(Build.ArtifactStagingDirectory)/app"
53 | - task: PublishCodeCoverageResults@1
54 | displayName: 'Publish code coverage results'
55 | inputs:
56 | codeCoverageTool: Cobertura
57 | reportDirectory: $(Agent.TempDirectory)/Result
58 | summaryFileLocation: $(Agent.TempDirectory)/**/*cobertura.xml
59 | - task: PublishBuildArtifacts@1
60 | displayName: 'Publish artifact to drop'
61 | inputs:
62 | artifactName: 'nuget'
63 | PathtoPublish: $(Build.ArtifactStagingDirectory)/app
--------------------------------------------------------------------------------
/readme.md:
--------------------------------------------------------------------------------
1 | [](https://nmillard.visualstudio.com/EzDev/_build/latest?definitionId=4&branchName=master)
2 | 
3 | [](https://nuget.org/packages/newtonsoft.json)
4 | [](https://medium.com/@nmillard)
5 |
6 | # Easy Generic Repository
7 |
8 | EzDev.GenericRepository is a very simplistic, lightweight generic repository based on EntityFramework Core, that doesn't
9 | lock you into a certain way of working. You're provided a single base class with simple CRUD-based operations, that you
10 | may override if you have other requirements.
11 |
12 | ## Installation
13 |
14 | Install with [NuGet](https://www.nuget.org/packages/EzDev.GenericRepository)
15 |
16 | or use .NET Core CLI
17 | `dotnet add package EzDev.GenericRepository`.
18 |
19 | Consider using `--prelease` for preview versions.
20 |
21 | ## How do I get started?
22 |
23 | Create a class that inherits from `EntityRepository` and implement its constructor.
24 | In its simplest form, you can have a repository such as below.
25 |
26 | `````c#
27 | public class SimpleEmployeeRepository : EntityRepository {
28 | public SimpleEmployeeRepository(DbContext context) : base(context) { }
29 | }
30 | `````
31 |
32 | That's honestly it.
33 |
34 | The `SimpleEmployeeRepository` now has default implementations for getting, adding, updating, and deleting `Employee`
35 | entities.
36 |
37 | ### More advanced options
38 |
39 | Say you have a `Company` type acting as an aggregate root with a list of employees, and you want to retrieve all
40 | employees whenever you query a company.
41 |
42 | In this case, you may want to override the default `Entities` property on the `EntityRepository`, as demonstrated below.
43 |
44 | ````c#
45 | public class CompanyRepository : EntityRepository {
46 | public CompanyRepository(DbContext context) : base(context) {
47 | Entities = context.Set().Include(c => c.Employees).AsNoTracking();
48 | }
49 | }
50 | ````
51 |
52 | ### Extension and listening points
53 |
54 | Take advantage of events to plug in your own code without having to override methods. This is great for implementing
55 | cross-cutting concerns such as logging.
56 |
57 | You can listen to repository events in two ways: implement the methods directly in the repository, or, register them
58 | with the dependency injection framework.
59 |
60 | ````c#
61 | public class EmployeeRepositoryWithEvents : EntityRepository {
62 | public EmployeeRepositoryWithEvents(DbContext context, ILogger logger) : base(context) {
63 | Events = new RepositoryEvents {
64 | OnBeforeSaving = async employee => logger.LogInformation("Before saving employee {Id}", employee.Id),
65 | OnSaved = async employee => logger.LogInformation("Saved employee {Id}", employee.Id),
66 | OnSavingFailed = async (employee, exception) => logger.LogError("Saving employee {Id} failed with message {Message}", employee.Id, exception.Message)
67 | };
68 | }
69 | }
70 | ````
71 |
72 | If you don't want to pollute your repository with logging statements, then you can register the `RepositoryEvents`
73 | with your dependency container framework, such as below.
74 |
75 | ````c#
76 | public class EmployeeRepositoryWithEvents : EntityRepository {
77 | public EmployeeRepositoryWithEvents(DbContext context, RepositoryEvents events) :
78 | base(context, events) { }
79 | }
80 |
81 | // In Startup.cs (or elsewhere)
82 | services.AddRepository()
83 | .WithEvents(_ => {
84 | OnBeforeSaving = async employee => logger.LogInformation("Before saving employee {Id}", employee.Id),
85 | OnSaved = async employee => logger.LogInformation("Saved employee {Id}", employee.Id),
86 | OnSavingFailed = async (employee, exception) => logger.LogError("Saving employee {Id} failed with message {Message}", employee.Id, exception.Message)
87 | });
88 | ````
--------------------------------------------------------------------------------
/samples/EzDev.Examples.GenericRepository.SimpleImplementations/EzDev.Examples.GenericRepository.SimpleImplementations.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 | runtime; build; native; contentfiles; analyzers; buildtransitive
15 | all
16 |
17 |
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 | all
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/samples/EzDev.Examples.GenericRepository.SimpleImplementations/Implementations.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using EzDev.GenericRepository;
3 | using Microsoft.EntityFrameworkCore;
4 | using Microsoft.Extensions.Logging;
5 |
6 | namespace EzDev.Examples.GenericRepository.SimpleImplementations;
7 |
8 | public class SimpleEmployeeRepository : EntityRepository {
9 | public SimpleEmployeeRepository(DbContext context) : base(context) { }
10 | }
11 |
12 | public class EmployeeRepositoryWithEvents : EntityRepository {
13 | public EmployeeRepositoryWithEvents(DbContext context, ILogger logger) : base(context) {
14 | Events = new RepositoryEvents {
15 | OnBeforeSaving = async employee => logger.LogInformation("Before saving employee {Id}", employee.Id),
16 | OnSaved = async employee => logger.LogInformation("Saved employee {Id}", employee.Id),
17 | OnSavingFailed = async (employee, exception) => logger.LogError("Saving employee {Id} failed with message {Message}", employee.Id, exception.Message)
18 | };
19 | }
20 |
21 | public EmployeeRepositoryWithEvents(DbContext context, RepositoryEvents events) :
22 | base(context, events) { }
23 | }
24 |
25 | public class CompanyRepository : EntityRepository {
26 | public CompanyRepository(DbContext context) : base(context) { }
27 |
28 | public CompanyRepository(DbContext context, RepositoryEvents events) : base(context, events) {
29 | Entities = context.Set().Include(c => c.Employees).AsNoTracking();
30 | }
31 | }
32 |
33 | // Demo in memory db context
34 | public class InMemoryDbContext : DbContext {
35 | public DbSet Companies { get; set; }
36 | public DbSet? Employees { get; set; }
37 |
38 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
39 | optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString());
40 | }
41 | }
--------------------------------------------------------------------------------
/samples/EzDev.Examples.GenericRepository.SimpleImplementations/Models.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 |
4 | namespace EzDev.Examples.GenericRepository.SimpleImplementations;
5 |
6 | /*
7 | * Simple models used with the demo repository classes.
8 | * Don't implement your models in this way, with public setters, etc.
9 | */
10 |
11 | public class Company {
12 | public Guid Id { get; set; }
13 | public List Employees { get; set; }
14 | }
15 |
16 | public record Employee(Guid Id, string Name, DateOnly Birthday);
17 |
--------------------------------------------------------------------------------
/samples/EzDev.Examples.GenericRepository.SimpleImplementations/Samples.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Threading.Tasks;
3 | using EzDev.GenericRepository;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Microsoft.Extensions.Logging;
7 | using Xunit;
8 | using Xunit.Abstractions;
9 |
10 | namespace EzDev.Examples.GenericRepository.SimpleImplementations;
11 |
12 |
13 | public class Samples {
14 | private readonly ITestOutputHelper outputHelper;
15 |
16 | public Samples(ITestOutputHelper outputHelper) => this.outputHelper = outputHelper;
17 |
18 | [Fact]
19 | public async Task SimpleInstantiation() {
20 | var repository = new SimpleEmployeeRepository(new InMemoryDbContext());
21 |
22 | var id = Guid.NewGuid();
23 | await repository.AddAsync(new Employee(id, "Faxe Kondi", new DateOnly(1991, 11, 26)));
24 | Employee? employee = await repository.GetAsync(emp => emp.Id == id);
25 |
26 | Assert.NotNull(employee);
27 | }
28 |
29 | [Fact]
30 | public async Task InstantiateWithEventsFromDependencyContainer() {
31 | // Just like how you'd register services in an aspnetcore app.
32 | ServiceProvider provider = new ServiceCollection()
33 | .AddScoped()
34 | .AddScoped>(provider => {
35 | var logger = provider.GetRequiredService>>();
36 | return new RepositoryEvents {
37 | OnBeforeSaving = async employee => logger.LogInformation("Before saving employee {Id}", employee.Id),
38 | OnSaved = async employee => logger.LogInformation("Saved employee {Id}", employee.Id),
39 | OnSavingFailed = async (employee, exception) => logger.LogError("Saving employee {Id} failed with message {Message}", employee.Id, exception.Message)
40 | };
41 | })
42 | .AddScoped()
43 | .BuildServiceProvider();
44 |
45 | var repository = provider.GetRequiredService();
46 |
47 | var id = Guid.NewGuid();
48 | await repository.AddAsync(new Employee(id, "Faxe Kondi", new DateOnly(1991, 11, 26)));
49 | Employee? employee = await repository.GetAsync(emp => emp.Id == id);
50 |
51 | Assert.NotNull(employee);
52 | }
53 | }
--------------------------------------------------------------------------------
/src/EzDev.GenericRepository/EntityRepository.cs:
--------------------------------------------------------------------------------
1 | using System.Linq.Expressions;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace EzDev.GenericRepository;
5 |
6 | ///
7 | /// A generic repository used with aggregates, based on EntityFramework Core .
8 | ///
9 | /// The aggregate type this repository operates on.
10 | public abstract class EntityRepository where TEntity : class {
11 | protected readonly DbContext Context;
12 |
13 | protected RepositoryEvents Events { get; init; } = new();
14 |
15 | ///
16 | /// Provides the base query. Override in the constructor if more complex query is required, such as including
17 | /// nested objects, filtering, etc.
18 | ///
19 | protected IQueryable Entities { get; init; }
20 |
21 | ///
22 | /// This constructor defaults to having the property query the provided 's set of
23 | /// with the AsNoTracking enabled.
24 | /// Override the if the entity require special includes or filters.
25 | ///
26 | /// The context this repository performs queries against.
27 | protected EntityRepository(DbContext context) {
28 | Context = context;
29 | Entities = context.Set().AsNoTracking().AsQueryable();
30 | }
31 |
32 | protected EntityRepository(DbContext context, RepositoryEvents events) : this(context)
33 | => Events = events;
34 |
35 | public virtual async Task GetAsync(Expression> predicate, CancellationToken cancellationToken = default)
36 | => await Entities.SingleOrDefaultAsync(predicate, cancellationToken);
37 |
38 | public virtual async Task> GetAllAsync(Expression> predicate, CancellationToken cancellationToken = default)
39 | => await Entities.Where(predicate).ToListAsync(cancellationToken);
40 |
41 | public virtual async Task> GetAllAsync(CancellationToken cancellationToken = default) => await Entities.ToListAsync(cancellationToken);
42 |
43 |
44 | public virtual async Task AddAsync(TEntity entity, CancellationToken cancellationToken = default) {
45 | await Context.AddAsync(entity, cancellationToken);
46 | bool result = await SaveAsync(entity, cancellationToken);
47 |
48 | return result;
49 | }
50 |
51 | public virtual async Task UpdateAsync(TEntity entity) {
52 | Context.Update(entity);
53 | bool result = await SaveAsync(entity);
54 |
55 | return result;
56 | }
57 |
58 | ///
59 | /// The saving operation performed when adding or updating an entity.
60 | ///
61 | /// true if save succeeded, false if an exception was thrown. Use to extract exception information.
62 | protected virtual async Task SaveAsync(TEntity entity, CancellationToken cancellationToken = default) {
63 | await Events.OnBeforeSaving(entity);
64 |
65 | try {
66 | await Context.SaveChangesAsync(cancellationToken);
67 | await Events.OnSaved(entity);
68 | return true;
69 | } catch (DbUpdateException due) {
70 | await Events.OnSavingFailed(entity, due);
71 | return false;
72 | }
73 | }
74 | }
--------------------------------------------------------------------------------
/src/EzDev.GenericRepository/EzDev.GenericRepository.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | EzDev.GenericRepository
5 |
6 |
7 |
8 | Nicklas Millard
9 |
10 | Simplifying generic repository logic.
11 | Simple, generic repository that only provides the bare minimum of what you'd expect.
12 |
13 | $(ProjectName)
14 | repository;generic;generic repository;data access;datalayer
15 | $(ProjectName)
16 | Apache-2.0
17 | https://github.com/NMillard/EzDev.GenericRepository
18 | https://github.com/NMillard/EzDev.GenericRepository
19 | readme.md
20 |
21 | true
22 | Copyright Nicklas Millard
23 | true
24 | snupkg
25 | embedded
26 |
27 |
28 |
29 | net6.0
30 | enable
31 | enable
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/EzDev.GenericRepository/RepositoryEvents.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 |
3 | namespace EzDev.GenericRepository;
4 |
5 | ///
6 | /// Provides extension and listening points. Default implementation of each delegate is to do nothing.
7 | ///
8 | /// The aggregate type which the repository operates on.
9 | public class RepositoryEvents {
10 | ///
11 | /// Triggered when performing the save operation, both when adding and updating an entity.
12 | ///
13 | public Func OnBeforeSaving { get; set; } = _ => Task.CompletedTask;
14 | ///
15 | /// Triggered when the add and update save operation succeeded.
16 | ///
17 | public Func OnSaved { get; set; } = _ => Task.CompletedTask;
18 | ///
19 | /// Triggered when the add and update save operation failed.
20 | ///
21 | public Func OnSavingFailed { get; set; } = (_,_) => Task.CompletedTask;
22 | }
--------------------------------------------------------------------------------
/src/EzDev.GenericRepository/ServiceCollectionExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 |
3 | namespace EzDev.GenericRepository;
4 |
5 | ///
6 | /// Extensions to easily register implementations of with the dependency container.
7 | ///
8 | public static class ServiceCollectionExtensions {
9 | ///
10 | /// Add a repository for the provided entity as a scoped service.
11 | ///
12 | /// The entity type the repository operates on.
13 | ///
14 | ///
15 | public static IServiceCollection AddRepository(this IServiceCollection services)
16 | where TEntity : class
17 | where TRepository : EntityRepository {
18 | services.AddScoped, TRepository>();
19 |
20 | return services;
21 | }
22 |
23 | ///
24 | /// Add repository events for the provided entity as a scoped service.
25 | ///
26 | ///
27 | ///
28 | ///
29 | ///
30 | public static IServiceCollection WithEvents(this IServiceCollection services, Action> configure) {
31 | var repositoryEvents = new RepositoryEvents();
32 | configure(repositoryEvents);
33 |
34 | services.AddScoped(_ => repositoryEvents);
35 |
36 | return services;
37 | }
38 | }
--------------------------------------------------------------------------------
/tests/EzDev.GenericRepositoryTests/EntityRepositoryShould.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Generic;
2 | using System.Linq;
3 | using System.Threading.Tasks;
4 | using EzDev.GenericRepository;
5 | using EzDev.GenericRepositoryTests.Models;
6 | using Xunit;
7 |
8 | namespace EzDev.GenericRepositoryTests;
9 |
10 | public class EntityRepositoryShould {
11 |
12 | [Fact]
13 | public async Task QueryAllEntries() {
14 | // Arrange
15 | var returnData = new List { new(), new(), new() };
16 | var testDbContext = new TestDbContext();
17 | await testDbContext.AddRangeAsync(returnData);
18 | await testDbContext.SaveChangesAsync();
19 |
20 | var sut = new EmployeeTestRepository(testDbContext);
21 |
22 | // Act
23 | IEnumerable result = await sut.GetAllAsync();
24 |
25 | Assert.Equal(returnData.Count, result.Count());
26 | }
27 |
28 | [Fact]
29 | public async Task QueryForSingleEntry() {
30 | // Arrange
31 | var entryToFind = new Employee();
32 |
33 | var returnData = new List { entryToFind, new(), new() };
34 | var testDbContext = new TestDbContext();
35 | await testDbContext.AddRangeAsync(returnData);
36 | await testDbContext.SaveChangesAsync();
37 |
38 | var sut = new EmployeeTestRepository(testDbContext);
39 |
40 | Employee? result = await sut.GetAsync(e => e.Id == entryToFind.Id);
41 |
42 | Assert.NotNull(result);
43 | Assert.Equal(entryToFind.Id, result!.Id);
44 | }
45 |
46 | [Fact]
47 | public async Task CallEvents() {
48 | // Arrange
49 | var onBeforeSaveCalledTimes = 0;
50 | var onSavedCalledTimes = 0;
51 | var repositoryEvents = new RepositoryEvents {
52 | OnBeforeSaving = _ => Task.FromResult(onBeforeSaveCalledTimes++),
53 | OnSaved = _ => Task.FromResult(onSavedCalledTimes++)
54 | };
55 | var sut = new EmployeeTestRepository(new TestDbContext(), repositoryEvents);
56 |
57 | var entry = new Employee();
58 | // Act
59 | await sut.AddAsync(entry);
60 | await sut.UpdateAsync(entry);
61 |
62 | Assert.Equal(2, onBeforeSaveCalledTimes);
63 | Assert.Equal(2, onSavedCalledTimes);
64 | }
65 | }
--------------------------------------------------------------------------------
/tests/EzDev.GenericRepositoryTests/EzDev.GenericRepositoryTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 |
7 | false
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 | all
17 |
18 |
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 | all
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/tests/EzDev.GenericRepositoryTests/Models/Employee.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace EzDev.GenericRepositoryTests.Models;
4 |
5 | public class Employee {
6 | public Guid Id { get; set; }
7 | }
--------------------------------------------------------------------------------
/tests/EzDev.GenericRepositoryTests/Models/EmployeeTestRepository.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using EzDev.GenericRepository;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace EzDev.GenericRepositoryTests.Models;
6 |
7 | public class EmployeeTestRepository : EntityRepository {
8 | public EmployeeTestRepository(DbContext context) : base(context) { }
9 | public EmployeeTestRepository(DbContext context, RepositoryEvents events) : base(context, events) { }
10 | }
11 |
12 | public class TestDbContext : DbContext {
13 | public DbSet Employees { get; set; }
14 |
15 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
16 | optionsBuilder.UseInMemoryDatabase(Guid.NewGuid().ToString());
17 | }
18 | }
--------------------------------------------------------------------------------
/tests/EzDev.GenericRepositoryTests/ServiceExtensionsShould.cs:
--------------------------------------------------------------------------------
1 | using System.Threading.Tasks;
2 | using EzDev.GenericRepository;
3 | using EzDev.GenericRepositoryTests.Models;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.Extensions.DependencyInjection;
6 | using Xunit;
7 |
8 | namespace EzDev.GenericRepositoryTests;
9 |
10 | public class ServiceExtensionsShould {
11 | private readonly IServiceCollection services;
12 |
13 | public ServiceExtensionsShould() {
14 | services = new ServiceCollection();
15 | services.AddDbContext();
16 | }
17 |
18 | [Fact]
19 | public void CanAddRepositoryBaseToServiceCollection() {
20 | // Act
21 | services.AddRepository();
22 |
23 | // Assert
24 | var repository = services.BuildServiceProvider().GetRequiredService>();
25 | Assert.NotNull(repository);
26 | }
27 |
28 | [Fact]
29 | public void CanAddRepositoryEvents() {
30 | // Arrange
31 | services.AddRepository()
32 | // Act
33 | .WithEvents(_ => { });
34 |
35 | // Assert
36 | var sut = services.BuildServiceProvider().GetService>();
37 | Assert.NotNull(sut);
38 | }
39 | }
--------------------------------------------------------------------------------