├── .gitignore ├── LICENSE ├── README.md ├── Wissance.WebApiToolkit ├── Wissance.WebApiToolkit.Data │ ├── Entity │ │ ├── IModelIdentifiable.cs │ │ ├── IModelSoftRemovable.cs │ │ └── IModelTrackable.cs │ ├── Tools │ │ └── DbContextHelper.cs │ └── Wissance.WebApiToolkit.Data.csproj ├── Wissance.WebApiToolkit.Dto │ ├── OperationResultDto.cs │ ├── PagedDataDto.cs │ └── Wissance.WebApiToolkit.Dto.csproj ├── Wissance.WebApiToolkit.TestApp │ ├── Controllers │ │ ├── CodeController.cs │ │ └── OrganizationController.cs │ ├── Data │ │ ├── Entity │ │ │ ├── CodeEntity.cs │ │ │ ├── Mapping │ │ │ │ ├── CodeMapper.cs │ │ │ │ ├── OrganizationMapper.cs │ │ │ │ └── UserMapper.cs │ │ │ ├── OrganizationEntity.cs │ │ │ ├── ProfileEntity.cs │ │ │ ├── RoleEntity.cs │ │ │ └── UserEntity.cs │ │ └── ModelContext.cs │ ├── DataInitializer.cs │ ├── Dto │ │ ├── CodeDto.cs │ │ └── OrganizationDto.cs │ ├── Factories │ │ ├── CodeFactory.cs │ │ └── OrganizationFactory.cs │ ├── Managers │ │ ├── CodeManager.cs │ │ └── OrganizationManager.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── WebServices │ │ └── Grpc │ │ │ ├── CodeGrpcService.cs │ │ │ ├── Generated │ │ │ ├── Code.cs │ │ │ └── CodeGrpc.cs │ │ │ ├── Helpers │ │ │ └── GrpcErrorCodeHelper.cs │ │ │ └── Proto │ │ │ ├── Code.proto │ │ │ └── Common │ │ │ └── Common.proto │ ├── Wissance.WebApiToolkit.TestApp.csproj │ ├── appsettings.Development.json │ └── appsettings.json ├── Wissance.WebApiToolkit.Tests │ ├── Controllers │ │ ├── TestBasicCrudController.cs │ │ └── TestBasicReadController.cs │ ├── Services │ │ └── TestResourceBasedDataManageableReadOnlyService.cs │ ├── Utils │ │ ├── Checkers │ │ │ ├── CodeChecker.cs │ │ │ └── OrganizationChecker.cs │ │ └── WebApiTestBasedOnTestApp.cs │ └── Wissance.WebApiToolkit.Tests.csproj ├── Wissance.WebApiToolkit.sln └── Wissance.WebApiToolkit │ ├── Controllers │ ├── BasicBulkCrudController.cs │ ├── BasicCrudController.cs │ ├── BasicPagedDataController.cs │ └── BasicReadController.cs │ ├── Data │ ├── EmptyAdditionalFilters.cs │ ├── IReadFilterable.cs │ └── SortOption.cs │ ├── Globals │ └── MessageCatalog.cs │ ├── Managers │ ├── EfModelManager.cs │ ├── EfSoftRemovableModelManager.cs │ ├── Helpers │ │ └── ResponseMessageBuilder.cs │ └── IModelManager.cs │ ├── Services │ ├── IResourceBasedCrudService.cs │ ├── IResourceBasedReadOnlyService.cs │ ├── ResourceBasedDataManageableCrudService.cs │ └── ResourceBasedDataManageableReadOnlyService.cs │ ├── Utils │ ├── Extractors │ │ └── ValueExtractor.cs │ └── PagingUtils.cs │ └── Wissance.WebApiToolkit.csproj ├── _config.yml └── img ├── bulk_performance.png ├── cover.jpg ├── cover.png └── v1.jpg /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | Wissance.WebApiToolkit/.idea/ 353 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Wissance.WebApiToolkit 2 | 3 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/wissance/WebApiToolkit?style=plastic) 4 | ![GitHub issues](https://img.shields.io/github/issues/wissance/WebApiToolkit?style=plastic) 5 | ![GitHub Release Date](https://img.shields.io/github/release-date/wissance/WebApiToolkit) 6 | ![GitHub release (latest by date)](https://img.shields.io/github/downloads/wissance/WebApiToolkit/v2.0.0/total?style=plastic) 7 | 8 | #### This lib helps to build `REST API` with `C#` and `AspNet` easier than writing it from scratch over and over in different projects. It helps to build consistent API (with same `REST` routes scheme) with minimal amount of code: minimal REST controller contains 10 lines of code. 9 | 10 | ![WebApiToolkit helps to build application easily](/img/cover.png) 11 | 12 | * [1. Key Features](#1-key-features) 13 | * [2. API Contract](#2-api-contract) 14 | * [3. Requirements](#3-requirements) 15 | * [4. Toolkit usage algorithm with EntityFramework](#4-toolkit-usage-algorithm-with-entityframework) 16 | + [4.1 REST Services](#41-rest-services) 17 | + [4.2 GRPC Services](#42-grpc-services) 18 | * [5. Nuget package](#5-nuget-package) 19 | * [6. Examples](#6-examples) 20 | + [6.1 REST Service example](#61-rest-service-example) 21 | + [6.2 GRPC Service example](#62-grpc-service-example) 22 | * [7. Extending API](#7-extending-api) 23 | + [7.1 Add new methods to existing controller](#71-add-new-methods-to-existing-controller) 24 | + [7.2 Add security to protect you API](#72-add-security-to-protect-you-api) 25 | * [8. Additional materials](#8-additional-materials) 26 | * [9. Contributors](#9-contributors) 27 | 28 | ### 1. Key Features 29 | * `REST API Controller` with **full `CRUD`** contains ***only 20 lines*** of code (~ 10 are imports) 30 | - `GET` methods have ***built-in paging*** support; 31 | - `GET` methods have ***built-in sorting and filter*** by query parameters; 32 | * support ***BULK operations*** with objects (Bulk `Create`, `Update` and `Delete`) on a Controller && interface level 33 | * support to work with ***any persistent storage*** (`IModelManager` interface); Good built-in EntityFramework support (see `EfModelManager` class). See [WeatherControl App](https://github.com/Wissance/WeatherControl) which has 2 WEB API projects: 34 | - `Wissance.WeatherControl.WebApi` uses `EntityFramework`; 35 | - `Wissance.WeatherControl.WebApi.V2` uses `EdgeDb` 36 | * support writing `GRPC` services with examples (see `Wissance.WebApiToolkit.TestApp` and `Wissance.WebApiToolkit.Tests`) 37 | 38 | Key concepts: 39 | 1. `Controller` is a class that handles `HTTP-requests` to `REST Resource`. 40 | 2. `REST Resource` is equal to `Entity class / Database Table` 41 | 3. Every operation on `REST Resource` produce `JSON` with `DTO` as output. We ASSUME to use only one `DTO` class with all `REST` methods. 42 | 43 | ### 2. API Contract 44 | * `DTO` classes: 45 | - `OperationResultDto` represents result of operation that changes Data in db; 46 | - `PagedDataDto` represents portion (page) of same objects (any type); 47 | * `Controllers` classes - abstract classes 48 | - basic read controller (`BasicReadController`) contains 2 methods: 49 | - `GET /api/[controller]/?[page={page}&size={size}&sort={sort}&order={order}]` to get `PagedDataDto` 50 | now we also have possibility to send **ANY number of query params**, you just have to pass filter func to `EfModelManager` or do it in your own way like in [WeatherControl example with edgedb](https://github.com/Wissance/WeatherControl/blob/master/WeatherControl/Wissance.WeatherControl.WebApi.V2/Helpers/EqlResolver.cs). We also pass sort (column name) && order (`asc` or `desc`) to manager classes, 51 | `EfModelManager` allows to sort **by any column**. 52 | Unfortunately here we have a ***ONE disadvantage*** - **we should override `Swagger` info to show query parameters usage!!!** Starting from `1.6.0` it is possible to see all parameters in `Swagger` and use them. 53 | - `GET /api/[controller]/{id}` to get one object by `id` 54 | - full `CRUD` controller (`BasicCrudController`) = basic read controller (`BasicReadController`) + `Create`, `Update` and `Delete` operations : 55 | - `POST /api/[controller]` - for new object creation 56 | - `PUT /api/[controller]/{id}` - for edit object by id 57 | - `DELETE /api/[controller]/{id}` - for delete object by id 58 | - full `CRUD` with **Bulk** operations (operations over multiple objects at once), Base class - `BasicBulkCrudController` = basic read controller (`BasicReadController`) + `BulkCreate`, `BulkUpdate` and `BulkDelete` operations: 59 | - `POST /api/bulk/[controller]` - for new objects creation 60 | - `PUT /api/bulk/[controller]` - for edit objects passing in a request body 61 | - `DELETE /api/bulk/[controller]/{idList}` - for delete multiple objects by id. 62 | 63 | Controllers classes expects that all operation will be performed using Manager classes (each controller must have it own manager) 64 | * Managers classes - classes that implements business logic of application 65 | - `IModelManager` - interface that describes basic operations 66 | - `EfModelManager`- is abstract class that contains implementation of `Get` and `Delete` operations 67 | - `EfSoftRemovableModelManager` is abstract class that contains implementation of `Get` and `Delete` operations with soft removable models (`IsDeleted = true` means model was removed) 68 | 69 | Example of how faster Bulk vs Non-Bulk: 70 | ![Bulk vs Non Bulk](/img/bulk_performance.png) 71 | ``` 72 | Elapsed time in Non-Bulk REST API with EF is 0.9759984016418457 secs. 73 | Elapsed time in Bulk API with EF is 0.004002094268798828 secs. 74 | ``` 75 | as a result we got almost ~`250 x` faster `API`. 76 | 77 | ### 3. Requirements 78 | There is **only ONE requirement**: all Entity classes for any Persistence storage that are using with controllers & managers MUST implements `IModelIdentifiable` from `Wissance.WebApiToolkit.Data.Entity`. 79 | If this toolkit should be used with `EntityFramework` you should derive you resource manager from 80 | `EfModelManager` it have built-in methods for: 81 | * `get many` items 82 | * `get one` item `by id` 83 | * `delete` item `by id` 84 | 85 | 86 | ### 4. Toolkit usage algorithm with EntityFramework 87 | 88 | #### 4.1 REST Services 89 | 90 | Full example is mentioned in section 6 (see below). But if you are starting to build new `REST Resource` 91 | `API` you should do following: 92 | 1. Create a `model` (`entity`) class implementing `IModelIdentifiable` and `DTO` class for it representation (**for soft remove** also **add** `IModelSoftRemovable` implementation), i.e.: 93 | ```csharp 94 | public class BookEntity : IModelIdentifiable 95 | { 96 | public int Id {get; set;} 97 | public string Title {get; set;} 98 | public string Authors {get; set;} // for simplicity 99 | public DateTimeOffset Created {get; set;} 100 | public DateTimeOffset Updated {get; set;} 101 | } 102 | 103 | public class BookDto 104 | { 105 | public int Id {get; set;} 106 | public string Title {get; set;} 107 | public string Authors {get; set;} 108 | } 109 | ``` 110 | 2. Create a factory function (i.e. static function of a static class) that converts `Model` to `DTO` i.e.: 111 | ```csharp 112 | public static class BookFactory 113 | { 114 | public static BookDto Create(BookEntity entity) 115 | { 116 | return new BookDto 117 | { 118 | Id = entity.Id, 119 | Title = entity.Title, 120 | Authors = entity.Authors; 121 | }; 122 | } 123 | } 124 | ``` 125 | 3. Create `IModelContext` interface that has you `BookEntity` as a `DbSet` and it's implementation class that also derives from `DbContext` (**Ef abstract class**): 126 | ```csharp 127 | public interface IModelContext 128 | { 129 | DbSet Books {get;set;} 130 | } 131 | 132 | public MoidelContext: DbContext, IModelContext 133 | { 134 | // todo: not mrntioned here constructor, entity mapping and so on 135 | public DbSet Books {get; set;} 136 | } 137 | ``` 138 | 4. Configure to inject `ModelContext` as a `DbContext` via `DI` see [Startup](https://github.com/Wissance/WeatherControl/blob/master/WeatherControl/Wissance.WeatherControl/Startup.cs) class 139 | 5. Create `Controller` class and a manager class pair, i.e. consider here full `CRUD` 140 | ```csharp 141 | [ApiController] 142 | public class BookController : BasicCrudController 143 | { 144 | public BookController(BookManager manager) 145 | { 146 | Manager = manager; // this is for basic operations 147 | _manager = manager; // this for extended operations 148 | } 149 | 150 | private BookManager _manager; 151 | } 152 | 153 | public class BookManager : EfModelManager 154 | { 155 | public BookManager(ModelContext modelContext, ILoggerFactory loggerFactory) : base(modelContext, BookFactory.Create, loggerFactory) 156 | { 157 | _modelContext = modelContext; 158 | } 159 | 160 | public override async Task> CreateAsync(StationDto data) 161 | { 162 | // todo: implement 163 | } 164 | 165 | public override async Task> UpdateAsync(int id, StationDto data) 166 | { 167 | // todo: implement 168 | } 169 | 170 | private readonly ModelContext _modelContext; 171 | } 172 | ``` 173 | 174 | Last generic parameter in above example - `EmptyAdditionalFilters` is a class that holds 175 | additional parameters for search to see in Swagger, just specify a new class implementing 176 | `IReadFilterable` i.e.: 177 | 178 | ```csharp 179 | public class BooksFilterable : IReadFilterable 180 | { 181 | public IDictionary SelectFilters() 182 | { 183 | IDictionary additionalFilters = new Dictionary(); 184 | if (!string.IsNullOrEmpty(Title)) 185 | { 186 | additionalFilters.Add(FilterParamsNames.TitleParameter, Title); 187 | } 188 | 189 | if (Authors != null && Authors.Length > 0) 190 | { 191 | additionalFilters.Add(FilterParamsNames.AuthorsParameter, string.Join(",", Authors)); 192 | } 193 | 194 | return additionalFilters; 195 | } 196 | 197 | [FromQuery(Name = "title")] public string Title { get; set; } 198 | [FromQuery(Name = "author")] public string[] Authors { get; set; } 199 | } 200 | ``` 201 | 202 | #### 4.2 GRPC Services 203 | 204 | Starting from `v3.0.0` it possible to create GRPC Services and we have algorithm for this with example based on same Manager classes with service classes that works as a proxy for generating GRPC-services, here we have 2 type of services: 205 | 1. `RO` service with methods for Read data - `ResourceBasedDataManageableReadOnlyService` (GRPC equivalent to `BasicReadController`) 206 | 2. `CRUD` service with methods Read + Create + Update and Delete - `ResourceBasedDataManageableCrudService` 207 | 208 | For building GRPC services based on these service implementation we just need to pass instance of this class to constructor, consider that we are having `CodeService` 209 | 210 | ```csharp 211 | public class CodeGrpcService : CodeService.CodeServiceBase 212 | { 213 | public CodeGrpcService(ResourceBasedDataManageableReadOnlyService serviceImpl) 214 | { 215 | _serviceImpl = serviceImpl; 216 | } 217 | 218 | // GRPC methods impl 219 | 220 | private readonly ResourceBasedDataManageableReadOnlyService _serviceImpl; 221 | } 222 | ``` 223 | 224 | Unfortunately GRPC generates all types Request and therefore we should implement additional mapping to convert `DTO` to Response, see full example in this solution in the `Wissance.WebApiToolkit.TestApp` project 225 | 226 | ### 5. Nuget package 227 | You could find nuget-package [here](https://www.nuget.org/packages/Wissance.WebApiToolkit) 228 | 229 | ### 6. Examples 230 | Here we consider only Full CRUD controllers because **Full CRUD = Read Only + Additional Operations (CREATE, UPDATE, DELETE)**, a **full example = full application** created with **Wissance.WebApiToolkit** could be found [here]( https://github.com/Wissance/WeatherControl) 231 | 232 | #### 6.1 REST Service example 233 | 234 | ```csharp 235 | [ApiController] 236 | public class StationController : BasicCrudController 237 | { 238 | public StationController(StationManager manager) 239 | { 240 | Manager = manager; // this is for basic operations 241 | _manager = manager; // this for extended operations 242 | } 243 | 244 | private StationManager _manager; 245 | } 246 | ``` 247 | 248 | ```csharp 249 | public class StationManager : EfModelManager 250 | { 251 | public StationManager(ModelContext modelContext, ILoggerFactory loggerFactory) : base(modelContext, StationFactory.Create, loggerFactory) 252 | { 253 | _modelContext = modelContext; 254 | } 255 | 256 | public override async Task> CreateAsync(StationDto data) 257 | { 258 | try 259 | { 260 | StationEntity entity = StationFactory.Create(data); 261 | await _modelContext.Stations.AddAsync(entity); 262 | int result = await _modelContext.SaveChangesAsync(); 263 | if (result >= 0) 264 | { 265 | return new OperationResultDto(true, (int)HttpStatusCode.Created, null, StationFactory.Create(entity)); 266 | } 267 | return new OperationResultDto(false, (int)HttpStatusCode.InternalServerError, "An unknown error occurred during station creation", null); 268 | 269 | } 270 | catch (Exception e) 271 | { 272 | return new OperationResultDto(false, (int)HttpStatusCode.InternalServerError, $"An error occurred during station creation: {e.Message}", null); 273 | } 274 | } 275 | 276 | public override async Task> UpdateAsync(int id, StationDto data) 277 | { 278 | try 279 | { 280 | StationEntity entity = StationFactory.Create(data); 281 | StationEntity existingEntity = await _modelContext.Stations.FirstOrDefaultAsync(s => s.Id == id); 282 | if (existingEntity == null) 283 | { 284 | return new OperationResultDto(false, (int)HttpStatusCode.NotFound, $"Station with id: {id} does not exists", null); 285 | } 286 | 287 | // Copy only name, description and positions, create measurements if necessary from MeasurementsManager 288 | existingEntity.Name = entity.Name; 289 | existingEntity.Description = existingEntity.Description; 290 | existingEntity.Latitude = existingEntity.Latitude; 291 | existingEntity.Longitude = existingEntity.Longitude; 292 | int result = await _modelContext.SaveChangesAsync(); 293 | if (result >= 0) 294 | { 295 | return new OperationResultDto(true, (int)HttpStatusCode.OK, null, StationFactory.Create(entity)); 296 | } 297 | return new OperationResultDto(false, (int)HttpStatusCode.InternalServerError, "An unknown error occurred during station update", null); 298 | 299 | } 300 | catch (Exception e) 301 | { 302 | return new OperationResultDto(false, (int)HttpStatusCode.InternalServerError, $"An error occurred during station update: {e.Message}", null); 303 | } 304 | 305 | } 306 | 307 | private readonly ModelContext _modelContext; 308 | } 309 | ``` 310 | 311 | *JUST 2 VERY SIMPLE CLASSES ^^ USING WebApiToolkit* 312 | 313 | #### 6.2 GRPC Service example 314 | 315 | For building GRPC service all what we need: 316 | 1. `.proto` file, consider our CodeService example, we have the following GRPC methods: 317 | ```proto 318 | service CodeService { 319 | rpc ReadOne(OneItemRequest) returns (CodeOperationResult); 320 | rpc ReadMany(PageDataRequest) returns (CodePagedDataOperationResult); 321 | } 322 | ``` 323 | 2. `DI` for making service implementation: 324 | ```csharp 325 | private void ConfigureWebServices(IServiceCollection services) 326 | { 327 | services.AddScoped>( 328 | sp => 329 | { 330 | return new ResourceBasedDataManageableReadOnlyService(sp.GetRequiredService()); 331 | }); 332 | } 333 | ``` 334 | 3. GRPC Service that derives from generated service and use as a proxy to `ResourceBasedDataManageableReadOnlyService`: 335 | ```csharp 336 | public class CodeGrpcService : CodeService.CodeServiceBase 337 | { 338 | public CodeGrpcService(ResourceBasedDataManageableReadOnlyService serviceImpl) 339 | { 340 | _serviceImpl = serviceImpl; 341 | } 342 | 343 | public override async Task ReadMany(PageDataRequest request, ServerCallContext context) 344 | { 345 | OperationResultDto> result = await _serviceImpl.ReadAsync(request.Page, request.Size, request.Sort, request.Order, 346 | new EmptyAdditionalFilters()); 347 | context.Status = GrpcErrorCodeHelper.GetGrpcStatus(result.Status, result.Message); 348 | CodePagedDataOperationResult response = new CodePagedDataOperationResult() 349 | { 350 | Success = result.Success, 351 | Message = result.Message ?? String.Empty, 352 | Status = result.Status, 353 | }; 354 | 355 | if (result.Data != null) 356 | { 357 | response.Data = new CodePagedDataResult() 358 | { 359 | Page = result.Data.Page, 360 | Pages = result.Data.Pages, 361 | Total = result.Data.Total, 362 | Data = {result.Data.Data.Select(c => Convert(c))} 363 | }; 364 | } 365 | 366 | return response; 367 | } 368 | 369 | public override async Task ReadOne(OneItemRequest request, ServerCallContext context) 370 | { 371 | OperationResultDto result = await _serviceImpl.ReadByIdAsync(request.Id); 372 | context.Status = GrpcErrorCodeHelper.GetGrpcStatus(result.Status, result.Message); 373 | CodeOperationResult response = new CodeOperationResult() 374 | { 375 | Success = result.Success, 376 | Message = result.Message ?? String.Empty, 377 | Status = result.Status, 378 | Data = Convert(result.Data) 379 | }; 380 | return response; 381 | } 382 | 383 | private Code Convert(CodeDto dto) 384 | { 385 | if (dto == null) 386 | return null; 387 | return new Code() 388 | { 389 | Id = dto.Id, 390 | Code_ = dto.Code, 391 | Name = dto.Name 392 | }; 393 | } 394 | 395 | private readonly ResourceBasedDataManageableReadOnlyService _serviceImpl; 396 | } 397 | ``` 398 | 399 | **Full example how it all works see in `Wissance.WebApiToolkit.TestApp` project**. 400 | 401 | ### 7. Extending API 402 | 403 | #### 7.1 Add new methods to existing controller 404 | Consider we would like to add method search to our controller: 405 | ```csharp 406 | [HttpGet] 407 | [Route("api/[controller]/search")] 408 | public async Task>> SearchAsync([FromQuery]string query, [FromQuery]int page, [FromQuery]int size) 409 | { 410 | OperationResultDto, long>> result = await Manager.GetAsync(page, size, query); 411 | if (result == null) 412 | { 413 | HttpContext.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 414 | } 415 | 416 | HttpContext.Response.StatusCode = result.Status; 417 | return new PagedDataDto(pageNumber, result.Data.Item2, GetTotalPages(result.Data.Item2, pageSize), result.Data.Item1); 418 | } 419 | ``` 420 | 421 | #### 7.2 Add security to protect you API 422 | 423 | We have [additional project](https://github.com/Wissance/Authorization) to protect `API` with `Keycloak` `OpenId-Connect`. 424 | pass `IHttpContextAccessor` to `Manager` class and check something like this: `ClaimsPrincipal principal = _httpContext.HttpContext.User;` 425 | 426 | ### 8. Additional materials 427 | 428 | You could see our articles about Toolkit usage: 429 | * [Medium article about v1.0.x usage]( https://medium.com/@m-ushakov/how-to-reduce-amount-of-code-when-writing-netcore-rest-api-services-28352edcfca6) 430 | * [Dev.to article about v1.0.x usage]( https://dev.to/wissance/dry-your-web-api-net-core-with-our-toolkit-cbb) 431 | 432 | ### 9. Contributors 433 | 434 | 435 | 436 | 437 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Data/Entity/IModelIdentifiable.cs: -------------------------------------------------------------------------------- 1 | namespace Wissance.WebApiToolkit.Data.Entity 2 | { 3 | public interface IModelIdentifiable 4 | { 5 | TId Id { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Data/Entity/IModelSoftRemovable.cs: -------------------------------------------------------------------------------- 1 | namespace Wissance.WebApiToolkit.Data.Entity 2 | { 3 | public interface IModelSoftRemovable 4 | { 5 | bool IsDeleted { get; set; } 6 | } 7 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Data/Entity/IModelTrackable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Wissance.WebApiToolkit.Data.Entity 4 | { 5 | public interface IModelTrackable 6 | { 7 | DateTimeOffset Created { get; set; } 8 | DateTimeOffset Modified { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Data/Tools/DbContextHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using System.Threading.Tasks.Dataflow; 6 | using Microsoft.EntityFrameworkCore; 7 | using Newtonsoft.Json.Linq; 8 | 9 | namespace Wissance.WebApiToolkit.Data.Tools 10 | { 11 | /// 12 | /// This class is a set of tools that helps us working with EntityFramework especially for migration generation 13 | /// in code first approach 14 | /// 15 | public class DbContextHelper 16 | { 17 | /// 18 | /// This function return connecting string from project json config file using path to navigate properties 19 | /// Example we have following JSON with name - migration.setting.json: 20 | /// { 21 | /// "AppSettings": { 22 | /// "Db" : { 23 | /// "ConnStr": ""Server=(localdb)\\mssqllocaldb;Database=MyApp;Trusted_Connection=True;" 24 | /// } 25 | /// } 26 | /// } 27 | /// consider that this settings file is located in project MyApp/MyApp.Data/migration.setting.json 28 | /// where MyApp is solution name, MyApp.Data - project name 29 | /// therefore for these values we should call this method to get conn str: 30 | /// GetConnStrFromJsonConfig("MyApp.Data", "migration.setting.json", "AppSettings.Db.ConnStr") 31 | /// 32 | /// 33 | /// name of json config path or path relative to project 34 | /// path to property that points to connection string 35 | /// 36 | public string GetConnStrFromJsonConfig(string project, string jsonConfigFile, string connStringPath) 37 | { 38 | try 39 | { 40 | string solutionDir = new DirectoryInfo(Environment.CurrentDirectory)?.Parent?.FullName; 41 | if (solutionDir == null) 42 | throw new InvalidOperationException("Solution dir can't be null"); 43 | Console.WriteLine($"Solution directory is: {solutionDir}"); 44 | string startupPath = Path.GetFullPath(Path.Combine(solutionDir, project)); 45 | Console.WriteLine($"Startup path is: {startupPath}"); 46 | string configFullPath = Path.Combine(startupPath, jsonConfigFile); 47 | string jsonData = File.ReadAllText(configFullPath); 48 | JObject json = JObject.Parse(jsonData); 49 | string connectionString = (string)json.SelectToken(connStringPath); 50 | Console.WriteLine($"Connection string is: {connectionString}"); 51 | return connectionString; 52 | } 53 | catch (Exception e) 54 | { 55 | Console.WriteLine($"An error occurred during getting connection string from JSON config file: {e.Message}"); 56 | return null; 57 | } 58 | } 59 | 60 | /// 61 | /// This function helps us to create instance of DbContext. It is more complicated that probably should because 62 | /// DbContext depends on option but options depends on Extensions like .UseMySql(), .UseSqlServer(), pseudo code, 63 | /// Consider that we are having ModelContext class derived from DbContext: 64 | /// { 65 | /// DbContextHelper helper = new DbContextHelper(); 66 | /// // string connStr = "server=127.0.0.1;database=my_app_db;uid=my_app_user;password=myPWD;SslMode=preferred;" 67 | /// string connStr = helper.GetConnStrFromJsonConfig("MyApp.Data", "migration.setting.json", "AppSettings.Db.ConnStr") 68 | /// DbContextOptionsBuilder optionsBuilder = new DbContextOptionsBuilder().UseMySql(connStr, ServerVersion.AutoDetect(connStr)); 69 | /// DbContextOptions options = optionsBuilder.Options; 70 | /// Func, ModelContext> constructor = opts => new ModelContext(opts); 71 | /// ModelContext context = helper.Create(constructor, options); 72 | /// // do other things .... 73 | /// } 74 | /// 75 | /// 76 | /// 77 | /// 78 | /// 79 | public T Create(Func, T> constructor, DbContextOptions options) where T: DbContext, new() 80 | { 81 | return constructor(options); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Data/Wissance.WebApiToolkit.Data.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0;netcoreapp3.1;net5.0;net6.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Dto/OperationResultDto.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Wissance.WebApiToolkit.Dto 4 | { 5 | public class OperationResultDto 6 | { 7 | public OperationResultDto() 8 | { 9 | } 10 | 11 | public OperationResultDto(bool success, int status, string message, T data) 12 | { 13 | Success = success; 14 | Status = status; 15 | Message = message; 16 | Data = data; 17 | } 18 | 19 | public bool Success { get; set; } 20 | [JsonIgnore] 21 | public int Status { get; set; } 22 | public string Message { get; set; } 23 | public T Data { get; set; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Dto/PagedDataDto.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace Wissance.WebApiToolkit.Dto 5 | { 6 | /// 7 | /// DTO that represents a collection of items of a same type i.e. a result of querying GET /api/controller/?page={p}&size={s} 8 | /// 9 | /// Type representing REST resource 10 | public class PagedDataDto where T: class 11 | { 12 | public PagedDataDto() 13 | { 14 | Data = new List(); 15 | } 16 | 17 | public PagedDataDto(long page, long total, long pages, IList data) 18 | { 19 | Page = page; 20 | Total = total; 21 | Pages = pages; 22 | Data = data; 23 | } 24 | 25 | /// 26 | /// Page is a number of data portion with specific size from beginning 27 | /// 28 | public long Page { get; set; } 29 | /// 30 | /// Total is a total number of items 31 | /// 32 | public long Total { get; set; } 33 | /// 34 | /// Pages is a total number of pages of a specified size 35 | /// 36 | public long Pages { get; set; } 37 | /// 38 | /// Data portion itself 39 | /// 40 | public IList Data { get; set; } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Dto/Wissance.WebApiToolkit.Dto.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0;netcoreapp3.1;net5.0;net6.0 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Controllers/CodeController.cs: -------------------------------------------------------------------------------- 1 | using Wissance.WebApiToolkit.Controllers; 2 | using Wissance.WebApiToolkit.Data; 3 | using Wissance.WebApiToolkit.TestApp.Data.Entity; 4 | using Wissance.WebApiToolkit.TestApp.Dto; 5 | using Wissance.WebApiToolkit.TestApp.Managers; 6 | 7 | namespace Wissance.WebApiToolkit.TestApp.Controllers 8 | { 9 | public sealed class CodeController : BasicReadController 10 | { 11 | public CodeController(CodeManager manager) 12 | { 13 | Manager = manager; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Controllers/OrganizationController.cs: -------------------------------------------------------------------------------- 1 | using Wissance.WebApiToolkit.Controllers; 2 | using Wissance.WebApiToolkit.Data; 3 | using Wissance.WebApiToolkit.TestApp.Data.Entity; 4 | using Wissance.WebApiToolkit.TestApp.Dto; 5 | using Wissance.WebApiToolkit.TestApp.Managers; 6 | 7 | namespace Wissance.WebApiToolkit.TestApp.Controllers 8 | { 9 | public class OrganizationController : BasicCrudController 10 | { 11 | public OrganizationController(OrganizationManager manager) 12 | { 13 | Manager = manager; 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Data/Entity/CodeEntity.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Collections.Generic; 3 | using Wissance.WebApiToolkit.Data.Entity; 4 | 5 | namespace Wissance.WebApiToolkit.TestApp.Data.Entity 6 | { 7 | public class CodeEntity : IModelIdentifiable 8 | { 9 | public int Id { get; set; } 10 | public string Code { get; set; } 11 | public string Name { get; set; } 12 | 13 | public virtual IList Organizations { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Data/Entity/Mapping/CodeMapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | 3 | namespace Wissance.WebApiToolkit.TestApp.Data.Entity.Mapping 4 | { 5 | internal static class CodeMapper 6 | { 7 | public static void Map(this EntityTypeBuilder builder) 8 | { 9 | builder.HasKey(p => p.Id); 10 | builder.Property(p => p.Code).IsRequired(); 11 | builder.Property(p => p.Name).IsRequired(); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Data/Entity/Mapping/OrganizationMapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | 3 | namespace Wissance.WebApiToolkit.TestApp.Data.Entity.Mapping 4 | { 5 | internal static class OrganizationMapper 6 | { 7 | public static void Map(this EntityTypeBuilder builder) 8 | { 9 | builder.HasKey(p => p.Id); 10 | builder.Property(p => p.Name).IsRequired(); 11 | builder.Property(p => p.ShortName).IsRequired(); 12 | builder.Property(p => p.TaxNumber).IsRequired(); 13 | builder.HasMany(p => p.Users) 14 | .WithOne(p => p.Organization) 15 | .HasForeignKey(p => p.OrganizationId); 16 | builder.HasMany(p => p.Codes).WithMany(p => p.Organizations); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Data/Entity/Mapping/UserMapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 2 | 3 | namespace Wissance.WebApiToolkit.TestApp.Data.Entity.Mapping 4 | { 5 | internal static class UserMapper 6 | { 7 | public static void Map(this EntityTypeBuilder builder) 8 | { 9 | builder.HasKey(p => p.Id); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Data/Entity/OrganizationEntity.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Wissance.WebApiToolkit.Data.Entity; 3 | 4 | namespace Wissance.WebApiToolkit.TestApp.Data.Entity 5 | { 6 | public class OrganizationEntity : IModelIdentifiable 7 | { 8 | public int Id { get; set; } 9 | public string Name { get; set; } 10 | public string ShortName { get; set; } 11 | public string TaxNumber { get; set; } 12 | public virtual IList Users { get; set; } 13 | public virtual IList Codes { get; set; } 14 | } 15 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Data/Entity/ProfileEntity.cs: -------------------------------------------------------------------------------- 1 | namespace Wissance.WebApiToolkit.TestApp.Data.Entity 2 | { 3 | public class ProfileEntity 4 | { 5 | 6 | } 7 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Data/Entity/RoleEntity.cs: -------------------------------------------------------------------------------- 1 | using Wissance.WebApiToolkit.Data.Entity; 2 | 3 | namespace Wissance.WebApiToolkit.TestApp.Data.Entity 4 | { 5 | public class RoleEntity : IModelIdentifiable 6 | { 7 | public int Id { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Data/Entity/UserEntity.cs: -------------------------------------------------------------------------------- 1 | using Wissance.WebApiToolkit.Data.Entity; 2 | 3 | namespace Wissance.WebApiToolkit.TestApp.Data.Entity 4 | { 5 | public class UserEntity : IModelIdentifiable 6 | { 7 | public int Id { get; set; } 8 | public string FullName { get; set; } 9 | public string Login { get; set; } 10 | public int OrganizationId { get; set; } 11 | public virtual OrganizationEntity Organization { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Data/ModelContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Wissance.WebApiToolkit.TestApp.Data.Entity; 3 | using Wissance.WebApiToolkit.TestApp.Data.Entity.Mapping; 4 | 5 | namespace Wissance.WebApiToolkit.TestApp.Data 6 | { 7 | public class ModelContext : DbContext 8 | { 9 | public ModelContext() 10 | { 11 | } 12 | 13 | public ModelContext(DbContextOptions options) 14 | :base(options) 15 | { 16 | } 17 | 18 | public override int SaveChanges() 19 | { 20 | try 21 | { 22 | return base.SaveChanges(); 23 | } 24 | catch (Exception e) 25 | { 26 | return -1; 27 | } 28 | } 29 | 30 | public async Task SaveChangesAsync() 31 | { 32 | return await base.SaveChangesAsync(); 33 | } 34 | 35 | 36 | protected override void OnModelCreating(ModelBuilder modelBuilder) 37 | { 38 | base.OnModelCreating(modelBuilder); 39 | 40 | modelBuilder.Entity().Map(); 41 | modelBuilder.Entity().Map(); 42 | modelBuilder.Entity().Map(); 43 | } 44 | 45 | public DbSet Codes { get; set; } 46 | public DbSet Organizations { get; set; } 47 | public DbSet Users { get; set; } 48 | } 49 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/DataInitializer.cs: -------------------------------------------------------------------------------- 1 | using Wissance.WebApiToolkit.TestApp.Data; 2 | using Wissance.WebApiToolkit.TestApp.Data.Entity; 3 | 4 | namespace Wissance.WebApiToolkit.TestApp 5 | { 6 | public static class DataInitializer 7 | { 8 | public static void Init(ModelContext context) 9 | { 10 | InitCodes(context); 11 | InitOrganizations(context); 12 | InitUsers(context); 13 | } 14 | 15 | private static void InitCodes(ModelContext context) 16 | { 17 | IList codes = new List() 18 | { 19 | new CodeEntity() 20 | { 21 | Id = 1, 22 | Code = "1", 23 | Name = "Software development" 24 | }, 25 | new CodeEntity() 26 | { 27 | Id = 2, 28 | Code = "2", 29 | Name = "Hardware development" 30 | }, 31 | new CodeEntity() 32 | { 33 | Id = 3, 34 | Code = "3", 35 | Name = "Researching" 36 | } 37 | }; 38 | context.AddRange(codes); 39 | context.SaveChanges(); 40 | } 41 | 42 | private static void InitOrganizations(ModelContext context) 43 | { 44 | IList codes = context.Codes.ToList(); 45 | for (int i = 0; i < 10; i++) 46 | { 47 | OrganizationEntity organization = new OrganizationEntity() 48 | { 49 | Name = $"LLC Organization {i}", 50 | ShortName = $"Organization {i}", 51 | TaxNumber = $"9{i}8{i}765{i}10", 52 | Codes = new List() 53 | }; 54 | if (i % 2 == 0) 55 | { 56 | organization.Codes.Add(codes[0]); 57 | organization.Codes.Add(codes[1]); 58 | } 59 | else 60 | { 61 | organization.Codes.Add(codes[1]); 62 | organization.Codes.Add(codes[2]); 63 | } 64 | 65 | context.Organizations.Add(organization); 66 | } 67 | 68 | context.SaveChanges(); 69 | } 70 | 71 | private static void InitUsers(ModelContext context) 72 | { 73 | 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Dto/CodeDto.cs: -------------------------------------------------------------------------------- 1 | namespace Wissance.WebApiToolkit.TestApp.Dto 2 | { 3 | public class CodeDto 4 | { 5 | public int Id { get; set; } 6 | public string Code { get; set; } 7 | public string Name { get; set; } 8 | } 9 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Dto/OrganizationDto.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Wissance.WebApiToolkit.TestApp.Dto 4 | { 5 | public class OrganizationDto 6 | { 7 | public OrganizationDto() 8 | { 9 | Codes = new List(); 10 | } 11 | 12 | public int Id { get; set; } 13 | public string Name { get; set; } 14 | public string ShortName { get; set; } 15 | public string TaxNumber { get; set; } 16 | 17 | public IList Codes { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Factories/CodeFactory.cs: -------------------------------------------------------------------------------- 1 | using Wissance.WebApiToolkit.TestApp.Data.Entity; 2 | using Wissance.WebApiToolkit.TestApp.Dto; 3 | 4 | namespace Wissance.WebApiToolkit.TestApp.Factories 5 | { 6 | internal static class CodeFactory 7 | { 8 | public static CodeDto Create(CodeEntity entity) 9 | { 10 | return new CodeDto() 11 | { 12 | Id = entity.Id, 13 | Name = entity.Name, 14 | Code = entity.Code 15 | }; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Factories/OrganizationFactory.cs: -------------------------------------------------------------------------------- 1 | using Wissance.WebApiToolkit.TestApp.Data.Entity; 2 | using Wissance.WebApiToolkit.TestApp.Dto; 3 | 4 | namespace Wissance.WebApiToolkit.TestApp.Factories 5 | { 6 | internal static class OrganizationFactory 7 | { 8 | public static OrganizationDto Create(OrganizationEntity entity) 9 | { 10 | return new OrganizationDto() 11 | { 12 | Id = entity.Id, 13 | Name = entity.Name, 14 | ShortName = entity.ShortName, 15 | TaxNumber = entity.TaxNumber, 16 | Codes = entity.Codes != null ? entity.Codes.Select(c => c.Id).ToList() : new List() 17 | }; 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Managers/CodeManager.cs: -------------------------------------------------------------------------------- 1 | 2 | using Microsoft.EntityFrameworkCore; 3 | using Wissance.WebApiToolkit.Managers; 4 | using Wissance.WebApiToolkit.TestApp.Data; 5 | using Wissance.WebApiToolkit.TestApp.Data.Entity; 6 | using Wissance.WebApiToolkit.TestApp.Dto; 7 | 8 | namespace Wissance.WebApiToolkit.TestApp.Managers 9 | { 10 | public class CodeManager : EfModelManager 11 | { 12 | public CodeManager(ModelContext dbContext, Func, bool> filterFunc, Func createFunc, 13 | ILoggerFactory loggerFactory) 14 | : base(dbContext, filterFunc, createFunc, loggerFactory) 15 | { 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Managers/OrganizationManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Logging; 7 | using Wissance.WebApiToolkit.Dto; 8 | using Wissance.WebApiToolkit.Managers; 9 | using Wissance.WebApiToolkit.TestApp.Data; 10 | using Wissance.WebApiToolkit.TestApp.Data.Entity; 11 | using Wissance.WebApiToolkit.TestApp.Dto; 12 | using Wissance.WebApiToolkit.TestApp.Factories; 13 | 14 | namespace Wissance.WebApiToolkit.TestApp.Managers 15 | { 16 | public class OrganizationManager : EfModelManager 17 | { 18 | public OrganizationManager(ModelContext dbContext, Func, bool> filterFunc, Func createFunc, ILoggerFactory loggerFactory) 19 | : base(dbContext, filterFunc, createFunc, loggerFactory) 20 | { 21 | _dbContext = dbContext; 22 | } 23 | 24 | public override async Task> CreateAsync(OrganizationDto data) 25 | { 26 | try 27 | { 28 | OrganizationEntity organization = new OrganizationEntity() 29 | { 30 | Name = data.Name, 31 | ShortName = data.ShortName, 32 | TaxNumber = data.TaxNumber 33 | }; 34 | 35 | // todo(UMV): temporarily offed 36 | /*if (data.Codes != null) 37 | { 38 | foreach (int code in data.Codes) 39 | { 40 | CodeEntity organizationCode = await _dbContext.Codes.FirstOrDefaultAsync(c => c.Id == code); 41 | organization.Codes.Add(organizationCode); 42 | } 43 | }*/ 44 | 45 | int result = await _dbContext.SaveChangesAsync(); 46 | if (result < 0) 47 | { 48 | return new OperationResultDto(false, (int) HttpStatusCode.InternalServerError, 49 | $"An unknown error occurred during Organization create", null); 50 | } 51 | 52 | return new OperationResultDto(true, (int) HttpStatusCode.Created, String.Empty, 53 | OrganizationFactory.Create(organization)); 54 | } 55 | catch (Exception e) 56 | { 57 | return new OperationResultDto(false, (int) HttpStatusCode.InternalServerError, 58 | $"An error occurred during Organization create: {e.Message}", null); 59 | } 60 | } 61 | 62 | public override async Task> UpdateAsync(int id, OrganizationDto data) 63 | { 64 | try 65 | { 66 | OrganizationEntity organization = await _dbContext.Organizations.FirstOrDefaultAsync(o => o.Id == id); 67 | if (organization == null) 68 | { 69 | return new OperationResultDto(false, (int) HttpStatusCode.NotFound, 70 | $"Organization with id: {id} was not found", null); 71 | } 72 | 73 | organization.Name = data.Name; 74 | organization.ShortName = data.ShortName; 75 | organization.TaxNumber = data.TaxNumber; 76 | 77 | int result = await _dbContext.SaveChangesAsync(); 78 | if (result < 0) 79 | { 80 | return new OperationResultDto(false, (int) HttpStatusCode.InternalServerError, 81 | $"An unknown error occurred during Organization update", null); 82 | } 83 | return new OperationResultDto(true, (int) HttpStatusCode.OK, 84 | String.Empty, OrganizationFactory.Create(organization)); 85 | } 86 | catch (Exception e) 87 | { 88 | return new OperationResultDto(false, (int) HttpStatusCode.InternalServerError, 89 | $"An error occurred during Organization update: {e.Message}", null); 90 | } 91 | } 92 | 93 | private readonly ModelContext _dbContext; 94 | } 95 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Program.cs: -------------------------------------------------------------------------------- 1 | namespace Wissance.WebApiToolkit.TestApp 2 | { 3 | public class Program 4 | { 5 | public static void Main(string[] args) 6 | { 7 | IHostBuilder hostBuilder = CreateWebHostBuilder(args); 8 | hostBuilder.Build().Run(); 9 | } 10 | 11 | public static IHostBuilder CreateWebHostBuilder(string[] args) 12 | { 13 | //todo: umv: temporarily stub 14 | _environment = "Development"; 15 | //webHostBuilder.GetSetting("environment"); 16 | 17 | IConfiguration configuration = new ConfigurationBuilder().SetBasePath(Directory.GetCurrentDirectory()) 18 | .AddJsonFile("appsettings.json") 19 | .AddJsonFile($"appsettings.{_environment}.json") 20 | .Build(); 21 | IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(builder => 22 | { 23 | builder.UseConfiguration(configuration); 24 | builder.UseStartup(); 25 | builder.UseKestrel(); 26 | }); 27 | return hostBuilder; 28 | } 29 | 30 | private static string _environment; 31 | } 32 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:40232", 8 | "sslPort": 44360 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "applicationUrl": "http://localhost:5182", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "https": { 22 | "commandName": "Project", 23 | "dotnetRunMessages": true, 24 | "launchBrowser": true, 25 | "applicationUrl": "https://localhost:7182;http://localhost:5182", 26 | "environmentVariables": { 27 | "ASPNETCORE_ENVIRONMENT": "Development" 28 | } 29 | }, 30 | "IIS Express": { 31 | "commandName": "IISExpress", 32 | "launchBrowser": true, 33 | "environmentVariables": { 34 | "ASPNETCORE_ENVIRONMENT": "Development" 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | using Wissance.WebApiToolkit.Data; 9 | using Wissance.WebApiToolkit.Services; 10 | using Wissance.WebApiToolkit.TestApp.Data; 11 | using Wissance.WebApiToolkit.TestApp.Data.Entity; 12 | using Wissance.WebApiToolkit.TestApp.Dto; 13 | using Wissance.WebApiToolkit.TestApp.Factories; 14 | using Wissance.WebApiToolkit.TestApp.Managers; 15 | using Wissance.WebApiToolkit.TestApp.WebServices.Grpc; 16 | 17 | namespace Wissance.WebApiToolkit.TestApp 18 | { 19 | public class Startup 20 | { 21 | public Startup(IConfiguration configuration, IWebHostEnvironment env) 22 | { 23 | Configuration = configuration; 24 | Environment = env; 25 | } 26 | 27 | public void ConfigureServices(IServiceCollection services) 28 | { 29 | ConfigureLogging(services); 30 | ConfigureDatabase(services); 31 | ConfigureWebApi(services); 32 | } 33 | 34 | private void ConfigureLogging(IServiceCollection services) 35 | { 36 | services.AddLogging(loggingBuilder => loggingBuilder.AddConfiguration(Configuration).AddConsole()); 37 | services.AddLogging(loggingBuilder => loggingBuilder.AddConfiguration(Configuration).AddDebug()); 38 | } 39 | 40 | private void ConfigureDatabase(IServiceCollection services) 41 | { 42 | Guid id = Guid.NewGuid(); 43 | services.AddDbContext(options => options.UseQueryTrackingBehavior(QueryTrackingBehavior.TrackAll) 44 | .UseInMemoryDatabase(id.ToString())); 45 | // init database with test data 46 | ServiceProvider sp = services.BuildServiceProvider(); 47 | ModelContext context = sp.GetRequiredService(); 48 | DataInitializer.Init(context); 49 | } 50 | 51 | private void ConfigureWebApi(IServiceCollection services) 52 | { 53 | services.AddControllers(); 54 | ConfigureManagers(services); 55 | services.AddGrpc(); 56 | ConfigureWebServices(services); 57 | } 58 | 59 | private void ConfigureManagers(IServiceCollection services) 60 | { 61 | services.AddScoped(sp => 62 | { 63 | // filter function was not written here yet 64 | return new CodeManager(sp.GetRequiredService(), 65 | null, CodeFactory.Create, sp.GetRequiredService()); 66 | }); 67 | 68 | services.AddScoped(sp => 69 | { 70 | return new OrganizationManager(sp.GetRequiredService(), 71 | null, OrganizationFactory.Create, sp.GetRequiredService()); 72 | }); 73 | } 74 | 75 | private void ConfigureWebServices(IServiceCollection services) 76 | { 77 | services.AddScoped>( 78 | sp => 79 | { 80 | return new ResourceBasedDataManageableReadOnlyService(sp.GetRequiredService()); 81 | }); 82 | } 83 | 84 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 85 | { 86 | app.UseRouting(); 87 | app.UseEndpoints(endpoints => 88 | { 89 | endpoints.MapControllers(); 90 | endpoints.MapGrpcService(); 91 | }); 92 | 93 | } 94 | 95 | private IConfiguration Configuration { get; } 96 | private IWebHostEnvironment Environment { get; } 97 | } 98 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/WebServices/Grpc/CodeGrpcService.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | using Wissance.WebApiToolkit.Data; 3 | using Wissance.WebApiToolkit.Dto; 4 | using Wissance.WebApiToolkit.Services; 5 | using Wissance.WebApiToolkit.TestApp.Data.Entity; 6 | using Wissance.WebApiToolkit.TestApp.Dto; 7 | using Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated; 8 | using Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Helpers; 9 | 10 | namespace Wissance.WebApiToolkit.TestApp.WebServices.Grpc 11 | { 12 | public class CodeGrpcService : CodeService.CodeServiceBase 13 | { 14 | public CodeGrpcService(ResourceBasedDataManageableReadOnlyService serviceImpl) 15 | { 16 | _serviceImpl = serviceImpl; 17 | } 18 | 19 | public override async Task ReadMany(PageDataRequest request, ServerCallContext context) 20 | { 21 | OperationResultDto> result = await _serviceImpl.ReadAsync(request.Page, request.Size, request.Sort, request.Order, 22 | new EmptyAdditionalFilters()); 23 | context.Status = GrpcErrorCodeHelper.GetGrpcStatus(result.Status, result.Message); 24 | CodePagedDataOperationResult response = new CodePagedDataOperationResult() 25 | { 26 | Success = result.Success, 27 | Message = result.Message ?? String.Empty, 28 | Status = result.Status, 29 | }; 30 | 31 | if (result.Data != null) 32 | { 33 | response.Data = new CodePagedDataResult() 34 | { 35 | Page = result.Data.Page, 36 | Pages = result.Data.Pages, 37 | Total = result.Data.Total, 38 | Data = {result.Data.Data.Select(c => Convert(c))} 39 | }; 40 | } 41 | 42 | return response; 43 | } 44 | 45 | public override async Task ReadOne(OneItemRequest request, ServerCallContext context) 46 | { 47 | OperationResultDto result = await _serviceImpl.ReadByIdAsync(request.Id); 48 | context.Status = GrpcErrorCodeHelper.GetGrpcStatus(result.Status, result.Message); 49 | CodeOperationResult response = new CodeOperationResult() 50 | { 51 | Success = result.Success, 52 | Message = result.Message ?? String.Empty, 53 | Status = result.Status, 54 | Data = Convert(result.Data) 55 | }; 56 | return response; 57 | } 58 | 59 | private Code Convert(CodeDto dto) 60 | { 61 | if (dto == null) 62 | return null; 63 | return new Code() 64 | { 65 | Id = dto.Id, 66 | Code_ = dto.Code, 67 | Name = dto.Name 68 | }; 69 | } 70 | 71 | private readonly ResourceBasedDataManageableReadOnlyService _serviceImpl; 72 | } 73 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/WebServices/Grpc/Generated/CodeGrpc.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Generated by the protocol buffer compiler. DO NOT EDIT! 3 | // source: Code.proto 4 | // 5 | #pragma warning disable 0414, 1591, 8981, 0612 6 | #region Designer generated code 7 | 8 | using grpc = global::Grpc.Core; 9 | 10 | namespace Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated { 11 | public static partial class CodeService 12 | { 13 | static readonly string __ServiceName = "CodeService"; 14 | 15 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 16 | static void __Helper_SerializeMessage(global::Google.Protobuf.IMessage message, grpc::SerializationContext context) 17 | { 18 | #if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION 19 | if (message is global::Google.Protobuf.IBufferMessage) 20 | { 21 | context.SetPayloadLength(message.CalculateSize()); 22 | global::Google.Protobuf.MessageExtensions.WriteTo(message, context.GetBufferWriter()); 23 | context.Complete(); 24 | return; 25 | } 26 | #endif 27 | context.Complete(global::Google.Protobuf.MessageExtensions.ToByteArray(message)); 28 | } 29 | 30 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 31 | static class __Helper_MessageCache 32 | { 33 | public static readonly bool IsBufferMessage = global::System.Reflection.IntrospectionExtensions.GetTypeInfo(typeof(global::Google.Protobuf.IBufferMessage)).IsAssignableFrom(typeof(T)); 34 | } 35 | 36 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 37 | static T __Helper_DeserializeMessage(grpc::DeserializationContext context, global::Google.Protobuf.MessageParser parser) where T : global::Google.Protobuf.IMessage 38 | { 39 | #if !GRPC_DISABLE_PROTOBUF_BUFFER_SERIALIZATION 40 | if (__Helper_MessageCache.IsBufferMessage) 41 | { 42 | return parser.ParseFrom(context.PayloadAsReadOnlySequence()); 43 | } 44 | #endif 45 | return parser.ParseFrom(context.PayloadAsNewBuffer()); 46 | } 47 | 48 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 49 | static readonly grpc::Marshaller __Marshaller_OneItemRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.OneItemRequest.Parser)); 50 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 51 | static readonly grpc::Marshaller __Marshaller_CodeOperationResult = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.CodeOperationResult.Parser)); 52 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 53 | static readonly grpc::Marshaller __Marshaller_PageDataRequest = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.PageDataRequest.Parser)); 54 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 55 | static readonly grpc::Marshaller __Marshaller_CodePagedDataOperationResult = grpc::Marshallers.Create(__Helper_SerializeMessage, context => __Helper_DeserializeMessage(context, global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.CodePagedDataOperationResult.Parser)); 56 | 57 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 58 | static readonly grpc::Method __Method_ReadOne = new grpc::Method( 59 | grpc::MethodType.Unary, 60 | __ServiceName, 61 | "ReadOne", 62 | __Marshaller_OneItemRequest, 63 | __Marshaller_CodeOperationResult); 64 | 65 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 66 | static readonly grpc::Method __Method_ReadMany = new grpc::Method( 67 | grpc::MethodType.Unary, 68 | __ServiceName, 69 | "ReadMany", 70 | __Marshaller_PageDataRequest, 71 | __Marshaller_CodePagedDataOperationResult); 72 | 73 | /// Service descriptor 74 | public static global::Google.Protobuf.Reflection.ServiceDescriptor Descriptor 75 | { 76 | get { return global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.CodeReflection.Descriptor.Services[0]; } 77 | } 78 | 79 | /// Base class for server-side implementations of CodeService 80 | [grpc::BindServiceMethod(typeof(CodeService), "BindService")] 81 | public abstract partial class CodeServiceBase 82 | { 83 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 84 | public virtual global::System.Threading.Tasks.Task ReadOne(global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.OneItemRequest request, grpc::ServerCallContext context) 85 | { 86 | throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); 87 | } 88 | 89 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 90 | public virtual global::System.Threading.Tasks.Task ReadMany(global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.PageDataRequest request, grpc::ServerCallContext context) 91 | { 92 | throw new grpc::RpcException(new grpc::Status(grpc::StatusCode.Unimplemented, "")); 93 | } 94 | 95 | } 96 | 97 | /// Client for CodeService 98 | public partial class CodeServiceClient : grpc::ClientBase 99 | { 100 | /// Creates a new client for CodeService 101 | /// The channel to use to make remote calls. 102 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 103 | public CodeServiceClient(grpc::ChannelBase channel) : base(channel) 104 | { 105 | } 106 | /// Creates a new client for CodeService that uses a custom CallInvoker. 107 | /// The callInvoker to use to make remote calls. 108 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 109 | public CodeServiceClient(grpc::CallInvoker callInvoker) : base(callInvoker) 110 | { 111 | } 112 | /// Protected parameterless constructor to allow creation of test doubles. 113 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 114 | protected CodeServiceClient() : base() 115 | { 116 | } 117 | /// Protected constructor to allow creation of configured clients. 118 | /// The client configuration. 119 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 120 | protected CodeServiceClient(ClientBaseConfiguration configuration) : base(configuration) 121 | { 122 | } 123 | 124 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 125 | public virtual global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.CodeOperationResult ReadOne(global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.OneItemRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) 126 | { 127 | return ReadOne(request, new grpc::CallOptions(headers, deadline, cancellationToken)); 128 | } 129 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 130 | public virtual global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.CodeOperationResult ReadOne(global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.OneItemRequest request, grpc::CallOptions options) 131 | { 132 | return CallInvoker.BlockingUnaryCall(__Method_ReadOne, null, options, request); 133 | } 134 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 135 | public virtual grpc::AsyncUnaryCall ReadOneAsync(global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.OneItemRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) 136 | { 137 | return ReadOneAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken)); 138 | } 139 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 140 | public virtual grpc::AsyncUnaryCall ReadOneAsync(global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.OneItemRequest request, grpc::CallOptions options) 141 | { 142 | return CallInvoker.AsyncUnaryCall(__Method_ReadOne, null, options, request); 143 | } 144 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 145 | public virtual global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.CodePagedDataOperationResult ReadMany(global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.PageDataRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) 146 | { 147 | return ReadMany(request, new grpc::CallOptions(headers, deadline, cancellationToken)); 148 | } 149 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 150 | public virtual global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.CodePagedDataOperationResult ReadMany(global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.PageDataRequest request, grpc::CallOptions options) 151 | { 152 | return CallInvoker.BlockingUnaryCall(__Method_ReadMany, null, options, request); 153 | } 154 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 155 | public virtual grpc::AsyncUnaryCall ReadManyAsync(global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.PageDataRequest request, grpc::Metadata headers = null, global::System.DateTime? deadline = null, global::System.Threading.CancellationToken cancellationToken = default(global::System.Threading.CancellationToken)) 156 | { 157 | return ReadManyAsync(request, new grpc::CallOptions(headers, deadline, cancellationToken)); 158 | } 159 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 160 | public virtual grpc::AsyncUnaryCall ReadManyAsync(global::Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated.PageDataRequest request, grpc::CallOptions options) 161 | { 162 | return CallInvoker.AsyncUnaryCall(__Method_ReadMany, null, options, request); 163 | } 164 | /// Creates a new instance of client from given ClientBaseConfiguration. 165 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 166 | protected override CodeServiceClient NewInstance(ClientBaseConfiguration configuration) 167 | { 168 | return new CodeServiceClient(configuration); 169 | } 170 | } 171 | 172 | /// Creates service definition that can be registered with a server 173 | /// An object implementing the server-side handling logic. 174 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 175 | public static grpc::ServerServiceDefinition BindService(CodeServiceBase serviceImpl) 176 | { 177 | return grpc::ServerServiceDefinition.CreateBuilder() 178 | .AddMethod(__Method_ReadOne, serviceImpl.ReadOne) 179 | .AddMethod(__Method_ReadMany, serviceImpl.ReadMany).Build(); 180 | } 181 | 182 | /// Register service method with a service binder with or without implementation. Useful when customizing the service binding logic. 183 | /// Note: this method is part of an experimental API that can change or be removed without any prior notice. 184 | /// Service methods will be bound by calling AddMethod on this object. 185 | /// An object implementing the server-side handling logic. 186 | [global::System.CodeDom.Compiler.GeneratedCode("grpc_csharp_plugin", null)] 187 | public static void BindService(grpc::ServiceBinderBase serviceBinder, CodeServiceBase serviceImpl) 188 | { 189 | serviceBinder.AddMethod(__Method_ReadOne, serviceImpl == null ? null : new grpc::UnaryServerMethod(serviceImpl.ReadOne)); 190 | serviceBinder.AddMethod(__Method_ReadMany, serviceImpl == null ? null : new grpc::UnaryServerMethod(serviceImpl.ReadMany)); 191 | } 192 | 193 | } 194 | } 195 | #endregion 196 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/WebServices/Grpc/Helpers/GrpcErrorCodeHelper.cs: -------------------------------------------------------------------------------- 1 | using Grpc.Core; 2 | 3 | namespace Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Helpers 4 | { 5 | public static class GrpcErrorCodeHelper 6 | { 7 | public static Status GetGrpcStatus(int statusCode, string message) 8 | { 9 | StatusCode grpcCode = StatusCode.Unknown; 10 | if (_httpToGrpcCodeMap.ContainsKey(statusCode)) 11 | grpcCode = _httpToGrpcCodeMap[statusCode]; 12 | return new Status(grpcCode, message); 13 | } 14 | 15 | private static IDictionary _httpToGrpcCodeMap = new Dictionary() 16 | { 17 | {200, StatusCode.OK}, 18 | {201, StatusCode.OK}, 19 | {400, StatusCode.InvalidArgument}, 20 | {404, StatusCode.NotFound}, 21 | {500, StatusCode.Internal} 22 | }; 23 | } 24 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/WebServices/Grpc/Proto/Code.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | option csharp_namespace = "Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated"; 4 | 5 | import 'Common/Common.proto'; 6 | 7 | message Code { 8 | required int32 id = 1; 9 | required string code = 2; 10 | required string name = 3; 11 | } 12 | 13 | message CodeOperationResult { 14 | required bool success = 1; 15 | optional string message = 2; 16 | required int32 status = 3; 17 | optional Code data = 4; 18 | } 19 | 20 | message CodePagedDataResult { 21 | required int64 page = 1; 22 | required int64 pages = 2; 23 | required int64 total = 3; 24 | repeated Code data = 4; 25 | } 26 | 27 | message CodePagedDataOperationResult { 28 | required bool success = 1; 29 | optional string message = 2; 30 | required int32 status = 3; 31 | optional CodePagedDataResult data = 4; 32 | } 33 | 34 | service CodeService { 35 | rpc ReadOne(OneItemRequest) returns (CodeOperationResult); 36 | rpc ReadMany(PageDataRequest) returns (CodePagedDataOperationResult); 37 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/WebServices/Grpc/Proto/Common/Common.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto2"; 2 | 3 | option csharp_namespace = "Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated"; 4 | 5 | message OperationResultStats { 6 | required bool success = 1; 7 | required string message = 2; 8 | required int32 status = 3; 9 | extensions 4 to max; 10 | } 11 | 12 | message PagedDataResult { 13 | required int64 page = 1; 14 | required int64 pages = 2; 15 | required int64 total = 3; 16 | extensions 4 to max; 17 | } 18 | 19 | message OneItemRequest { 20 | required int32 id = 1; 21 | } 22 | 23 | message PageDataRequest { 24 | optional int32 page = 1; 25 | optional int32 size = 2; 26 | optional string sort = 3; 27 | optional string order = 4; 28 | extensions 5 to max; 29 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/Wissance.WebApiToolkit.TestApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | disable 6 | enable 7 | 10 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | true 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.TestApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Tests/Controllers/TestBasicCrudController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Net.Http.Json; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | using Wissance.WebApiToolkit.Dto; 8 | using Wissance.WebApiToolkit.TestApp.Dto; 9 | using Wissance.WebApiToolkit.Tests.Utils; 10 | using Wissance.WebApiToolkit.Tests.Utils.Checkers; 11 | 12 | namespace Wissance.WebApiToolkit.Tests.Controllers 13 | { 14 | public class TestBasicCrudController : WebApiTestBasedOnTestApp 15 | { 16 | [Fact] 17 | public async Task TestReadAsync() 18 | { 19 | using (HttpClient client = Application.CreateClient()) 20 | { 21 | HttpResponseMessage resp = await client.GetAsync("api/Organization"); 22 | Assert.Equal(HttpStatusCode.OK, resp.StatusCode); 23 | string pagedDataStr = await resp.Content.ReadAsStringAsync(); 24 | Assert.True(pagedDataStr.Length > 0); 25 | OperationResultDto> result = JsonConvert.DeserializeObject>>(pagedDataStr); 26 | // TODO(UMV): check very formally only that ReadAsync returns PagedData wrapped in OperationResult 27 | Assert.NotNull(result); 28 | Assert.True(result.Success); 29 | } 30 | } 31 | 32 | [Fact] 33 | public async Task TestCreateAsync() 34 | { 35 | using (HttpClient client = Application.CreateClient()) 36 | { 37 | OrganizationDto organization = new OrganizationDto() 38 | { 39 | Name = "LLC SuperDuper", 40 | ShortName = "SuperDuper", 41 | TaxNumber = "999091234", 42 | Codes = new List(){1, 3} 43 | }; 44 | JsonContent content = JsonContent.Create(organization); 45 | HttpResponseMessage resp = await client.PostAsync("api/Organization", content); 46 | Assert.Equal(HttpStatusCode.Created, resp.StatusCode); 47 | string orgCreateDataStr = await resp.Content.ReadAsStringAsync(); 48 | Assert.True(orgCreateDataStr.Length > 0); 49 | OperationResultDto result = JsonConvert.DeserializeObject>(orgCreateDataStr); 50 | Assert.NotNull(result); 51 | Assert.True(result.Success); 52 | // todo(UMV): perform body check 53 | } 54 | } 55 | 56 | [Theory] 57 | [InlineData(1)] 58 | public async Task TestUpdateAsync(int organizationId) 59 | { 60 | using (HttpClient client = Application.CreateClient()) 61 | { 62 | HttpResponseMessage resp = await client.GetAsync($"api/Organization/{organizationId}"); 63 | Assert.Equal(HttpStatusCode.OK, resp.StatusCode); 64 | string orgDataStr = await resp.Content.ReadAsStringAsync(); 65 | Assert.True(orgDataStr.Length > 0); 66 | OperationResultDto result = JsonConvert.DeserializeObject>(orgDataStr); 67 | Assert.NotNull(result); 68 | Assert.True(result.Success); 69 | result.Data.Name = "TestNameUpd LLC"; 70 | result.Data.ShortName = "TestNameUpd"; 71 | JsonContent content = JsonContent.Create(result.Data); 72 | resp = await client.PutAsync($"api/Organization/{organizationId}", content); 73 | Assert.Equal(HttpStatusCode.OK, resp.StatusCode); 74 | string updOrgDataStr = await resp.Content.ReadAsStringAsync(); 75 | Assert.True(updOrgDataStr.Length > 0); 76 | OperationResultDto updResult = JsonConvert.DeserializeObject>(updOrgDataStr); 77 | Assert.NotNull(updResult); 78 | Assert.True(updResult.Success); 79 | OrganizationChecker.Check(result.Data, updResult.Data); 80 | } 81 | } 82 | 83 | [Theory] 84 | [InlineData(1)] 85 | public async Task TestDeleteAsync(int organizationId) 86 | { 87 | using (HttpClient client = Application.CreateClient()) 88 | { 89 | HttpResponseMessage resp = await client.DeleteAsync($"api/Organization/{organizationId}"); 90 | Assert.Equal(HttpStatusCode.NoContent, resp.StatusCode); 91 | resp = await client.DeleteAsync($"api/Organization/{organizationId}"); 92 | Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode); 93 | resp = await client.GetAsync($"api/Organization/{organizationId}"); 94 | Assert.Equal(HttpStatusCode.NotFound, resp.StatusCode); 95 | string orgDataStr = await resp.Content.ReadAsStringAsync(); 96 | Assert.True(orgDataStr.Length > 0); 97 | OperationResultDto result = JsonConvert.DeserializeObject>(orgDataStr); 98 | Assert.NotNull(result); 99 | Assert.False(result.Success); 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Tests/Controllers/TestBasicReadController.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Http; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Newtonsoft.Json; 6 | using Wissance.WebApiToolkit.Dto; 7 | using Wissance.WebApiToolkit.TestApp.Dto; 8 | using Wissance.WebApiToolkit.Tests.Utils; 9 | using Wissance.WebApiToolkit.Tests.Utils.Checkers; 10 | 11 | namespace Wissance.WebApiToolkit.Tests.Controllers 12 | { 13 | public class TestBasicReadController : WebApiTestBasedOnTestApp 14 | { 15 | 16 | [Theory] 17 | [InlineData(0, 0, 1, 1, 3)] 18 | [InlineData(1, -1, 1, 1, 3)] 19 | [InlineData(0, null, 1, 1, 3)] 20 | [InlineData(1, null, 1, 1, 3)] 21 | [InlineData(-1, -1, 1, 1, 3)] 22 | [InlineData(null, null, 1, 1, 3)] 23 | [InlineData(1, 10, 1, 1, 3)] 24 | [InlineData(2, 2, 2, 2, 3)] 25 | public async Task TestReadAsync(int? page, int? size, int expectedPage, int expectedPages, int expectedTotal) 26 | { 27 | using(HttpClient client = Application.CreateClient()) 28 | { 29 | StringBuilder resourceUri = new StringBuilder("/api/Code"); 30 | if (page != null) 31 | resourceUri.Append($"?page={page.Value}"); 32 | 33 | if (size != null) 34 | { 35 | if (page != null) 36 | resourceUri.Append($"&size={size.Value}"); 37 | else 38 | resourceUri.Append($"?size={size.Value}"); 39 | } 40 | 41 | HttpResponseMessage resp = await client.GetAsync(resourceUri.ToString()); 42 | Assert.Equal(HttpStatusCode.OK, resp.StatusCode); 43 | string pagedDataStr = await resp.Content.ReadAsStringAsync(); 44 | Assert.True(pagedDataStr.Length > 0); 45 | OperationResultDto> result = JsonConvert.DeserializeObject>>(pagedDataStr); 46 | // TODO(UMV): check very formally only that ReadAsync returns PagedData wrapped in OperationResult 47 | Assert.NotNull(result); 48 | Assert.True(result.Success); 49 | Assert.Equal(expectedPage, result.Data.Page); 50 | Assert.Equal(expectedPages, result.Data.Pages); 51 | Assert.Equal(expectedTotal, result.Data.Total); 52 | } 53 | } 54 | 55 | [Fact] 56 | public async Task TestReadByIdAsync() 57 | { 58 | using(HttpClient client = Application.CreateClient()) 59 | { 60 | HttpResponseMessage resp = await client.GetAsync("/api/Code/2"); 61 | Assert.Equal(HttpStatusCode.OK, resp.StatusCode); 62 | string readStr = await resp.Content.ReadAsStringAsync(); 63 | Assert.True(readStr.Length > 0); 64 | OperationResultDto result = JsonConvert.DeserializeObject>(readStr); 65 | Assert.NotNull(result); 66 | Assert.True(result.Success); 67 | CodeDto expectedCode = new CodeDto() 68 | { 69 | Id = 2, 70 | Code = "2", 71 | Name = "Hardware development" 72 | }; 73 | CodeChecker.Check(expectedCode, result.Data); 74 | } 75 | 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Tests/Services/TestResourceBasedDataManageableReadOnlyService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated; 3 | using Wissance.WebApiToolkit.Tests.Utils; 4 | using Wissance.WebApiToolkit.Tests.Utils.Checkers; 5 | 6 | namespace Wissance.WebApiToolkit.Tests.Services 7 | { 8 | public class TestResourceBasedDataManageableReadOnlyService : WebApiTestBasedOnTestApp 9 | { 10 | 11 | [Theory] 12 | [InlineData(1, 25, 1)] 13 | [InlineData(1, 2, 2)] 14 | public async Task TestReadAsync(int page, int size, int expectedPages) 15 | { 16 | CodeService.CodeServiceClient client = new CodeService.CodeServiceClient(CreateChannel()); 17 | PageDataRequest request = new PageDataRequest() 18 | { 19 | Page = page, 20 | Size = size 21 | }; 22 | 23 | CodePagedDataOperationResult response = await client.ReadManyAsync(request); 24 | Assert.True(response.Success); 25 | Assert.Equal(expectedPages, response.Data.Pages); 26 | } 27 | 28 | [Theory] 29 | [InlineData(1)] 30 | public async Task TestReadByIdAsync(int id) 31 | { 32 | CodeService.CodeServiceClient client = new CodeService.CodeServiceClient(CreateChannel()); 33 | OneItemRequest request = new OneItemRequest() 34 | { 35 | Id = id 36 | }; 37 | CodeOperationResult response = await client.ReadOneAsync(request); 38 | Code expected = new Code() 39 | { 40 | Id = 1, 41 | Name = "Software development", 42 | Code_ = "1" 43 | }; 44 | CodeChecker.Check(expected, response.Data); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Tests/Utils/Checkers/CodeChecker.cs: -------------------------------------------------------------------------------- 1 | using Wissance.WebApiToolkit.TestApp.Dto; 2 | using Wissance.WebApiToolkit.TestApp.WebServices.Grpc.Generated; 3 | 4 | namespace Wissance.WebApiToolkit.Tests.Utils.Checkers 5 | { 6 | internal static class CodeChecker 7 | { 8 | public static void Check(CodeDto expected, CodeDto actual) 9 | { 10 | Assert.Equal(expected.Id, actual.Id); 11 | Assert.Equal(expected.Name, actual.Name); 12 | Assert.Equal(expected.Code, actual.Code); 13 | } 14 | 15 | public static void Check(Code expected, Code actual) 16 | { 17 | Assert.Equal(expected.Id, actual.Id); 18 | Assert.Equal(expected.Name, actual.Name); 19 | Assert.Equal(expected.Code_, actual.Code_); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Tests/Utils/Checkers/OrganizationChecker.cs: -------------------------------------------------------------------------------- 1 | using Wissance.WebApiToolkit.TestApp.Dto; 2 | 3 | namespace Wissance.WebApiToolkit.Tests.Utils.Checkers 4 | { 5 | internal static class OrganizationChecker 6 | { 7 | public static void Check(OrganizationDto expected, OrganizationDto actual) 8 | { 9 | Assert.Equal(expected.Id, actual.Id); 10 | Assert.Equal(expected.Name, actual.Name); 11 | Assert.Equal(expected.ShortName, actual.ShortName); 12 | Assert.Equal(expected.TaxNumber, actual.TaxNumber); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Tests/Utils/WebApiTestBasedOnTestApp.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http; 2 | using Grpc.Net.Client; 3 | using Microsoft.AspNetCore.Mvc.Testing; 4 | using Wissance.WebApiToolkit.TestApp; 5 | 6 | namespace Wissance.WebApiToolkit.Tests.Utils 7 | { 8 | public class WebApiTestBasedOnTestApp 9 | { 10 | public WebApiTestBasedOnTestApp() 11 | { 12 | Application = new WebApplicationFactory(); 13 | } 14 | 15 | public GrpcChannel CreateChannel() 16 | { 17 | GrpcChannel channel = GrpcChannel.ForAddress("http://localhost", 18 | new GrpcChannelOptions() 19 | { 20 | HttpClient = Application.CreateClient() 21 | }); 22 | return channel; 23 | } 24 | 25 | public WebApplicationFactory Application { get; } 26 | } 27 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.Tests/Wissance.WebApiToolkit.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | disable 6 | disable 7 | 8 | false 9 | true 10 | 10 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31729.503 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wissance.WebApiToolkit", "Wissance.WebApiToolkit\Wissance.WebApiToolkit.csproj", "{8FB88DB6-B241-4B50-B6C9-385615183E0E}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wissance.WebApiToolkit.Dto", "Wissance.WebApiToolkit.Dto\Wissance.WebApiToolkit.Dto.csproj", "{2E1C637A-A6A8-45A6-A568-24DDCCF9558C}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wissance.WebApiToolkit.Data", "Wissance.WebApiToolkit.Data\Wissance.WebApiToolkit.Data.csproj", "{63E47004-27FF-4AEA-8679-9789A8D73A83}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wissance.WebApiToolkit.Tests", "Wissance.WebApiToolkit.Tests\Wissance.WebApiToolkit.Tests.csproj", "{94BB07CF-9B3F-4562-9A44-65A397DA6AAC}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Wissance.WebApiToolkit.TestApp", "Wissance.WebApiToolkit.TestApp\Wissance.WebApiToolkit.TestApp.csproj", "{0897B59F-D6F8-4AC5-ACE6-CCD7A1026B93}" 15 | EndProject 16 | Global 17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 18 | Debug|Any CPU = Debug|Any CPU 19 | Release|Any CPU = Release|Any CPU 20 | EndGlobalSection 21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 22 | {8FB88DB6-B241-4B50-B6C9-385615183E0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 23 | {8FB88DB6-B241-4B50-B6C9-385615183E0E}.Debug|Any CPU.Build.0 = Debug|Any CPU 24 | {8FB88DB6-B241-4B50-B6C9-385615183E0E}.Release|Any CPU.ActiveCfg = Release|Any CPU 25 | {8FB88DB6-B241-4B50-B6C9-385615183E0E}.Release|Any CPU.Build.0 = Release|Any CPU 26 | {2E1C637A-A6A8-45A6-A568-24DDCCF9558C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {2E1C637A-A6A8-45A6-A568-24DDCCF9558C}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {2E1C637A-A6A8-45A6-A568-24DDCCF9558C}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {2E1C637A-A6A8-45A6-A568-24DDCCF9558C}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {63E47004-27FF-4AEA-8679-9789A8D73A83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {63E47004-27FF-4AEA-8679-9789A8D73A83}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {63E47004-27FF-4AEA-8679-9789A8D73A83}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {63E47004-27FF-4AEA-8679-9789A8D73A83}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {94BB07CF-9B3F-4562-9A44-65A397DA6AAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {94BB07CF-9B3F-4562-9A44-65A397DA6AAC}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {94BB07CF-9B3F-4562-9A44-65A397DA6AAC}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {94BB07CF-9B3F-4562-9A44-65A397DA6AAC}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {0897B59F-D6F8-4AC5-ACE6-CCD7A1026B93}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {0897B59F-D6F8-4AC5-ACE6-CCD7A1026B93}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {0897B59F-D6F8-4AC5-ACE6-CCD7A1026B93}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {0897B59F-D6F8-4AC5-ACE6-CCD7A1026B93}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(ExtensibilityGlobals) = postSolution 47 | SolutionGuid = {BD004E69-A9CC-4CB6-A044-46A6CDA9D20F} 48 | EndGlobalSection 49 | EndGlobal 50 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Controllers/BasicBulkCrudController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Wissance.WebApiToolkit.Data; 4 | using Wissance.WebApiToolkit.Dto; 5 | 6 | namespace Wissance.WebApiToolkit.Controllers 7 | { 8 | [Route("api/bulk/[controller]")] 9 | public class BasicBulkCrudController : BasicReadController 10 | where TRes : class 11 | where TFilter: class, IReadFilterable 12 | { 13 | [HttpPost] 14 | public virtual async Task> BulkCreateAsync([FromBody] TRes[] data) 15 | { 16 | OperationResultDto result = await Manager.BulkCreateAsync(data); 17 | HttpContext.Response.StatusCode = result.Status; 18 | return result; 19 | } 20 | 21 | [HttpPut] 22 | public virtual async Task> UpdateAsync([FromBody] TRes[] data) 23 | { 24 | OperationResultDto result = await Manager.BulkUpdateAsync(data); 25 | HttpContext.Response.StatusCode = result.Status; 26 | return result; 27 | } 28 | 29 | [HttpDelete] 30 | public virtual async Task DeleteAsync([FromQuery] TId[] id) 31 | { 32 | OperationResultDto result = await Manager.BulkDeleteAsync(id); 33 | HttpContext.Response.StatusCode = result.Status; 34 | return; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Controllers/BasicCrudController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Wissance.WebApiToolkit.Data; 6 | using Wissance.WebApiToolkit.Dto; 7 | 8 | namespace Wissance.WebApiToolkit.Controllers 9 | { 10 | [Route("api/[controller]")] 11 | public abstract class BasicCrudController : BasicReadController 12 | where TRes : class 13 | where TFilter: class, IReadFilterable 14 | { 15 | [HttpPost] 16 | public virtual async Task> CreateAsync([FromBody] TRes data) 17 | { 18 | OperationResultDto result = await Manager.CreateAsync(data); 19 | HttpContext.Response.StatusCode = result.Status; 20 | return result; 21 | } 22 | 23 | [HttpPut("{id}")] 24 | public virtual async Task> UpdateAsync([FromRoute] TId id, [FromBody] TRes data) 25 | { 26 | OperationResultDto result = await Manager.UpdateAsync(id, data); 27 | HttpContext.Response.StatusCode = result.Status; 28 | return result; 29 | } 30 | 31 | [HttpDelete("{id}")] 32 | public virtual async Task DeleteAsync([FromRoute] TId id) 33 | { 34 | OperationResultDto result = await Manager.DeleteAsync(id); 35 | HttpContext.Response.StatusCode = result.Status; 36 | return; 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Controllers/BasicPagedDataController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Wissance.WebApiToolkit.Utils; 3 | 4 | namespace Wissance.WebApiToolkit.Controllers 5 | { 6 | public abstract class BasicPagedDataController : ControllerBase 7 | { 8 | protected int GetPage(int? page) 9 | { 10 | return PagingUtils.GetPage(page); 11 | } 12 | 13 | protected int GetPageSize(int? size) 14 | { 15 | return PagingUtils.GetPageSize(size); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Controllers/BasicReadController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Primitives; 8 | using Wissance.WebApiToolkit.Data; 9 | using Wissance.WebApiToolkit.Dto; 10 | using Wissance.WebApiToolkit.Managers; 11 | using Wissance.WebApiToolkit.Utils; 12 | 13 | namespace Wissance.WebApiToolkit.Controllers 14 | { 15 | 16 | /// 17 | /// This is a basic Read Controller implementing two operations: 18 | /// 1. Get multiple items via api/controller/[page={20} & size={50} & sort=name & order=asc|desc ] with paging 19 | /// and sorting by one column and filtering by any number of parameters, sort is a name of column/property 20 | /// order is a sort order only asc and desc values are allowed. 21 | /// Examples with Station controller: 22 | /// - ~/api/Station to get items with default paging (page = 1, size = 25) 23 | /// - ~/api/Station?page=2&size=50 to get items with paging options (page = 2, size = 50) 24 | /// - ~/api/Station?page=1&size=40&sort=name&order=desc to get items with paging and sorting by column/property 25 | /// name and in desc order (from Z to A) 26 | /// - ~/api/Station?page=1&size=40&sort=name&order=desc&from=2022-01-01&to=2024-12-31 to get items with paging, 27 | /// sorting and filter by date in range (2022-01-01, 2024-12-31) 28 | /// 2. Get single item via api by id, example with Station controller: 29 | /// - ~/api/Station/145 to get station with id = 145 30 | /// Restrictions: 31 | /// 1. We are working with a single type of result representation inn Response (TRes). 32 | /// 2. To get custom filters there should be a class implementing IReadFilter, you could just return an 33 | /// empty dictionary and act with TFilter on you own way. 34 | /// 3. There are no option (currently) to receive all data without paging 35 | /// 36 | /// Is A DTO object type to return back 37 | /// Is a type that represents persistent object (i.e. Entity class) 38 | /// Is a type of persistant object identifier 39 | /// Is a type of filter class 40 | [Route("api/[controller]")] 41 | public abstract class BasicReadController : BasicPagedDataController 42 | where TRes: class 43 | where TFilter: class, IReadFilterable 44 | { 45 | [HttpGet] 46 | public virtual async Task>> ReadAsync([FromQuery] int? page, [FromQuery] int? size, [FromQuery] string sort, 47 | [FromQuery] string order, TFilter additionalFilters = null) 48 | { 49 | int pageNumber = GetPage(page); 50 | int pageSize = GetPageSize(size); 51 | SortOption sorting = !string.IsNullOrEmpty(sort) ? new SortOption(sort, order) : null; 52 | IDictionary additionalQueryParams = additionalFilters != null 53 | ? additionalFilters.SelectFilters() 54 | : new Dictionary(); 55 | OperationResultDto, long>> result = await Manager.GetAsync(pageNumber, pageSize, sorting, additionalQueryParams); 56 | HttpContext.Response.StatusCode = result.Status; 57 | return new OperationResultDto>(result.Success, result.Status, result.Message, 58 | new PagedDataDto(pageNumber, result.Data.Item2, PagingUtils.GetTotalPages(result.Data.Item2, pageSize), result.Data.Item1)); 59 | } 60 | 61 | [HttpGet("{id}")] 62 | public virtual async Task> ReadByIdAsync([FromRoute] TId id) 63 | { 64 | OperationResultDto result = await Manager.GetByIdAsync(id); 65 | HttpContext.Response.StatusCode = result.Status; 66 | return result; 67 | } 68 | public IModelManager Manager { get; set; } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Data/EmptyAdditionalFilters.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Wissance.WebApiToolkit.Data 4 | { 5 | public class EmptyAdditionalFilters : IReadFilterable 6 | { 7 | public IDictionary SelectFilters() 8 | { 9 | return new Dictionary(); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Data/IReadFilterable.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Wissance.WebApiToolkit.Data 4 | { 5 | public interface IReadFilterable 6 | { 7 | IDictionary SelectFilters(); 8 | } 9 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Data/SortOption.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Wissance.WebApiToolkit.Data 4 | { 5 | public enum SortOrder 6 | { 7 | Ascending, 8 | Descending 9 | } 10 | 11 | public class SortOption 12 | { 13 | public SortOption(string sort, string sortOrder) 14 | { 15 | Sort = sort; 16 | if (string.IsNullOrEmpty(sortOrder)) 17 | { 18 | Order = SortOrder.Ascending; 19 | } 20 | else 21 | { 22 | string lowerOrderVal = sortOrder.ToLower(); 23 | if (_availableSortOptions.ContainsKey(lowerOrderVal)) 24 | { 25 | Order = _availableSortOptions[lowerOrderVal]; 26 | } 27 | else 28 | { 29 | Order = SortOrder.Ascending; 30 | } 31 | } 32 | } 33 | 34 | public string Sort { get; } 35 | public SortOrder Order { get; } 36 | 37 | private readonly IDictionary _availableSortOptions = new Dictionary() 38 | { 39 | {"asc", SortOrder.Ascending}, 40 | {"desc", SortOrder.Descending} 41 | }; 42 | } 43 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Globals/MessageCatalog.cs: -------------------------------------------------------------------------------- 1 | namespace Wissance.WebApiToolkit.Globals 2 | { 3 | public static class MessageCatalog 4 | { 5 | public const string ResourceNotFoundTemplate = "Recource of type \"{0}\" with id: {1} was not found"; 6 | public const string CurrentUserIsNotResourceOwnerTemplate = "Current user is not \"{0}\" owner"; 7 | public const string CreateFailureMessageTemplate = "An error occurred during \"{0}\" create with error: {1}"; 8 | public const string UpdateFailureMessageTemplate = "An error occurred during \"{0}\" update with id: \"{1}\", error: {2}"; 9 | public const string UpdateFailureNotFoundMessageTemplate = "{0} with id: {1} was not found"; 10 | 11 | public const string UnknownErrorMessageTemplate = "An error occurred during {0} \"{1}\", contact system maintainer"; 12 | public const string UserNotAuthenticatedMessage = "User is not authenticated"; 13 | } 14 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Managers/EfModelManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.EntityFrameworkCore; 9 | using Wissance.WebApiToolkit.Data; 10 | using Wissance.WebApiToolkit.Data.Entity; 11 | using Wissance.WebApiToolkit.Dto; 12 | using Wissance.WebApiToolkit.Managers.Helpers; 13 | 14 | namespace Wissance.WebApiToolkit.Managers 15 | { 16 | /// 17 | /// This is a Model Manager for working with EntityFramework ORM (EF) as a tool for perform CRUD operations over persistent objects 18 | /// It has a default implementation of the following method of IModelManager: 19 | /// * GetAsync method for obtain many items 20 | /// * GetByIdAsync method for obtain one item by id 21 | /// * Delete method 22 | /// 23 | /// DTO class (representation of Model in other systems i.e. in frontend)) 24 | /// Model class implements IModelIdentifiable 25 | /// Identifier type that is using as database table PK 26 | public abstract class EfModelManager : IModelManager 27 | where TObj: class, IModelIdentifiable 28 | where TRes: class 29 | where TId: IComparable 30 | 31 | { 32 | /// 33 | /// Constructor of default model manager requires that Model Context derives from EfDbContext 34 | /// 35 | /// Ef Database context 36 | /// Delegate (factory func) for creating DTO from Model 37 | /// Function that use dictionary with query params to filter result set 38 | /// Logger factory 39 | /// 40 | public EfModelManager(DbContext dbContext, Func, bool> filterFunc, Func createFunc, 41 | ILoggerFactory loggerFactory) 42 | { 43 | _dbContext = dbContext ?? throw new ArgumentNullException("dbContext"); 44 | _logger = loggerFactory.CreateLogger>(); 45 | _defaultCreateFunc = createFunc; 46 | _filterFunc = filterFunc; 47 | } 48 | 49 | /// 50 | /// GetManyAsync method allow to extract data from Ef using optional sorting and filtering (sorting applies first, then filter). 51 | /// This method contains more params then GetAsync(), GetAsync default impl calls this method and pass null as filter && sort. 52 | /// This method designed for having ADDITIONAL GET methods in controller. 53 | /// 54 | /// Page number starting from 1 55 | /// Data portion size 56 | /// Query parameters for data filter 57 | /// sorting params (Sort - Field name, Order - Sort direction (ASC, DESC)) 58 | /// Function that describes how to filter data prior to get a portion 59 | /// >Function that describes how to sort data prior to get a portion 60 | /// Function that describes how to construct DTO from Model, if null passes here then uses _defaultCreateFunc 61 | /// OperationResult with data portion 62 | public virtual async Task, long>>> GetManyAsync(int page, int size, IDictionary parameters, SortOption sorting, 63 | Func, bool> filterFunc = null, 64 | Func sortFunc = null, Func createFunc = null) 65 | { 66 | try 67 | { 68 | //IQueryable filteredObjects = dbSet; 69 | long totalItems = 0; 70 | DbSet dbSet = _dbContext.Set(); 71 | IList entities = null; 72 | if (sorting != null) 73 | { 74 | if (sortFunc != null) 75 | { 76 | if (sorting.Order == SortOrder.Ascending) 77 | { 78 | entities = dbSet.OrderBy(sortFunc).ToList(); 79 | } 80 | else 81 | { 82 | entities = dbSet.OrderByDescending(sortFunc).ToList(); 83 | } 84 | } 85 | } 86 | 87 | if (filterFunc != null) 88 | { 89 | // filteredObjects = await dbSet.ToListAsync(); 90 | if (entities == null) 91 | { 92 | entities = await dbSet.ToListAsync(); 93 | } 94 | 95 | IEnumerable items = entities.Where(o => filterFunc(o, parameters)); 96 | totalItems = items.Count(); 97 | entities = items.Skip(size * (page - 1)).Take(size).ToList(); 98 | } 99 | else 100 | { 101 | if (entities == null) 102 | { 103 | totalItems = await dbSet.LongCountAsync(); 104 | entities = await dbSet.Skip(size * (page - 1)).Take(size).ToListAsync(); 105 | } 106 | else 107 | { 108 | totalItems = await dbSet.LongCountAsync(); 109 | entities = entities.Skip(size * (page - 1)).Take(size).ToList(); 110 | } 111 | } 112 | 113 | return new OperationResultDto, long>>(true, (int)HttpStatusCode.OK, null, 114 | new Tuple, long>(entities.Select(e => createFunc!=null ? createFunc(e) : _defaultCreateFunc(e)).ToList(), totalItems)); 115 | } 116 | catch (Exception e) 117 | { 118 | _logger.LogError($"An error: {e.Message} occurred during collection of object of type: {typeof(TObj)} retrieve and convert to objects of type: {typeof(TRes)}"); 119 | return new OperationResultDto, long>>(true, (int)HttpStatusCode.InternalServerError, "Error occurred, contact system maintainer", 120 | new Tuple, long>(null, 0)); 121 | } 122 | } 123 | 124 | /// 125 | /// GetOneAsync method used 4 getting ONE object by id from Database using EF. Optionally could be used different createFunc, but in most 126 | /// cases (99.(9) percents) this method is identical to GetByIdAsync. 127 | /// 128 | /// item identifier 129 | /// Function that describes how to construct DTO from Model, if null passes here then uses _defaultCreateFunc 130 | /// 131 | public virtual async Task> GetOneAsync(TId id, Func createFunc = null) 132 | { 133 | try 134 | { 135 | DbSet dbSet = _dbContext.Set(); 136 | TObj entity = await dbSet.FirstOrDefaultAsync(i => i.Id.Equals(id)); 137 | if (entity == null) 138 | return new OperationResultDto(false, (int)HttpStatusCode.NotFound, 139 | ResponseMessageBuilder.GetResourceNotFoundMessage(typeof(TObj).ToString(), id), null); 140 | return new OperationResultDto(true, (int)HttpStatusCode.OK, null, 141 | createFunc != null?createFunc(entity): _defaultCreateFunc(entity)); 142 | } 143 | catch (Exception e) 144 | { 145 | _logger.LogError($"An error: {e.Message} occurred during object of type: {typeof(TObj)} with id: {id} retrieve and convert to object of type: {typeof(TRes)}"); 146 | return new OperationResultDto(false, (int)HttpStatusCode.NotFound, 147 | ResponseMessageBuilder.GetResourceNotFoundMessage(typeof(TObj).ToString(), id), null); 148 | } 149 | } 150 | 151 | /// 152 | /// GetAsync return portion of DTO unlike GetMany methods have not a default sorting && filtering . Default implementation 153 | /// of IModelManager for get data portion via EF. 154 | /// 155 | /// page number starting from 1 156 | /// size of data portion 157 | /// sorting params (Sort - Field name, Order - Sort direction (ASC, DESC)) 158 | /// raw query parameters 159 | /// OperationResult with data portion 160 | public virtual async Task, long>>> GetAsync(int page, int size, SortOption sorting = null, 161 | IDictionary parameters = null) 162 | { 163 | // this method is using default sorting and order, if specific order or sorting is required please specify it using another GetAsync method 164 | Func sortingFunc = null; 165 | if (sorting != null) 166 | { 167 | PropertyInfo[] modelProperties = typeof(TObj).GetProperties(); 168 | MethodInfo prop = modelProperties.FirstOrDefault(p => string.Equals(p.Name.ToLower(), sorting.Sort.ToLower()))?.GetGetMethod(); 169 | if (prop != null) 170 | { 171 | sortingFunc = o => prop.Invoke(o, null); 172 | } 173 | } 174 | 175 | return await GetManyAsync(page, size, parameters, sorting, _filterFunc, sortingFunc); 176 | } 177 | 178 | /// 179 | /// GetByIdAsync returns one item by id, IModelManager default implementation 180 | /// 181 | /// item identifier 182 | /// OperationResult with one item 183 | public async Task> GetByIdAsync(TId id) 184 | { 185 | return await GetOneAsync(id); 186 | } 187 | 188 | /// 189 | /// Method for create new object in database using Ef, in this class still have not a default impl, but will be 190 | /// 191 | /// DTO with Model representation 192 | /// DTO of newly created object 193 | /// 194 | public virtual Task> CreateAsync(TRes data) 195 | { 196 | throw new NotImplementedException(); 197 | } 198 | 199 | /// 200 | /// Method for create new objects in database using Ef, in this class still have not a default impl, but will be 201 | /// 202 | /// Array of DTO with Model representation 203 | /// Array of DTO of a newly created objects 204 | /// 205 | public virtual Task> BulkCreateAsync(TRes[] data) 206 | { 207 | throw new NotImplementedException(); 208 | } 209 | 210 | /// 211 | /// Method for update existing objects using EF, still have not default impl, but will be 212 | /// 213 | /// item identifier 214 | /// >DTO with Model representation 215 | /// DTO of updated object 216 | /// 217 | public virtual Task> UpdateAsync(TId id, TRes data) 218 | { 219 | throw new NotImplementedException(); 220 | } 221 | 222 | /// 223 | /// Method for update existing objects in a database using Ef, in this class still have not a default impl, but will be 224 | /// 225 | /// Array of DTO with Model representation 226 | /// Array of DTO of a updated objects 227 | /// 228 | public virtual Task> BulkUpdateAsync(TRes[] data) 229 | { 230 | throw new NotImplementedException(); 231 | } 232 | 233 | /// 234 | /// DeleteAsync method for remove object from Database using Ef 235 | /// 236 | /// item identifier 237 | /// true if removal was successful, otherwise false 238 | public async Task> DeleteAsync(TId id) 239 | { 240 | try 241 | { 242 | DbSet dbSet = _dbContext.Set(); 243 | TObj item = await dbSet.FirstOrDefaultAsync(t => t.Id.Equals(id)); 244 | 245 | if (item == null) 246 | return new OperationResultDto(false, (int)HttpStatusCode.NotFound, "Item does not exists", false); 247 | dbSet.Remove(item); 248 | await _dbContext.SaveChangesAsync(); 249 | return new OperationResultDto(true, (int)HttpStatusCode.NoContent, null, true); 250 | } 251 | catch (Exception e) 252 | { 253 | _logger.LogError($"An error occurred during object of type: {nameof(TObj)} with id: {id} remove: {e.Message}"); 254 | return new OperationResultDto(false, (int)HttpStatusCode.InternalServerError, "Error occurred during object delete, contact system maintainer", false); 255 | } 256 | } 257 | 258 | /// 259 | /// BulkDeleteAsync method for remove object from Database using Ef 260 | /// 261 | /// item identifiers 262 | /// true if removal was successful, otherwise false 263 | public virtual Task> BulkDeleteAsync(TId[] objectsIds) 264 | { 265 | throw new NotImplementedException(); 266 | } 267 | 268 | private readonly ILogger> _logger; 269 | private readonly DbContext _dbContext; 270 | private readonly Func _defaultCreateFunc; 271 | private readonly Func, bool> _filterFunc; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Managers/EfSoftRemovableModelManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Reflection; 6 | using System.Threading.Tasks; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.Logging; 9 | using Wissance.WebApiToolkit.Data; 10 | using Wissance.WebApiToolkit.Data.Entity; 11 | using Wissance.WebApiToolkit.Dto; 12 | using Wissance.WebApiToolkit.Managers.Helpers; 13 | 14 | namespace Wissance.WebApiToolkit.Managers 15 | { 16 | public abstract class EfSoftRemovableModelManager : IModelManager 17 | where TObj: class, IModelIdentifiable, IModelSoftRemovable 18 | where TRes: class 19 | where TId: IComparable 20 | { 21 | /// 22 | /// Constructor of default model manager requires that Model Context derives from EfDbContext 23 | /// 24 | /// Ef Database context 25 | /// Delegate (factory func) for creating DTO from Model 26 | /// Function that use dictionary with query params to filter result set 27 | /// Logger factory 28 | /// 29 | public EfSoftRemovableModelManager(DbContext dbContext, Func, bool> filterFunc, Func createFunc, 30 | ILoggerFactory loggerFactory) 31 | { 32 | _dbContext = dbContext ?? throw new ArgumentNullException("dbContext"); 33 | _logger = loggerFactory.CreateLogger>(); 34 | _defaultCreateFunc = createFunc; 35 | _filterFunc = filterFunc; 36 | } 37 | 38 | /// 39 | /// GetManyAsync method allow to extract data from Ef using optional sorting and filtering (sorting applies first, then filter). 40 | /// This method contains more params then GetAsync(), GetAsync default impl calls this method and pass null as filter && sort. 41 | /// This method designed for having ADDITIONAL GET methods in controller. 42 | /// 43 | /// Page number starting from 1 44 | /// Data portion size 45 | /// Query parameters for data filter 46 | /// sorting params (Sort - Field name, Order - Sort direction (ASC, DESC)) 47 | /// Function that describes how to filter data prior to get a portion 48 | /// >Function that describes how to sort data prior to get a portion 49 | /// Function that describes how to construct DTO from Model, if null passes here then uses _defaultCreateFunc 50 | /// OperationResult with data portion 51 | public virtual async Task, long>>> GetManyAsync(int page, int size, IDictionary parameters, SortOption sorting, 52 | Func, bool> filterFunc = null, 53 | Func sortFunc = null, Func createFunc = null) 54 | { 55 | try 56 | { 57 | //IQueryable filteredObjects = dbSet; 58 | long totalItems = 0; 59 | DbSet dbSet = _dbContext.Set(); 60 | IList entities = null; 61 | if (sorting != null) 62 | { 63 | if (sortFunc != null) 64 | { 65 | if (sorting.Order == SortOrder.Ascending) 66 | { 67 | entities = dbSet.Where(m => !m.IsDeleted).OrderBy(sortFunc).ToList(); 68 | } 69 | else 70 | { 71 | entities = dbSet.Where(m => !m.IsDeleted).OrderByDescending(sortFunc).ToList(); 72 | } 73 | } 74 | } 75 | 76 | if (filterFunc != null) 77 | { 78 | // filteredObjects = await dbSet.ToListAsync(); 79 | if (entities == null) 80 | { 81 | entities = await dbSet.Where(m => !m.IsDeleted).ToListAsync(); 82 | } 83 | 84 | IEnumerable items = entities.Where(o => filterFunc(o, parameters)); 85 | totalItems = items.Count(); 86 | entities = items.Skip(size * (page - 1)).Take(size).ToList(); 87 | } 88 | else 89 | { 90 | totalItems = await dbSet.Where(m => !m.IsDeleted).LongCountAsync(); 91 | if (entities == null) 92 | { 93 | entities = await dbSet.Where(m => !m.IsDeleted).Skip(size * (page - 1)).Take(size).ToListAsync(); 94 | } 95 | else 96 | { 97 | entities = entities.Skip(size * (page - 1)).Take(size).ToList(); 98 | } 99 | } 100 | 101 | return new OperationResultDto, long>>(true, (int)HttpStatusCode.OK, null, 102 | new Tuple, long>(entities.Select(e => createFunc!=null ? createFunc(e) : _defaultCreateFunc(e)).ToList(), totalItems)); 103 | } 104 | catch (Exception e) 105 | { 106 | _logger.LogError($"An error: {e.Message} occurred during collection of object of type: {typeof(TObj)} retrieve and convert to objects of type: {typeof(TRes)}"); 107 | return new OperationResultDto, long>>(true, (int)HttpStatusCode.InternalServerError, "Error occurred, contact system maintainer", 108 | new Tuple, long>(null, 0)); 109 | } 110 | } 111 | 112 | /// 113 | /// GetOneAsync method used 4 getting ONE object by id from Database using EF. Optionally could be used different createFunc, but in most 114 | /// cases (99.(9) percents) this method is identical to GetByIdAsync. 115 | /// 116 | /// item identifier 117 | /// Function that describes how to construct DTO from Model, if null passes here then uses _defaultCreateFunc 118 | /// 119 | public virtual async Task> GetOneAsync(TId id, Func createFunc = null) 120 | { 121 | try 122 | { 123 | DbSet dbSet = _dbContext.Set(); 124 | TObj entity = await dbSet.FirstOrDefaultAsync(i => i.Id.Equals(id)); 125 | if (entity == null || entity.IsDeleted) 126 | return new OperationResultDto(false, (int)HttpStatusCode.NotFound, 127 | ResponseMessageBuilder.GetResourceNotFoundMessage(typeof(TObj).ToString(), id), null); 128 | return new OperationResultDto(true, (int)HttpStatusCode.OK, null, 129 | createFunc != null?createFunc(entity): _defaultCreateFunc(entity)); 130 | } 131 | catch (Exception e) 132 | { 133 | _logger.LogError($"An error: {e.Message} occurred during object of type: {typeof(TObj)} with id: {id} retrieve and convert to object of type: {typeof(TRes)}"); 134 | return new OperationResultDto(false, (int)HttpStatusCode.NotFound, 135 | ResponseMessageBuilder.GetResourceNotFoundMessage(typeof(TObj).ToString(), id), null); 136 | } 137 | } 138 | 139 | /// 140 | /// GetAsync return portion of DTO unlike GetMany methods have not a default sorting && filtering . Default implementation 141 | /// of IModelManager for get data portion via EF. 142 | /// 143 | /// page number starting from 1 144 | /// size of data portion 145 | /// sorting params (Sort - Field name, Order - Sort direction (ASC, DESC)) 146 | /// raw query parameters 147 | /// OperationResult with data portion 148 | public virtual async Task, long>>> GetAsync(int page, int size, SortOption sorting = null, 149 | IDictionary parameters = null) 150 | { 151 | // this method is using default sorting and order, if specific order or sorting is required please specify it using another GetAsync method 152 | Func sortingFunc = null; 153 | if (sorting != null) 154 | { 155 | PropertyInfo[] modelProperties = typeof(TObj).GetProperties(); 156 | MethodInfo prop = modelProperties.FirstOrDefault(p => string.Equals(p.Name.ToLower(), sorting.Sort.ToLower()))?.GetGetMethod(); 157 | if (prop != null) 158 | { 159 | sortingFunc = o => prop.Invoke(o, null); 160 | } 161 | } 162 | 163 | return await GetManyAsync(page, size, parameters, sorting, _filterFunc, sortingFunc); 164 | } 165 | 166 | /// 167 | /// GetByIdAsync returns one item by id, IModelManager default implementation 168 | /// 169 | /// item identifier 170 | /// OperationResult with one item 171 | public async Task> GetByIdAsync(TId id) 172 | { 173 | return await GetOneAsync(id); 174 | } 175 | 176 | /// 177 | /// Method for create new object in database using Ef, in this class still have not a default impl, but will be 178 | /// 179 | /// DTO with Model representation 180 | /// DTO of newly created object 181 | /// 182 | public virtual Task> CreateAsync(TRes data) 183 | { 184 | throw new NotImplementedException(); 185 | } 186 | 187 | /// 188 | /// Method for create new objects in database using Ef, in this class still have not a default impl, but will be 189 | /// 190 | /// Array of DTO with Model representation 191 | /// Array of DTO of a newly created objects 192 | /// 193 | public virtual Task> BulkCreateAsync(TRes[] data) 194 | { 195 | throw new NotImplementedException(); 196 | } 197 | 198 | /// 199 | /// Method for update existing objects using EF, still have not default impl, but will be 200 | /// 201 | /// item identifier 202 | /// >DTO with Model representation 203 | /// DTO of updated object 204 | /// 205 | public virtual Task> UpdateAsync(TId id, TRes data) 206 | { 207 | throw new NotImplementedException(); 208 | } 209 | 210 | /// 211 | /// Method for update existing objects in a database using Ef, in this class still have not a default impl, but will be 212 | /// 213 | /// Array of DTO with Model representation 214 | /// Array of DTO of a updated objects 215 | /// 216 | public virtual Task> BulkUpdateAsync(TRes[] data) 217 | { 218 | throw new NotImplementedException(); 219 | } 220 | 221 | /// 222 | /// DeleteAsync method for remove object from Database using Ef 223 | /// 224 | /// item identifier 225 | /// true if removal was successful, otherwise false 226 | public async Task> DeleteAsync(TId id) 227 | { 228 | try 229 | { 230 | DbSet dbSet = _dbContext.Set(); 231 | TObj item = await dbSet.FirstOrDefaultAsync(t => t.Id.Equals(id)); 232 | 233 | if (item == null || item.IsDeleted) 234 | return new OperationResultDto(false, (int)HttpStatusCode.NotFound, "Item does not exists", false); 235 | item.IsDeleted = true; 236 | await _dbContext.SaveChangesAsync(); 237 | return new OperationResultDto(true, (int)HttpStatusCode.NoContent, null, true); 238 | } 239 | catch (Exception e) 240 | { 241 | _logger.LogError($"An error occurred during object of type: {nameof(TObj)} with id: {id} remove: {e.Message}"); 242 | return new OperationResultDto(false, (int)HttpStatusCode.InternalServerError, "Error occurred during object delete, contact system maintainer", false); 243 | } 244 | } 245 | 246 | /// 247 | /// BulkDeleteAsync method for remove object from Database using Ef 248 | /// 249 | /// item identifiers 250 | /// true if removal was successful, otherwise false 251 | public virtual Task> BulkDeleteAsync(TId[] objectsIds) 252 | { 253 | throw new NotImplementedException(); 254 | } 255 | 256 | private readonly ILogger> _logger; 257 | private readonly DbContext _dbContext; 258 | private readonly Func _defaultCreateFunc; 259 | private readonly Func, bool> _filterFunc; 260 | } 261 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Managers/Helpers/ResponseMessageBuilder.cs: -------------------------------------------------------------------------------- 1 | using Wissance.WebApiToolkit.Globals; 2 | 3 | namespace Wissance.WebApiToolkit.Managers.Helpers 4 | { 5 | public static class ResponseMessageBuilder 6 | { 7 | /// 8 | /// Method for getting Create Failure reason message using entity and reason 9 | /// 10 | /// Entity type/table 11 | /// Exception message 12 | /// Formatted text with object creation error 13 | public static string GetCreateFailureMessage(string entity, string exceptionMessage) 14 | { 15 | return string.Format(MessageCatalog.CreateFailureMessageTemplate, entity, exceptionMessage); 16 | } 17 | 18 | /// 19 | /// Method for getting Resource Not Found Message (Get Method) 20 | /// 21 | /// Resource = entity/table 22 | /// item identifier 23 | /// 24 | public static string GetResourceNotFoundMessage(string resource, TId id) 25 | { 26 | return string.Format(MessageCatalog.ResourceNotFoundTemplate, resource, id); 27 | } 28 | 29 | /// 30 | /// Method for getting Update Failure reason message using entity and reason 31 | /// 32 | /// Entity type/table 33 | /// Item identifier 34 | /// Exception method 35 | /// 36 | public static string GetUpdateFailureMessage(string entity, int id, string exceptionMessage) 37 | { 38 | return string.Format(MessageCatalog.UpdateFailureMessageTemplate, entity, id, exceptionMessage); 39 | } 40 | 41 | /// 42 | /// Method for getting Resource Not Found Message (Update Method) 43 | /// 44 | /// Entity type/table 45 | /// Item identifier 46 | /// 47 | public static string GetUpdateNotFoundMessage(string entity, int id) 48 | { 49 | return string.Format(MessageCatalog.UpdateFailureNotFoundMessageTemplate, entity, id); 50 | } 51 | 52 | /// 53 | /// Method for getting User Has No Access to Resource Message 54 | /// 55 | /// Resource = entity/table 56 | /// 57 | public static string GetCurrentUserResourceAccessErrorMessage(string resource) 58 | { 59 | return string.Format(MessageCatalog.CurrentUserIsNotResourceOwnerTemplate, resource); 60 | } 61 | 62 | /// 63 | /// Method for getting Unknown Error Message 64 | /// 65 | /// Operation type = Create, Update, Read or Delete 66 | /// Resource = entity/table 67 | /// 68 | public static string GetUnknownErrorMessage(string operation, string resource) 69 | { 70 | return string.Format(MessageCatalog.UnknownErrorMessageTemplate, operation, resource); 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Managers/IModelManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Wissance.WebApiToolkit.Data; 5 | using Wissance.WebApiToolkit.Dto; 6 | 7 | namespace Wissance.WebApiToolkit.Managers 8 | { 9 | /// 10 | /// This is a common interface to perform CRUD and some other common operation over PERSISTENCE Data. 11 | /// General convention are: 12 | /// 1. Get, Update and Create operates with the SAME DTO 13 | /// 2. All models (TData) MUST implement IModelIdentifiable Generic interface 14 | /// 3. All operations are wrapped inside OperationResultDto which contains additional information 15 | /// about operation success and message is something goes wrong. 16 | /// 17 | /// TRes is a Result parameter which used as input and output for operation (DTO) 18 | /// Model type (Class that is mapping to PERSISTENT storage) 19 | /// Type of identifier, because IModelIdentifiable is a GENERIC 20 | public interface IModelManager 21 | { 22 | /// 23 | /// Creates a new item in persistent storage (i.e. Database). To assign DTO to Model you should create a custom 24 | /// ModelManager either deriving from default implementation or you own 25 | /// 26 | /// DTO that contains data 4 creation. This data should be assigned to new model object 27 | /// A result of operation with DTO of created object 28 | Task> CreateAsync(TRes data); 29 | /// 30 | /// Creates a bunch of new items in persistent storage (i.e. Database). To assign DTO to Model you should create a custom 31 | /// ModelManager either deriving from default implementation or you own 32 | /// 33 | /// DTO that contains data 4 creation. This data should be assigned to new model object 34 | /// A result of operation with a DTO with Array of created objects 35 | Task> BulkCreateAsync(TRes[] data); 36 | /// 37 | /// Updates existing item that could be found in persistent storage (i.e. database) by id. This method impl 38 | /// should include update of only required field and probably couldn't be fully generalized 39 | /// 40 | /// Identifier of object in persistent storage 41 | /// DTO containing representation ob object in other systems or frontend 42 | /// A result of operation with DTO of updated object 43 | Task> UpdateAsync(TId id, TRes data); 44 | /// 45 | /// Updates existing items that could be found in persistent storage (i.e. database) by id. Id should be passed 46 | /// in object class This method impl should include update of only required field and probably couldn't be fully 47 | /// generalized () 48 | /// 49 | /// DTO containing representation ob object in other systems or frontend 50 | /// A result of operation with DTO of updated object 51 | Task> BulkUpdateAsync(TRes[] data); 52 | /// 53 | /// Removes object from persistent storage by id 54 | /// 55 | /// identifier of object that should be removed 56 | /// true if object was removed successfully, otherwise - false 57 | Task> DeleteAsync(TId id); 58 | /// 59 | /// Removes objects from persistent storage by their identifiers 60 | /// 61 | /// identifiers of objects that should be removed 62 | /// true if objects were removed successfully, otherwise - false 63 | Task> BulkDeleteAsync(TId[] objectsIds); 64 | /// 65 | /// Return a set of DTO objects representation with paging 66 | /// 67 | /// number of page, starting from 1 68 | /// size of data potion (size of IList) 69 | /// sorting params (Sort - Field name, Order - sort direction (ASC, DESC) ) 70 | /// raw query parameters 71 | /// 72 | Task, long>>> GetAsync(int page, int size, SortOption sorting = null, 73 | IDictionary parameters = null); 74 | /// 75 | /// Return DTO representation of 1 object 76 | /// 77 | /// 78 | /// 79 | Task> GetByIdAsync(TId id); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Services/IResourceBasedCrudService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Wissance.WebApiToolkit.Data; 3 | using Wissance.WebApiToolkit.Data.Entity; 4 | using Wissance.WebApiToolkit.Dto; 5 | 6 | namespace Wissance.WebApiToolkit.Services 7 | { 8 | /// 9 | /// This is a Resource based CRUD Service interface to interact in a Resource-oriented way 10 | /// via GRPC, could be used also for others Frameworks && Protocols like SignalR && WCF. This interface should be 11 | /// implemented in some real service classes. Examples will be here: https://github.com/Wissance/WeatherControl . Contains methods 12 | /// for Create, Update and Delete TData item respectively. 13 | /// 14 | /// TRes (Resource) means Representation of Persistent data in external system i.e. DTO 15 | /// Persistent item type, in terms of Web App it is a Table or some ORM Entity Class 16 | /// Unique Identifier type (could be different for different apps i.e int/string/Guid) 17 | /// Filter class 18 | public interface IResourceBasedCrudService : IResourceBasedReadOnlyService 19 | where TRes: class 20 | where TData: IModelIdentifiable 21 | where TFilter: class, IReadFilterable 22 | { 23 | Task> CreateAsync(TRes data); 24 | Task> UpdateAsync(TId id, TRes data); 25 | Task> DeleteAsync(TId id); 26 | } 27 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Services/IResourceBasedReadOnlyService.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Threading.Tasks; 3 | using Wissance.WebApiToolkit.Data; 4 | using Wissance.WebApiToolkit.Data.Entity; 5 | using Wissance.WebApiToolkit.Dto; 6 | 7 | namespace Wissance.WebApiToolkit.Services 8 | { 9 | /// 10 | /// This is a general RO-interface for object Reading. Mainly this interface is using for interact in a Resource-oriented way 11 | /// via GRPC, could be used also for others Frameworks && Protocols like SignalR && WCF. This interface should be 12 | /// implemented in some real service classes. Examples will be here: https://github.com/Wissance/WeatherControl 13 | /// 14 | /// TRes (Resource) means Representation of Persistent data in external system i.e. DTO 15 | /// Persistent item type, in terms of Web App it is a Table or some ORM Entity Class 16 | /// Unique Identifier type (could be different for different apps i.e int/string/Guid) 17 | /// Filter class 18 | public interface IResourceBasedReadOnlyService 19 | where TRes: class 20 | where TData: IModelIdentifiable 21 | where TFilter: class, IReadFilterable 22 | { 23 | Task>> ReadAsync(int? page, int? size, string sort, string order, TFilter filterParams); 24 | Task> ReadByIdAsync(TId id); 25 | } 26 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Services/ResourceBasedDataManageableCrudService.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Wissance.WebApiToolkit.Data; 3 | using Wissance.WebApiToolkit.Data.Entity; 4 | using Wissance.WebApiToolkit.Dto; 5 | using Wissance.WebApiToolkit.Managers; 6 | 7 | namespace Wissance.WebApiToolkit.Services 8 | { 9 | /// 10 | /// This is a full CRUD service implementation based on usage IModelManager as a service class for accessing 11 | /// persistent storage. This class has both Read operations (from ResourceBasedDataManageableReadOnlyService) 12 | /// and Create, Update and Delete operations 13 | /// 14 | /// TRes (Resource) means Representation of Persistent data in external system i.e. DTO 15 | /// Persistent item type, in terms of Web App it is a Table or some ORM Entity Class 16 | /// Unique Identifier type (could be different for different apps i.e int/string/Guid) 17 | /// Filter class 18 | public class ResourceBasedDataManageableCrudService : ResourceBasedDataManageableReadOnlyService, 19 | IResourceBasedCrudService 20 | where TRes: class 21 | where TData: IModelIdentifiable 22 | where TFilter: class, IReadFilterable 23 | { 24 | public ResourceBasedDataManageableCrudService(IModelManager manager) 25 | :base(manager) 26 | { 27 | } 28 | 29 | public async Task> CreateAsync(TRes data) 30 | { 31 | OperationResultDto result = await Manager.CreateAsync(data); 32 | return result; 33 | } 34 | 35 | public async Task> UpdateAsync(TId id, TRes data) 36 | { 37 | OperationResultDto result = await Manager.UpdateAsync(id, data); 38 | return result; 39 | } 40 | 41 | public async Task> DeleteAsync(TId id) 42 | { 43 | OperationResultDto result = await Manager.DeleteAsync(id); 44 | return result; 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Services/ResourceBasedDataManageableReadOnlyService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Wissance.WebApiToolkit.Data; 5 | using Wissance.WebApiToolkit.Data.Entity; 6 | using Wissance.WebApiToolkit.Dto; 7 | using Wissance.WebApiToolkit.Managers; 8 | using Wissance.WebApiToolkit.Utils; 9 | 10 | namespace Wissance.WebApiToolkit.Services 11 | { 12 | /// 13 | /// This is an implementation of IResourceBasedReadOnlyService based on usage IModelManager as a service class for accessing 14 | /// persistent storage. This class has 2 Operations: 15 | /// 1. Read portion (page) of TData via Manager and Transform it to TRes inside Manager (see i.e. EfModelManager, EfSoftRemovableModelManager) 16 | /// 2. Read single item from Manager and return it TRes representation 17 | /// This service class ia analog of a REST BasicReadController 18 | /// 19 | /// TRes (Resource) means Representation of Persistent data in external system i.e. DTO 20 | /// Persistent item type, in terms of Web App it is a Table or some ORM Entity Class 21 | /// Unique Identifier type (could be different for different apps i.e int/string/Guid) 22 | /// Filter class 23 | public class ResourceBasedDataManageableReadOnlyService : IResourceBasedReadOnlyService 24 | where TRes: class 25 | where TData: IModelIdentifiable 26 | where TFilter: class, IReadFilterable 27 | { 28 | public ResourceBasedDataManageableReadOnlyService(IModelManager manager) 29 | { 30 | Manager = manager; 31 | } 32 | 33 | public virtual async Task>> ReadAsync(int? page, int? size, string sort, string order, 34 | TFilter filterParams) 35 | { 36 | int pageNumber = PagingUtils.GetPage(page); 37 | int pageSize = PagingUtils.GetPageSize(size); 38 | SortOption sorting = !string.IsNullOrEmpty(sort) ? new SortOption(sort, order) : null; 39 | OperationResultDto, long>> result = await Manager.GetAsync(pageNumber, pageSize, sorting, filterParams.SelectFilters()); 40 | return new OperationResultDto>(result.Success, result.Status, result.Message, 41 | new PagedDataDto(pageNumber, result.Data.Item2, PagingUtils.GetTotalPages(result.Data.Item2, pageSize), 42 | result.Data.Item1)); 43 | } 44 | 45 | public virtual async Task> ReadByIdAsync(TId id) 46 | { 47 | OperationResultDto result = await Manager.GetByIdAsync(id); 48 | return result; 49 | } 50 | 51 | public IModelManager Manager { get; set; } 52 | } 53 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Utils/Extractors/ValueExtractor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.EntityFrameworkCore.Diagnostics; 3 | 4 | namespace Wissance.WebApiToolkit.Utils.Extractors 5 | { 6 | /// 7 | /// ValueExtractor is a static class that helps to extract variables of a specific type when we deal with query string. 8 | /// 9 | public static class ValueExtractor 10 | { 11 | /// 12 | /// Get scalar type value of type T as a Tuple of T and bool (represents success of data extract) from string 13 | /// 14 | /// string representation of variable of type T 15 | /// type (scalar) of variable in a string 16 | /// Tuple with variable of type T (default(T) if conversion failed) and a bool as a success of 17 | public static Tuple TryGetVal(string value) 18 | { 19 | try 20 | { 21 | Type tType = typeof(T); 22 | T typedValue = (T)Convert.ChangeType(value, tType); 23 | return new Tuple(typedValue, true); 24 | } 25 | catch (Exception) 26 | { 27 | return new Tuple(default(T), false); 28 | } 29 | 30 | } 31 | 32 | /// 33 | /// Get vector type value of type T[] as a Tuple of T[] and bool (represents success of data extract) from string. Internally calls 34 | /// scalar version of this func 35 | /// 36 | /// string representation of array of variables of type T 37 | /// type of variable in a string 38 | /// Tuple with array of T (T[]) and a bool (parsing result) 39 | public static Tuple TryGetArray(string value) 40 | { 41 | string[] parts = value.Split(","); 42 | T[] values = new T[parts.Length]; 43 | for (int i=0; i < values.Length; i++) 44 | { 45 | Tuple item = TryGetVal(parts[i]); 46 | if (!item.Item2) 47 | return new Tuple(null, false); 48 | values[i] = item.Item1; 49 | } 50 | 51 | return new Tuple(values, true); 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Utils/PagingUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Wissance.WebApiToolkit.Utils 4 | { 5 | public static class PagingUtils 6 | { 7 | public static long GetTotalPages(long totalItems, int pageSize) 8 | { 9 | if (pageSize <= 0) 10 | { 11 | // todo(UMV): this is hardly ever possible but add logging here for jokers 12 | return -1; 13 | } 14 | 15 | return (long)Math.Ceiling((double)totalItems / pageSize); 16 | } 17 | 18 | public static int GetPage(int? page) 19 | { 20 | int selectedPage = page ?? DefaultPage; 21 | return selectedPage < 1 ? 1 : selectedPage; 22 | } 23 | 24 | public static int GetPageSize(int? size) 25 | { 26 | int selectedSize = size ?? DefaultSize; 27 | return selectedSize <= 0 ? DefaultSize : selectedSize; 28 | } 29 | 30 | private const int DefaultPage = 1; 31 | private const int DefaultSize = 25; 32 | } 33 | } -------------------------------------------------------------------------------- /Wissance.WebApiToolkit/Wissance.WebApiToolkit/Wissance.WebApiToolkit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0;net6.0;netcoreapp3.1;net8.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-architect -------------------------------------------------------------------------------- /img/bulk_performance.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wissance/WebApiToolkit/bfa30311a319c19de7c980fd36170165e5918273/img/bulk_performance.png -------------------------------------------------------------------------------- /img/cover.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wissance/WebApiToolkit/bfa30311a319c19de7c980fd36170165e5918273/img/cover.jpg -------------------------------------------------------------------------------- /img/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wissance/WebApiToolkit/bfa30311a319c19de7c980fd36170165e5918273/img/cover.png -------------------------------------------------------------------------------- /img/v1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wissance/WebApiToolkit/bfa30311a319c19de7c980fd36170165e5918273/img/v1.jpg --------------------------------------------------------------------------------