├── .gitattributes ├── .github ├── FUNDING.yml ├── dependabot.yml └── workflows │ └── dotnet-desktop.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── OA.sln ├── README.md ├── _config.yml ├── code_of_conduct.md ├── docs ├── PPT │ ├── Onion Architecture.pptx │ └── Unit test.pptx ├── UML Script │ ├── CQRS.drawio │ ├── DP_AP.drawio │ ├── OnionArchitecture.drawio │ └── UML Diagram.nomnoml └── img │ ├── CQRS.png │ ├── OnionArchitecture.png │ ├── OnionArchitecture_icon.png │ ├── Step.png │ ├── Step1.png │ ├── Step2.png │ ├── Step3.png │ ├── Step4.png │ ├── Step5.png │ ├── Step6.png │ └── classDiagram.png └── src ├── .template.config ├── template.json └── template.vstemplate ├── OA.Domain ├── Auth │ ├── ApplicationUser.cs │ ├── AuthenticationRequest.cs │ ├── AuthenticationResponse.cs │ ├── ForgotPasswordRequest.cs │ ├── RefreshToken.cs │ ├── RegisterRequest.cs │ └── ResetPasswordRequest.cs ├── BaseEntity.cs ├── Common │ ├── IpHelper.cs │ └── Response.cs ├── Entities │ ├── Category.cs │ ├── Customer.cs │ ├── Order.cs │ ├── OrderDetail.cs │ ├── Product.cs │ └── Supplier.cs ├── Enum │ ├── FeatureManagement.cs │ └── Roles.cs ├── OA.Domain.csproj └── Settings │ ├── AppSettings.cs │ ├── ApplicationDetail.cs │ ├── JWTSettings.cs │ ├── MailRequest.cs │ └── MailSettings.cs ├── OA.Infrastructure ├── Extension │ ├── ConfigureContainer.cs │ └── ConfigureServiceContainer.cs ├── Mapping │ └── CustomerProfile.cs ├── OA.Infrastructure.csproj └── ViewModel │ └── CustomerModel.cs ├── OA.Persistence ├── ApplicationDbContext.cs ├── ApplicationDbContext.dgml ├── IApplicationDbContext.cs ├── IdentityContext.cs ├── OA.Persistence.csproj └── Seeds │ ├── ContextSeed.cs │ ├── DefaultRoles.cs │ ├── DefaultUser.cs │ └── MappingUserRole.cs ├── OA.Service ├── Contract │ ├── IAccountService.cs │ ├── IAuthenticatedUserService.cs │ ├── IDateTimeService.cs │ └── IEmailService.cs ├── DependencyInjection.cs ├── Exceptions │ ├── ApiException.cs │ ├── BadRequestException.cs │ ├── DeleteFailureException.cs │ ├── NotFoundException.cs │ └── ValidationException.cs ├── Features │ └── CustomerFeatures │ │ ├── Commands │ │ ├── CreateCustomerCommand.cs │ │ ├── DeleteCustomerByIdCommand.cs │ │ └── UpdateCustomerCommand.cs │ │ └── Queries │ │ ├── GetAllCustomerQuery.cs │ │ └── GetCustomerByIdQuery.cs ├── Implementation │ ├── AccountService.cs │ ├── DateTimeService.cs │ └── MailService.cs ├── Middleware │ └── CustomExceptionMiddleware.cs └── OA.Service.csproj ├── OA.Test.Integration ├── ApiEndpoints │ └── ApiCustomerTest.cs ├── HttpClientAuthExtensions.cs ├── HttpClientExtensions.cs ├── OA.Test.Integration.csproj └── TestClientProvider.cs ├── OA.Test.Unit ├── OA.Test.Unit.csproj └── Persistence │ └── ApplicationDbContextTest.cs ├── OA.sln ├── OA ├── Controllers │ ├── AccountController.cs │ ├── CustomerController.cs │ ├── MailController.cs │ └── MetaController.cs ├── Customization │ └── custom.css ├── OA.csproj ├── Program.cs ├── Properties │ └── launchSettings.json ├── appsettings.Development.json ├── appsettings.Test.json └── appsettings.json └── OATemplate ├── LICENSE.txt ├── OATemplate.csproj ├── OnionArchitecture_icon.png ├── Properties ├── AssemblyInfo.cs ├── project-icon.png └── wafflebuilder.targets ├── Resources └── OnionArchitecture_icon.png ├── index.html ├── source.extension.vsixmanifest ├── stylesheet.css ├── template.pkgdef └── template └── templatepack.OATemplate.proj /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: ['https://www.buymeacoffee.com/codewithamit',"https://www.instamojo.com/@amitpnk"] 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/src" 5 | schedule: 6 | interval: weekly 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-desktop.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core Desktop 2 | 3 | on: 4 | push: 5 | branches: [ "develop" ] 6 | pull_request: 7 | branches: [ "develop" ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | strategy: 14 | matrix: 15 | configuration: [Debug, Release] 16 | 17 | runs-on: windows-latest 18 | 19 | env: 20 | Solution_Name: OA.sln 21 | Test_Project_Path: ./src 22 | 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v4 26 | with: 27 | fetch-depth: 0 28 | 29 | # Install the .NET Core workload 30 | - name: Install .NET Core 31 | uses: actions/setup-dotnet@v4 32 | with: 33 | dotnet-version: 8.0.x 34 | 35 | # Add MSBuild to the PATH: https://github.com/microsoft/setup-msbuild 36 | - name: Setup MSBuild.exe 37 | uses: microsoft/setup-msbuild@v2 38 | 39 | # Execute all unit tests in the solution 40 | - name: Execute unit tests 41 | run: dotnet test 42 | 43 | # Restore the application to populate the obj folder with RuntimeIdentifiers 44 | - name: Restore the application 45 | run: msbuild $env:Solution_Name /t:Restore /p:Configuration=$env:Configuration 46 | env: 47 | Configuration: ${{ matrix.configuration }} -------------------------------------------------------------------------------- /.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 | # JustCode is a .NET coding add-in 131 | .JustCode 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | healthchecksdb* 349 | 350 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 351 | MigrationBackup/ 352 | 353 | # Ionide (cross platform F# VS Code tools) working folder 354 | .ionide/ 355 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | Version will be updated as it is released to Microsoft Marketplace 6 | 7 | ## Version 1.3 8 | 9 | ### Added API Versioning 10 | 11 | * Added API versioning 12 | * Fixed bug related to update 13 | 14 | ## Version 1.2 15 | 16 | ### Fixed bug 17 | 18 | * Fixed bug 19 | 20 | ## Version 1.0 21 | 22 | ### Code refactored 23 | 24 | * Removed data layer and refactored 25 | 26 | ## Version 0.6 27 | 28 | ### Added feature 29 | 30 | * Swagger 31 | * CQRS Pattern 32 | * Loggings - seriLog 33 | 34 | ## Version 0.5 35 | 36 | ### Added feature 37 | 38 | * Email feature 39 | 40 | ## Version 0.2 - 0.4 41 | 42 | ### Fixed issue related to Project template 43 | 44 | * Project template 45 | 46 | ## Version 0.1 - 26-Jun-2020 47 | 48 | ### Initial release 49 | 50 | * Application is implemented on Onion architecture 51 | * API 52 | * Entityframework Core 53 | * Expection handling 54 | * Automapper 55 | * Unit testing via NUnit 56 | * Integration testing via NUnit 57 | 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Amit P Naik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /OA.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA", "src\OA\OA.csproj", "{5CE8523B-CD37-4F3E-9F41-9A3D2DAB3D39}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Domain", "src\OA.Domain\OA.Domain.csproj", "{74D8BF98-D40C-447E-BB40-29B1BAA363AB}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Infrastructure", "src\OA.Infrastructure\OA.Infrastructure.csproj", "{97A14F11-44A9-443C-ADC4-CF5696BC64F7}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Persistence", "src\OA.Persistence\OA.Persistence.csproj", "{5F6B0320-95CE-4D4C-82D6-7A0972243716}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Service", "src\OA.Service\OA.Service.csproj", "{B20723E2-C6FC-41B2-8807-FC1E52B012F0}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Test.Unit", "src\OA.Test.Unit\OA.Test.Unit.csproj", "{FDDC1E0E-296B-448C-90C2-9364B118F5E2}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Test.Integration", "src\OA.Test.Integration\OA.Test.Integration.csproj", "{4150259A-1CC5-4BB2-A0D4-891F56338028}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {5CE8523B-CD37-4F3E-9F41-9A3D2DAB3D39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {5CE8523B-CD37-4F3E-9F41-9A3D2DAB3D39}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {5CE8523B-CD37-4F3E-9F41-9A3D2DAB3D39}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {5CE8523B-CD37-4F3E-9F41-9A3D2DAB3D39}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {74D8BF98-D40C-447E-BB40-29B1BAA363AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {74D8BF98-D40C-447E-BB40-29B1BAA363AB}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {74D8BF98-D40C-447E-BB40-29B1BAA363AB}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {74D8BF98-D40C-447E-BB40-29B1BAA363AB}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {97A14F11-44A9-443C-ADC4-CF5696BC64F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {97A14F11-44A9-443C-ADC4-CF5696BC64F7}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {97A14F11-44A9-443C-ADC4-CF5696BC64F7}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {97A14F11-44A9-443C-ADC4-CF5696BC64F7}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {5F6B0320-95CE-4D4C-82D6-7A0972243716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {5F6B0320-95CE-4D4C-82D6-7A0972243716}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {5F6B0320-95CE-4D4C-82D6-7A0972243716}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {5F6B0320-95CE-4D4C-82D6-7A0972243716}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {B20723E2-C6FC-41B2-8807-FC1E52B012F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {B20723E2-C6FC-41B2-8807-FC1E52B012F0}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {B20723E2-C6FC-41B2-8807-FC1E52B012F0}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {B20723E2-C6FC-41B2-8807-FC1E52B012F0}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {FDDC1E0E-296B-448C-90C2-9364B118F5E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {FDDC1E0E-296B-448C-90C2-9364B118F5E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {FDDC1E0E-296B-448C-90C2-9364B118F5E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {FDDC1E0E-296B-448C-90C2-9364B118F5E2}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {4150259A-1CC5-4BB2-A0D4-891F56338028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {4150259A-1CC5-4BB2-A0D4-891F56338028}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {4150259A-1CC5-4BB2-A0D4-891F56338028}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {4150259A-1CC5-4BB2-A0D4-891F56338028}.Release|Any CPU.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(ExtensibilityGlobals) = postSolution 59 | SolutionGuid = {5E5A61BE-464E-48CF-88FA-7982CFBAED9E} 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Visual Studio Marketplace version](https://vsmarketplacebadges.dev/version/AmitNaik.OnionArchitecture.svg?&colorB=magenta)](https://marketplace.visualstudio.com/items?itemName=AmitNaik.OnionArchitecture) 2 | [![Visual Studio Marketplace downloads](https://vsmarketplacebadges.dev/downloads/AmitNaik.OnionArchitecture.svg?&colorB=blue)](https://marketplace.visualstudio.com/items?itemName=AmitNaik.OnionArchitecture) 3 | [![Visual Studio Marketplace ratings](https://vsmarketplacebadges.dev/rating/AmitNaik.OnionArchitecture.svg?&colorB=darkgreen)](https://marketplace.visualstudio.com/items?itemName=AmitNaik.OnionArchitecture) 4 | 5 | ---- 6 | 7 | ![.NET Core](https://github.com/Amitpnk/Onion-architecture-ASP.NET-Core/workflows/.NET%20Core%20Desktop/badge.svg) 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Amitpnk/Onion-architecture-ASP.NET-Core/blob/develop/LICENSE) 9 | 10 | 17 | ---- 18 | 19 | 20 | # WhiteApp/QuickApp Onion architecture with ASP.NET Core 21 | 22 |
23 |

24 | 25 | Logo 26 | 27 | 28 |

Onion Architecture

29 | 30 |

31 | WhiteApp or QuickApp API solution template which is built on Onion Architecture with all essential feature using .NET Core! 32 |
33 | Explore the docs » 34 |
35 |
36 | Download from Marketplace 37 | · 38 | Report Bug 39 | · 40 | Request Feature 41 |

42 |

43 | 44 | ## Give a Star! :star: 45 | If you like or are using this project to learn or start your solution, please give it a star. Thanks! 46 | 47 | ## Support This Project 48 | 49 | If you have found this project helpful, either as a library that you use or as a learning tool, please consider buying me a coffee: 50 | 51 | Buy Me A Coffee 52 | 53 | 54 | 55 | ## Table of Contents 56 | 57 | * [Onion Architecture](#Onion-Architecture) 58 | * [reference](#reference) 59 | * [About the Project](#about-the-project) 60 | 61 | * [Getting Started](#getting-started) 62 | * [Features available in this project](#Features-available-in-this-project) 63 | * [Project description](#project-description) 64 | * [Design patterns Used](#roadmap) 65 | * [Contributing](#contributing) 66 | * [Licence Used](#Licence-Used) 67 | * [Contact](#contact) 68 | * [Support This Project](#Support-This-Project) 69 | 70 | 71 | ## Onion Architecture 72 | 73 | Onion Architecture was introduced by Jeffrey Palermo to provide a better way to build applications in perspective of better testability, maintainability, and dependability on the infrastructures like databases and services 74 | 75 | Onion, Clean or Hexagonal architecture: it's all the same. Which is built on Domain-Driven Desgin approach. 76 | 77 | Domain in center and building layer top of it. You can call it as Domain-centric Architecture too. 78 | 79 | ### Reference 80 | 81 | * [It's all the same (Domain centeric architecture) - Mark Seemann](https://blog.ploeh.dk/2013/12/03/layers-onions-ports-adapters-its-all-the-same/) 82 | * [Onion Architecture by Jeffrey Palermo](https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/) 83 | * [Clean Architecture by Robert C. Martin (Uncle Bob) 84 | ](https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html) 85 | * [Hexagonal Architecture by Dr. Alistair Cockburn](https://alistair.cockburn.us/hexagonal+architecture) 86 | 87 | ## About The Project 88 | 89 | 90 | 91 | WhiteApp or QuickApp API solution template which is built on Onion Architecture with all essential feature using .NET Core. 92 | 93 | ![image](docs/img/OnionArchitecture.png) 94 | 95 | ## Getting Started 96 | 97 | ### Step 1: Download extension from project template 98 | 99 |

Download from Marketplace

100 | 101 | ![image](docs/img/Step.png) 102 | 103 | ### Step 2: Create Project 104 | 105 | Select project type as API, and select Onion Architecture 106 | 107 | ![image](docs/img/Step1.png) 108 | 109 | ### Step 3: Select Onion Architecture project template 110 | 111 | Select project type as API, and select Onion Architecture 112 | 113 | ![image](docs/img/Step2.png) 114 | 115 | ### Step 4: Project is ready 116 | 117 | ![image](docs/img/Step3.png) 118 | 119 | ### Step 5: Configure connection string in appsettings.json 120 | 121 | Make sure to connect proper database 122 | 123 | ```json 124 | "ConnectionStrings": { 125 | "OnionArchConn": "Data Source=(local)\\sqlexpress01;Initial Catalog=OnionDb;Integrated Security=True", 126 | "IdentityConnection": "Data Source=(local)\\sqlexpress01;Initial Catalog=OnionDb;Integrated Security=True" 127 | }, 128 | ``` 129 | 130 | and connect to logging in DB or proer path 131 | 132 | ```diff 133 | "Serilog": { 134 | "MinimumLevel": "Information", 135 | "WriteTo": [ 136 | { 137 | "Name": "RollingFile", 138 | "Args": { 139 | ++ "pathFormat": "D:\\Logs\\log-{Date}.log", 140 | "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}" 141 | } 142 | }, 143 | { 144 | "Name": "MSSqlServer", 145 | "Args": { 146 | ++ "connectionString": "Data Source=(local)\\sqlexpress01;Initial Catalog=OnionDb3;Integrated Security=True", 147 | "sinkOptionsSection": { 148 | "tableName": "Logs", 149 | "schemaName": "EventLogging", 150 | "autoCreateSqlTable": true 151 | }, 152 | "restrictedToMinimumLevel": "Warning" 153 | } 154 | } 155 | ], 156 | "Properties": { 157 | "Application": "Onion Architecture application" 158 | } 159 | }, 160 | ``` 161 | 162 | ### Step 6: Create Database (Sample is for Microsoft SQL Server) 163 | 164 | For Code First approach (To run this application, use Code First apporach) 165 | 166 | - For running migration: 167 | 168 | + Option 1: Using Package Manager Console: 169 | + Open Package Manager Console, select *<< ProjectName >>.Persistence* as Default Project 170 | + Run these commands: 171 | ```sh 172 | PM> add-migration Initial-commit-Application -Context ApplicationDbContext -o Migrations/Application 173 | PM> add-migration Identity-commit -Context IdentityContext -o Migrations/Identity 174 | 175 | PM> update-database -Context ApplicationDbContext 176 | PM> update-database -Context IdentityContext 177 | ``` 178 | 179 | ![Migration](docs/img/Step4.png) 180 | 181 | + Option 2: Using dotnet cli: 182 | + Install **dotnet-ef** cli: 183 | ``` 184 | dotnet tool install --global dotnet-ef --version="3.1" 185 | ``` 186 | + Navigate to [OA](https://github.com/Amitpnk/Onion-architecture-ASP.NET-Core/tree/develop/src/OA/) and run these commands: 187 | ``` 188 | $ dotnet ef migrations add Initial-commit-Application --context ApplicationDbContext -o Migrations/Application 189 | $ dotnet ef migrations add Identity-commit-Identity --context IdentityContext -o Migrations/Identity 190 | $ dotnet ef database update --context ApplicationDbContext 191 | $ dotnet ef database update --context IdentityContext 192 | ``` 193 | 194 | For Database First approach 195 | 196 | In Package Manager console in *<< ProjectName >>.Persistence*, run below command 197 | 198 | ```sh 199 | scaffold-dbcontext -provider Microsoft.EntityFrameworkCore.SqlServer -connection "Data Source=(local)\SQLexpress;Initial Catalog=OnionArchitectureDB;Integrated Security=True" 200 | ``` 201 | 202 | ### Step 7: Build and run application 203 | 204 | #### Health check UI 205 | 206 | Navigate to Health Checks UI https://localhost:44356/healthcheck-ui and make sure everything is green. 207 | 208 | ** Change port number according to your application 209 | 210 | ![image](docs/img/Step6.png) 211 | 212 | #### Swagger UI 213 | 214 | Swagger UI https://localhost:44356/OpenAPI/index.html 215 | 216 | ** Change port number according to your application 217 | 218 | ![image](docs/img/Step5.png) 219 | 220 | ## Features available in this project 221 | 222 | This is default white application for ASP.NET Core API development 223 | 224 | This whiteapp contains following features, uncheck feature need to implement yet. 225 | 226 | - [x] Application is implemented on Onion architecture 227 | - [x] RESTful API 228 | - [x] Entityframework Core 229 | - [x] Expection handling 230 | - [x] Automapper 231 | - [x] Unit testing via NUnit 232 | - [x] Integration testing via NUnit 233 | - [x] Versioning 234 | - [x] Swagger UI 235 | - [x] CQRS Pattern 236 | 237 | Below features will be implemented in infrastructure layer. You can plug and play based on your project. 238 | 239 | - [x] Loggings - seriLog 240 | - [x] Email 241 | - [x] Health checks UI 242 | - [x] JWT authentication with Microsoft Identity 243 | - [x] Role based Authorization 244 | - [x] Fluent validations 245 | - [x] Database Seeding 246 | - [x] Enable CORS origin 247 | - [x] Enable feature flag (Make it true when you configure your email configuration) 248 | 249 | 250 | ## Project description 251 | 252 | we can see that all the Layers are dependent only on the Core Layers 253 | 254 |
255 | Domain layer 256 |

257 | Domain Layers (Core layer) is implemented in center and never depends on any other layer. Therefore, what we do is that we create interfaces to Persistence layer and these interfaces get implemented in the external layers. This is also known and DIP or Dependency Inversion Principle 258 |

259 |
260 |
261 | Persistence layer 262 |

263 | In Persistence layer where we implement reposistory design pattern. In our project, we have implement Entityframework which already implements a repository design pattern. DbContext will be UoW (Unit of Work) and each DbSet is the repository. This interacts with our database using dataproviders 264 |

265 |
266 |
267 | Service layer 268 |

269 | Service layer (or also called as Application layer) where we can implement business logic. For OLAP/OLTP process, we can implement CQRS design pattern. In our project, we have implemented CQRS design pattern on top of Mediator design pattern via MediatR libraries 270 |

271 |

In case you want to implement email feature logic, we define an IMailService in the Service Layer. 272 | Using DIP, it is easily possible to switch the implementations. This helps build scalable applications. 273 |

274 |
275 |
276 | Infrastructure Layer 277 |

278 | In this layer, we add our third party libraries like JWT Tokens Authentication or Serilog for logging, etc. so that all the third libraries will be in one place. In our project, we have implemented almost all important libraries, you can plug & play (add/remove) based on your project requirement in StartUp.cs file. 279 |

280 |
281 |
282 | Presentation Layer 283 |

284 | This can be WebApi or UI. 285 |

286 |
287 | 288 | ## Licence Used 289 | 290 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Amitpnk/Onion-architecture-ASP.NET-Core/blob/develop/LICENSE) 291 | 292 | See the contents of the LICENSE file for details 293 | 294 | ## Contact 295 | 296 | Having any issues or troubles getting started? Drop a mail to amit.naik8103@gmail.com or [Raise a Bug or Feature Request](https://github.com/Amitpnk/Onion-architecture-ASP.NET-Core/issues/new). Always happy to help. 297 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-tactile -------------------------------------------------------------------------------- /code_of_conduct.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | In the interest of fostering an open and welcoming environment, we as 7 | contributors and maintainers pledge to make participation in our project and 8 | our community a harassment-free experience for everyone, regardless of age, body 9 | size, disability, ethnicity, sex characteristics, gender identity and expression, 10 | level of experience, education, socio-economic status, nationality, personal 11 | appearance, race, religion, or sexual identity and orientation. 12 | 13 | ## Our Standards 14 | 15 | Examples of behavior that contributes to creating a positive environment 16 | include: 17 | 18 | * Using welcoming and inclusive language 19 | * Being respectful of differing viewpoints and experiences 20 | * Gracefully accepting constructive criticism 21 | * Focusing on what is best for the community 22 | * Showing empathy towards other community members 23 | 24 | Examples of unacceptable behavior by participants include: 25 | 26 | * The use of sexualized language or imagery and unwelcome sexual attention or 27 | advances 28 | * Trolling, insulting/derogatory comments, and personal or political attacks 29 | * Public or private harassment 30 | * Publishing others' private information, such as a physical or electronic 31 | address, without explicit permission 32 | * Other conduct which could reasonably be considered inappropriate in a 33 | professional setting 34 | 35 | ## Our Responsibilities 36 | 37 | Project maintainers are responsible for clarifying the standards of acceptable 38 | behavior and are expected to take appropriate and fair corrective action in 39 | response to any instances of unacceptable behavior. 40 | 41 | Project maintainers have the right and responsibility to remove, edit, or 42 | reject comments, commits, code, wiki edits, issues, and other contributions 43 | that are not aligned to this Code of Conduct, or to ban temporarily or 44 | permanently any contributor for other behaviors that they deem inappropriate, 45 | threatening, offensive, or harmful. 46 | 47 | ## Scope 48 | 49 | This Code of Conduct applies within all project spaces, and it also applies when 50 | an individual is representing the project or its community in public spaces. 51 | Examples of representing a project or community include using an official 52 | project e-mail address, posting via an official social media account, or acting 53 | as an appointed representative at an online or offline event. Representation of 54 | a project may be further defined and clarified by project maintainers. 55 | 56 | ## Enforcement 57 | 58 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 59 | reported by contacting the project team at [amit.naik8103@gmail.com](amit.naik8103@gmail.com). All 60 | complaints will be reviewed and investigated and will result in a response that 61 | is deemed necessary and appropriate to the circumstances. The project team is 62 | obligated to maintain confidentiality with regard to the reporter of an incident. 63 | Further details of specific enforcement policies may be posted separately. 64 | 65 | Project maintainers who do not follow or enforce the Code of Conduct in good 66 | faith may face temporary or permanent repercussions as determined by other 67 | members of the project's leadership. 68 | 69 | ## Attribution 70 | 71 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 72 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 73 | 74 | [homepage]: https://www.contributor-covenant.org 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq 78 | 79 | -------------------------------------------------------------------------------- /docs/PPT/Onion Architecture.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/PPT/Onion Architecture.pptx -------------------------------------------------------------------------------- /docs/PPT/Unit test.pptx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/PPT/Unit test.pptx -------------------------------------------------------------------------------- /docs/UML Script/CQRS.drawio: -------------------------------------------------------------------------------- 1 | 7Vrbdps4FP0aP8aLO/gxsdPpQzqTWemsto8yyKApIEaSY7tfPxJI3LFNzNirnTgPQUdHF7S3NucIZuYy2f9GQBZ9wgGMZ4YW7GfmamYYuqtZ/J+wHArLwvMKQ0hQIJ0qwwv6AaVRk9YtCiBtODKMY4ayptHHaQp91rABQvCu6bbBcXPUDISwY3jxQdy1fkEBiwqrZ2uV/SNEYaRG1jVZkwDlLA00AgHe1Uzm48xcEoxZcZXslzAWi6fWpWj3YaC2nBiBKTungZX+/pHs/vr8+YcLP339sqMPa+1OovMK4q28YTlZdlArAAO+ILKICYtwiFMQP1bWB4K3aQDFMBovVT5PGGfcqHPj35Cxg0QXbBnmpoglsawtxhQDDd6bwh6QELIjN2SWK8spCXECGTnwdgTGgKHXZv9AciMs/cqmzxjxkQ1N8thSeEsW66bW7ILiLfGhbFWBcE8IONTcMuFAR4xjtzAd588vihmoUm1NKlPOkxGcMXs448QclIcNzidXkcf5Z4tVxR3N4b/nDqaW7atKfhWK/88EUo43xwmnqkc+waLTwqVDzop6gke7CDH4kgFf1O64IDVptkFxvMQxJnlbc7OBju9zO2UEf4e1msBdrDWtHO8VEgb3x6nZpVwlgU2IPFneVXJiK9GIalLitChWZ2kN4PH4LX6yPV/srSM3ZJ+pDd6F2nDRqtsX7xrD7ts1S5wkIA3or7NjbKO5Y4xFd8coobvOjvEGsYvMYeR2cmoCuxSTBIhx8iXS8voNSFB8KOp5U5BkeaVpWmKFYYghr9iiTlXuvuxrBAgSgwz5U5DSOwoJ2pQTiVEK76LaRPW51eXYaKZavfoOCUWUwZSTTc0xXdOsNkCHvKVZLPQvw+hS72/GaPed0RNo759bPjSk/zM2tyOa2+uz3vdwvX5Iw9ePHL6K9nNbFb/V61Z72XlROsjShKGQbpwZC7kXxkL9+QhPSBrUsNp5UjGvTp40VWqibv9d196f1JNom63fXNucM7QtDe7FWRcv+TGgFPktYdojVtMlXvqmhIhfV6okCkqUamKmW94YOctLnEKILwAk02uce6bEDRwFNfLuLpDKdumJkdYkkuW1CDJwYnTyiOfqktoXKo7l38CDUTvBpPNJc5IM9i3JYC+aj0XOjreRwV5Y81ZX9nXpoG5+ejnSj8rRhPoxcAx0GyrY7lt1wWnqgqtdmQh9odZEujBdwHxSF4qI8VZsMN0TIJ7NBr31lFhcmQ19LwWG2ZDiVGRcAaBRGQ7WeCHsz4Dx6CHNLYZmloGfegtnXxAinmbOzcIGfdHA0TnzRdPJjjoB6n9NiL4z05Hy0ATc+AngMxfNbai/NerrdHTlqM/oe0k0LrXUvb7UcgUYWAMKz31dQSOQiUv/wBPgQKQTp/LGdZFkPq1LA/C/h3nq+ceWiTS6Qa+aSjj5T95JzV78unqzehR/0ySepmu0IjtH7dgadVXwEDU/ORim6UWpp+LcsT2MiyVdlp9faHVdFwWxLMgH8RNYw/gZU5S/3zVXa8wYTmoO9zEKRQUTx20PQJZ8voA57HVhkKRI9qH46mQOdtScb2nu1sbIWJkrgR33DBCscJWPoCmg87y524rKu9A5Jb518FxHWUegx4vVZyPFjq8+vjEf/wU= -------------------------------------------------------------------------------- /docs/UML Script/DP_AP.drawio: -------------------------------------------------------------------------------- 1 | 7ZlLU9swEMc/TWbogRm/kxxJwuMAUwZaOCvyxlZRLCPLefDpK9lyEluG0iEhdFwOwfqvJMv721W0Ts8dz1eXHKXxDQuB9hwrXPXcSc9xBr4jP5WwLgXfsksh4iQspR3hnryAFi2t5iSErNZRMEYFSesiZkkCWNQ0xDlb1rvNGK3fNUURGMI9RtRUH0ko4uqxrK1+BSSKqzvblrbMUdVZC1mMQrbckdzznjvmjInyar4aA1W+q/xSjrt4xbpZGIdEvGfA6vL6Cn/H4ePP56sXn019NJyc6lkWiOb6gfVixbrywDImAu5ThFV7KSH33FEs5lS2bHmJsrT0+4ysQN5qNCOUjhllvBjuzmYQYCz1THD2BDuWsD+cSm+5I70G4AJWrz6cvXGZDDVgcxB8LbvoAUEFRIeZPfTL9nILza5IxLvAKhHpQIk2c299KS+0O//Ctc47fAuhDDbdZFzELGIJoudbdSSjJlXWGYXVmYpnqXGWJ6Hy9UR5bzvumrFUU/kFQqx1NqFcsDqzGUuENtqeiSz0YRB6bcgGztQNAmmJOAqJZFMDrf42ONWjvQ1TeoLlHMNbPtQZj3gE4k9hbAYHB4oEWdTXsX/Qh8+hAYb2HJoOfM/fUw55gf/lcsj6n0MfzSHvnTnkHDOHvIPnENgSSb8NyDDouyjYTw45Xy+HghbXBlToEK75OHjOWWU4zYrgPpMd7H662hrlVaT+TyAjUVJ4RwjgSTWrXGQ5cdnN4CidK+ro6kQSlkADn5YQVTd0J1jCAKmPFCoiz2pn2jAnYVhkfFus1DN+H2eOYf3M4Xgma68F9cFI9w9E+oxj5U8scg4d5u1b/tfiPfgc3uXXqJ55yregO8bfbfKvao5j8R8a/Mey/JYKhYWqwr8kiOa5aR8bcb9R/PmeAWbQAsY7FBjbPLaOKMNP3UPjDRrnId/cMz8XjVmV33KSYJKqPa4rWJwmlsDcyj4Xi1kAXCAsmJp6bH733EHKMlKarclta5/u0Gzuf65z7CTzDZo3D+OCknXz8HDTHTTN/e/4aMxy8O78/sdFXgzNgC8I7tJGOGzwcY+9EZpF3MljjNRpfs1ytTCUiG//CKCirRfp7INW8+2KbdJqfbtyOFxmDXZyxZYbWmQuzxVz6C4yr/nDTBsy/1ORmWVTDVnG6KJ4zcHZVLLrKrjAfUeuVfn4QXCyuf0dtLDt/Jjsnv8G -------------------------------------------------------------------------------- /docs/UML Script/OnionArchitecture.drawio: -------------------------------------------------------------------------------- 1 | 7Vldb5swFP01eUwUMJDksU26dVKnRUqlSXsz2IBbwMyYhvTXz4D5NFG7LSxRupcWH9sX+5x7fS/OBKzD7DODsf+VIhxM9DnKJmAz0fWlqYu/OXAoAWNllIDHCCohrQF25BVLcC7RlCCcdAZySgNO4i7o0CjCDu9gkDG67w5zadB9aww9rAA7BwYq+p0g7lfbmjf4PSaeX71Zm8ueEFaDJZD4ENF9CwJ3E7BmlPLyKczWOMi5q3gp53060lsvjOGIv2fCLvtx/2D5z+nT43dr+rp9QmkytUorLzBI5YblYvmhYkBYEWSLxu3eJxzvYujkPXsht8B8HgaipYlHmMSlAi7JsHjprUuCYE0DygpDwHWx5TgCTzijz7jVgxYrW/AGbuVqMOM4O7pNrSZPOB2mIebsIIZUE+aLmVlOki5Xt/eNgIYpMb8lXg1C6TRebb3hVTxIan+DZnB9NC+smd7juUFaTAOt5r/NdQs+Odva1bEtTpyeU2sN0iJb14BKdQ2enGj9+ohezrs0V+0WyVqVm/zuwT+WN8/fwXKEbvJkJ1oRjXCX2XI0Rkqi65EiMitkHuZvpQuVvBY55gA3FcZwADl56S5jiC/5hi0lYoG1NsDoamNYev8ISWjKHCzntXNh39SqZ0pTTJVcKKYKCeut/4Wq78i8uScTUYo8QBsHW5oQTmgkumzKOQ3zUKhKitz7EUz8PHCKBgyIlw91hMSYdf2hMnsjx3Aa12FV1ThaaT3OVxJmXl7azULqPKfxLIQs/xeQws+ORyMiTAR0ueKIstzs0Zg+QeSCXjYCKzBbDJyQA/5pNYnr9NG7uHadR9RUreX0gQpjoQ8VGPpo5/Hyv6J/rKjRO3gvQc+VoueGhpBEiqpi27wrSJdDmXrb9ElIEbWvZEgQCo5VSYymEaodxKURl9/Jmnmis1N7O8qMgYNzNE10tebZYpaQhONIMPNhhdFMtRz9t8Ko31Y7zF7IRxZldW5N1M+wLUvEZmCRdT6sLuDcuqi3Pl8il0FBd+rwlImQsYJcCpt1FLJ+pvmdYEHRNCk4uhEDNDPOCqKqfvHk8Zq6i1OYUemAYDM1zHEkN8xuOQGsc2tuKJo/4oRfvkRjKTRwTwjOnsRMRSRFoKqEdg6iXEYF0W9cYNkltQ92DUDn2SsI/5bysugu8F55jSBeuoMXW5azxLZ7oout/oWHqoE1oMFyNA3U+48N5NCGyaVWEqfIUD0RDDBWIIhm88tSeWfV/DwH7n4B -------------------------------------------------------------------------------- /docs/UML Script/UML Diagram.nomnoml: -------------------------------------------------------------------------------- 1 | [API] --> [Service] 2 | [API] --> [Persistence] 3 | [Service] --:> [Persistence] 4 | [Persistence] --:> [Data] 5 | [Data] --:> [Domain] -------------------------------------------------------------------------------- /docs/img/CQRS.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/CQRS.png -------------------------------------------------------------------------------- /docs/img/OnionArchitecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/OnionArchitecture.png -------------------------------------------------------------------------------- /docs/img/OnionArchitecture_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/OnionArchitecture_icon.png -------------------------------------------------------------------------------- /docs/img/Step.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/Step.png -------------------------------------------------------------------------------- /docs/img/Step1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/Step1.png -------------------------------------------------------------------------------- /docs/img/Step2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/Step2.png -------------------------------------------------------------------------------- /docs/img/Step3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/Step3.png -------------------------------------------------------------------------------- /docs/img/Step4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/Step4.png -------------------------------------------------------------------------------- /docs/img/Step5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/Step5.png -------------------------------------------------------------------------------- /docs/img/Step6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/Step6.png -------------------------------------------------------------------------------- /docs/img/classDiagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/docs/img/classDiagram.png -------------------------------------------------------------------------------- /src/.template.config/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Amit Naik", 3 | "classifications": [], 4 | "description": "WhiteApp or QuickApp API solution template which is built on Domain-Driven Design (DDD)-based with all essential feature using .NET Core", 5 | "name": "Onion Architecture", 6 | "defaultName": "OA", 7 | "identity": "OnionArchitecture.CSharp", 8 | "groupIdentity": "OnionArchitecture", 9 | "tags": { 10 | "language": "C#", 11 | "type": "project" 12 | }, 13 | "shortName": "OnionArchitecture", 14 | "sourceName": "OA", 15 | "guids": [], 16 | "primaryOutputs": [ 17 | { 18 | "path": "OA\\OA.csproj" 19 | }, 20 | { 21 | "path": "OA.Domain\\OA.Domain.csproj" 22 | }, 23 | { 24 | "path": "OA.Infrastructure\\OA.Infrastructure.csproj" 25 | }, 26 | { 27 | "path": "OA.Persistence\\OA.Persistence.csproj" 28 | }, 29 | { 30 | "path": "OA.Service\\OA.Service.csproj" 31 | }, 32 | { 33 | "path": "OA.Test.Integration\\OA.Test.Integration.csproj" 34 | }, 35 | { 36 | "path": "OA.Test.Unit\\OA.Test.Unit.csproj" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /src/.template.config/template.vstemplate: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Onion Architecture 5 | WhiteApp or QuickApp API solution template which is built on Onion Architecture using .NET Core 6 | OnionArchitecture.CSharp 7 | OnionArchitecture 8 | 9 | project-icon.png 10 | 11 | CSharp 12 | 1 13 | 5000 14 | true 15 | true 16 | Enabled 17 | true 18 | C# 19 | windows 20 | API 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Microsoft.VisualStudio.TemplateEngine.Wizard, Version=1.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a 33 | Microsoft.VisualStudio.TemplateEngine.Wizard.TemplateEngineWizard 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/OA.Domain/Auth/ApplicationUser.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | 3 | namespace OA.Domain.Auth; 4 | 5 | public class ApplicationUser : IdentityUser 6 | { 7 | public string FirstName { get; set; } 8 | public string LastName { get; set; } 9 | public List RefreshTokens { get; set; } 10 | public bool OwnsToken(string token) 11 | { 12 | return this.RefreshTokens?.Find(x => x.Token == token) != null; 13 | } 14 | } -------------------------------------------------------------------------------- /src/OA.Domain/Auth/AuthenticationRequest.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Auth; 2 | 3 | public class AuthenticationRequest 4 | { 5 | public string Email { get; set; } 6 | public string Password { get; set; } 7 | } -------------------------------------------------------------------------------- /src/OA.Domain/Auth/AuthenticationResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace OA.Domain.Auth; 4 | 5 | public class AuthenticationResponse 6 | { 7 | public string Id { get; set; } 8 | public string UserName { get; set; } 9 | public string Email { get; set; } 10 | public List Roles { get; set; } 11 | public bool IsVerified { get; set; } 12 | public string JWToken { get; set; } 13 | [JsonIgnore] 14 | public string RefreshToken { get; set; } 15 | } -------------------------------------------------------------------------------- /src/OA.Domain/Auth/ForgotPasswordRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OA.Domain.Auth; 4 | 5 | public class ForgotPasswordRequest 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } 10 | } -------------------------------------------------------------------------------- /src/OA.Domain/Auth/RefreshToken.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Auth; 2 | 3 | public class RefreshToken 4 | { 5 | public int Id { get; set; } 6 | public string Token { get; set; } 7 | public DateTime Expires { get; set; } 8 | public bool IsExpired => DateTime.UtcNow >= Expires; 9 | public DateTime Created { get; set; } 10 | public string CreatedByIp { get; set; } 11 | public DateTime? Revoked { get; set; } 12 | public string RevokedByIp { get; set; } 13 | public string ReplacedByToken { get; set; } 14 | public bool IsActive => Revoked == null && !IsExpired; 15 | } -------------------------------------------------------------------------------- /src/OA.Domain/Auth/RegisterRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OA.Domain.Auth; 4 | 5 | public class RegisterRequest 6 | { 7 | [Required] 8 | public string FirstName { get; set; } 9 | 10 | [Required] 11 | public string LastName { get; set; } 12 | 13 | [Required] 14 | [EmailAddress] 15 | public string Email { get; set; } 16 | [Required] 17 | [MinLength(6)] 18 | public string UserName { get; set; } 19 | 20 | [Required] 21 | [MinLength(6)] 22 | public string Password { get; set; } 23 | 24 | [Required] 25 | [Compare("Password")] 26 | public string ConfirmPassword { get; set; } 27 | } -------------------------------------------------------------------------------- /src/OA.Domain/Auth/ResetPasswordRequest.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OA.Domain.Auth; 4 | 5 | public class ResetPasswordRequest 6 | { 7 | [Required] 8 | [EmailAddress] 9 | public string Email { get; set; } 10 | [Required] 11 | public string Token { get; set; } 12 | [Required] 13 | [MinLength(6)] 14 | public string Password { get; set; } 15 | 16 | [Required] 17 | [Compare("Password")] 18 | public string ConfirmPassword { get; set; } 19 | } -------------------------------------------------------------------------------- /src/OA.Domain/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace OA.Domain; 4 | 5 | public class BaseEntity 6 | { 7 | [Key] 8 | public int Id { get; set; } 9 | } -------------------------------------------------------------------------------- /src/OA.Domain/Common/IpHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.Sockets; 3 | 4 | namespace OA.Domain.Common; 5 | 6 | public class IpHelper 7 | { 8 | public static string GetIpAddress() 9 | { 10 | var host = Dns.GetHostEntry(Dns.GetHostName()); 11 | foreach (var ip in host.AddressList) 12 | { 13 | if (ip.AddressFamily == AddressFamily.InterNetwork) 14 | { 15 | return ip.ToString(); 16 | } 17 | } 18 | return string.Empty; 19 | } 20 | } -------------------------------------------------------------------------------- /src/OA.Domain/Common/Response.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Common; 2 | 3 | public class Response 4 | { 5 | public Response() 6 | { 7 | } 8 | public Response(T data, string message = null) 9 | { 10 | Succeeded = true; 11 | Message = message; 12 | Data = data; 13 | } 14 | public Response(string message) 15 | { 16 | Succeeded = false; 17 | Message = message; 18 | } 19 | public bool Succeeded { get; set; } 20 | public string Message { get; set; } 21 | public List Errors { get; set; } 22 | public T Data { get; set; } 23 | } -------------------------------------------------------------------------------- /src/OA.Domain/Entities/Category.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Entities; 2 | 3 | public class Category : BaseEntity 4 | { 5 | public string CategoryName { get; set; } 6 | public string Description { get; set; } 7 | public List Products { get; set; } 8 | } -------------------------------------------------------------------------------- /src/OA.Domain/Entities/Customer.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Entities; 2 | 3 | public class Customer : BaseEntity 4 | { 5 | public string CustomerName { get; set; } 6 | public string ContactName { get; set; } 7 | public string ContactTitle { get; set; } 8 | public string Address { get; set; } 9 | public string City { get; set; } 10 | public string Region { get; set; } 11 | public string PostalCode { get; set; } 12 | public string Country { get; set; } 13 | public string Phone { get; set; } 14 | public string Fax { get; set; } 15 | public List Orders { get; set; } 16 | } -------------------------------------------------------------------------------- /src/OA.Domain/Entities/Order.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Entities; 2 | 3 | public class Order : BaseEntity 4 | { 5 | public Customer Customers { get; set; } 6 | public int CustomerId { get; set; } 7 | public int EmployeeId { get; set; } 8 | public DateTime OrderDate { get; set; } 9 | public DateTime RequiredDate { get; set; } 10 | public List OrderDetails { get; set; } 11 | 12 | } -------------------------------------------------------------------------------- /src/OA.Domain/Entities/OrderDetail.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Entities; 2 | 3 | public class OrderDetail 4 | { 5 | public int OrderId { get; set; } 6 | public int ProductId { get; set; } 7 | public Order Orders { get; set; } 8 | public Product Product { get; set; } 9 | } -------------------------------------------------------------------------------- /src/OA.Domain/Entities/Product.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations.Schema; 2 | 3 | namespace OA.Domain.Entities; 4 | 5 | public class Product : BaseEntity 6 | { 7 | public string ProductName { get; set; } 8 | 9 | [Column(TypeName = "money")] 10 | public decimal UnitPrice { get; set; } 11 | 12 | } -------------------------------------------------------------------------------- /src/OA.Domain/Entities/Supplier.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Entities; 2 | 3 | public class Supplier : BaseEntity 4 | { 5 | public string SupplierName { get; set; } 6 | public List Products { get; set; } 7 | } -------------------------------------------------------------------------------- /src/OA.Domain/Enum/FeatureManagement.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Enum; 2 | 3 | public enum FeatureManagement 4 | { 5 | EnableEmailService 6 | } -------------------------------------------------------------------------------- /src/OA.Domain/Enum/Roles.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Enum; 2 | 3 | public enum Roles 4 | { 5 | SuperAdmin, 6 | Admin, 7 | Moderator, 8 | Basic 9 | } 10 | 11 | public static class Constants 12 | { 13 | public static readonly string SuperAdmin = Guid.NewGuid().ToString(); 14 | public static readonly string Admin = Guid.NewGuid().ToString(); 15 | public static readonly string Moderator = Guid.NewGuid().ToString(); 16 | public static readonly string Basic = Guid.NewGuid().ToString(); 17 | 18 | public static readonly string SuperAdminUser = Guid.NewGuid().ToString(); 19 | public static readonly string BasicUser = Guid.NewGuid().ToString(); 20 | } -------------------------------------------------------------------------------- /src/OA.Domain/OA.Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/OA.Domain/Settings/AppSettings.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Settings; 2 | 3 | public class AppSettings 4 | { 5 | public ApplicationDetail ApplicationDetail { get; set; } 6 | } -------------------------------------------------------------------------------- /src/OA.Domain/Settings/ApplicationDetail.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Settings; 2 | 3 | public class ApplicationDetail 4 | { 5 | public string ApplicationName { get; set; } 6 | public string Description { get; set; } 7 | public string ContactWebsite { get; set; } 8 | public string LicenseDetail { get; set; } 9 | } -------------------------------------------------------------------------------- /src/OA.Domain/Settings/JWTSettings.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Settings; 2 | 3 | public class JWTSettings 4 | { 5 | public string Key { get; set; } 6 | public string Issuer { get; set; } 7 | public string Audience { get; set; } 8 | public double DurationInMinutes { get; set; } 9 | } -------------------------------------------------------------------------------- /src/OA.Domain/Settings/MailRequest.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Settings; 2 | 3 | public class MailRequest 4 | { 5 | public string ToEmail { get; set; } 6 | public string Subject { get; set; } 7 | public string Body { get; set; } 8 | public string From { get; set; } 9 | 10 | } -------------------------------------------------------------------------------- /src/OA.Domain/Settings/MailSettings.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Domain.Settings; 2 | 3 | public class MailSettings 4 | { 5 | public string EmailFrom { get; set; } 6 | public string SmtpHost { get; set; } 7 | public int SmtpPort { get; set; } 8 | public string SmtpUser { get; set; } 9 | public string SmtpPass { get; set; } 10 | public string DisplayName { get; set; } 11 | } -------------------------------------------------------------------------------- /src/OA.Infrastructure/Extension/ConfigureContainer.cs: -------------------------------------------------------------------------------- 1 | using HealthChecks.UI.Client; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Diagnostics.HealthChecks; 6 | using Microsoft.Extensions.Logging; 7 | using OA.Service.Middleware; 8 | using Serilog; 9 | 10 | namespace OA.Infrastructure.Extension; 11 | 12 | public static class ConfigureContainer 13 | { 14 | public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app) 15 | { 16 | app.UseMiddleware(); 17 | } 18 | 19 | public static void ConfigureSwagger(this IApplicationBuilder app) 20 | { 21 | app.UseSwagger(); 22 | 23 | app.UseSwaggerUI(setupAction => 24 | { 25 | setupAction.SwaggerEndpoint("/swagger/OpenAPISpecification/swagger.json", "Onion Architecture API"); 26 | setupAction.RoutePrefix = "OpenAPI"; 27 | }); 28 | } 29 | 30 | public static void ConfigureSwagger(this ILoggerFactory loggerFactory) 31 | { 32 | loggerFactory.AddSerilog(); 33 | } 34 | 35 | public static void UseHealthCheck(this IApplicationBuilder app) 36 | { 37 | app.UseHealthChecks("/healthz", new HealthCheckOptions 38 | { 39 | Predicate = _ => true, 40 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, 41 | ResultStatusCodes = 42 | { 43 | [HealthStatus.Healthy] = StatusCodes.Status200OK, 44 | [HealthStatus.Degraded] = StatusCodes.Status500InternalServerError, 45 | [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable, 46 | }, 47 | }).UseHealthChecksUI(setup => 48 | { 49 | setup.ApiPath = "/healthcheck"; 50 | setup.UIPath = "/healthcheck-ui"; 51 | }); 52 | } 53 | } -------------------------------------------------------------------------------- /src/OA.Infrastructure/Extension/ConfigureServiceContainer.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Diagnostics.HealthChecks; 7 | using Microsoft.OpenApi.Models; 8 | using OA.Domain.Settings; 9 | using OA.Infrastructure.Mapping; 10 | using OA.Persistence; 11 | using OA.Service.Contract; 12 | using OA.Service.Implementation; 13 | 14 | namespace OA.Infrastructure.Extension; 15 | 16 | public static class ConfigureServiceContainer 17 | { 18 | public static void AddDbContext(this IServiceCollection serviceCollection, IConfiguration configuration) 19 | { 20 | 21 | if (configuration.GetValue("UseInMemoryDatabase")) 22 | { 23 | serviceCollection.AddDbContext(options => 24 | options.UseInMemoryDatabase(nameof(ApplicationDbContext))); 25 | } 26 | else 27 | { 28 | serviceCollection.AddDbContext(options => 29 | options.UseSqlServer(configuration.GetConnectionString("OnionArchConn") 30 | , b => b.MigrationsAssembly(typeof(ApplicationDbContext).Assembly.FullName))); 31 | } 32 | 33 | } 34 | 35 | public static void AddAutoMapper(this IServiceCollection serviceCollection) 36 | { 37 | var mappingConfig = new MapperConfiguration(mc => 38 | { 39 | mc.AddProfile(new CustomerProfile()); 40 | }); 41 | IMapper mapper = mappingConfig.CreateMapper(); 42 | serviceCollection.AddSingleton(mapper); 43 | } 44 | 45 | public static void AddScopedServices(this IServiceCollection serviceCollection) 46 | { 47 | serviceCollection.AddScoped(provider => provider.GetService()); 48 | 49 | 50 | } 51 | public static void AddTransientServices(this IServiceCollection serviceCollection) 52 | { 53 | serviceCollection.AddTransient(); 54 | serviceCollection.AddTransient(); 55 | } 56 | 57 | 58 | 59 | public static void AddSwaggerOpenAPI(this IServiceCollection serviceCollection) 60 | { 61 | serviceCollection.AddSwaggerGen(setupAction => 62 | { 63 | 64 | setupAction.SwaggerDoc( 65 | "OpenAPISpecification", 66 | new OpenApiInfo() 67 | { 68 | Title = "Onion Architecture WebAPI", 69 | Version = "1", 70 | Description = "Through this API you can access customer details", 71 | Contact = new OpenApiContact() 72 | { 73 | Email = "amit.naik8103@gmail.com", 74 | Name = "Amit Naik", 75 | Url = new Uri("https://amitpnk.github.io/") 76 | }, 77 | License = new OpenApiLicense() 78 | { 79 | Name = "MIT License", 80 | Url = new Uri("https://opensource.org/licenses/MIT") 81 | } 82 | }); 83 | 84 | setupAction.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme 85 | { 86 | Type = SecuritySchemeType.Http, 87 | Scheme = "bearer", 88 | BearerFormat = "JWT", 89 | Description = $"Input your Bearer token in this format - Bearer token to access this API", 90 | }); 91 | setupAction.AddSecurityRequirement(new OpenApiSecurityRequirement 92 | { 93 | { 94 | new OpenApiSecurityScheme 95 | { 96 | Reference = new OpenApiReference 97 | { 98 | Type = ReferenceType.SecurityScheme, 99 | Id = "Bearer", 100 | }, 101 | }, new List() 102 | }, 103 | }); 104 | }); 105 | 106 | } 107 | 108 | public static void AddMailSetting(this IServiceCollection serviceCollection, 109 | IConfiguration configuration) 110 | { 111 | serviceCollection.Configure(configuration.GetSection("MailSettings")); 112 | } 113 | 114 | public static void AddController(this IServiceCollection serviceCollection) 115 | { 116 | serviceCollection.AddControllers().AddNewtonsoftJson(); 117 | } 118 | 119 | public static void AddVersion(this IServiceCollection serviceCollection) 120 | { 121 | serviceCollection.AddApiVersioning(config => 122 | { 123 | config.DefaultApiVersion = new ApiVersion(1, 0); 124 | config.AssumeDefaultVersionWhenUnspecified = true; 125 | config.ReportApiVersions = true; 126 | }); 127 | } 128 | 129 | public static void AddHealthCheck(this IServiceCollection serviceCollection, AppSettings appSettings, IConfiguration configuration) 130 | { 131 | serviceCollection.AddHealthChecks() 132 | .AddDbContextCheck(name: "Application DB Context", failureStatus: HealthStatus.Degraded) 133 | .AddUrlGroup(new Uri(appSettings.ApplicationDetail.ContactWebsite), name: "My personal website", failureStatus: HealthStatus.Degraded) 134 | .AddSqlServer(configuration.GetConnectionString("OnionArchConn")); 135 | 136 | serviceCollection.AddHealthChecksUI(setupSettings: setup => 137 | { 138 | setup.AddHealthCheckEndpoint("Basic Health Check", $"/healthz"); 139 | }).AddInMemoryStorage(); 140 | } 141 | 142 | 143 | } -------------------------------------------------------------------------------- /src/OA.Infrastructure/Mapping/CustomerProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using OA.Domain.Entities; 3 | using OA.Infrastructure.ViewModel; 4 | 5 | namespace OA.Infrastructure.Mapping; 6 | 7 | public class CustomerProfile : Profile 8 | { 9 | public CustomerProfile() 10 | { 11 | CreateMap() 12 | .ForMember(dest => dest.Id, 13 | opt => opt.MapFrom(src => src.CustomerId)) 14 | .ReverseMap(); 15 | } 16 | } -------------------------------------------------------------------------------- /src/OA.Infrastructure/OA.Infrastructure.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/OA.Infrastructure/ViewModel/CustomerModel.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Infrastructure.ViewModel; 2 | 3 | public class CustomerModel 4 | { 5 | public int CustomerId { get; set; } 6 | public string CustomerName { get; set; } 7 | public string ContactName { get; set; } 8 | public string ContactTitle { get; set; } 9 | public string Address { get; set; } 10 | public string City { get; set; } 11 | public string Region { get; set; } 12 | public string PostalCode { get; set; } 13 | public string Country { get; set; } 14 | public string Phone { get; set; } 15 | public string Fax { get; set; } 16 | } -------------------------------------------------------------------------------- /src/OA.Persistence/ApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using OA.Domain.Entities; 3 | 4 | namespace OA.Persistence; 5 | 6 | public class ApplicationDbContext : DbContext, IApplicationDbContext 7 | { 8 | // This constructor is used of runit testing 9 | public ApplicationDbContext() 10 | { 11 | 12 | } 13 | public ApplicationDbContext(DbContextOptions options) : base(options) 14 | { 15 | ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking; 16 | } 17 | 18 | public DbSet Customers { get; set; } 19 | public DbSet Orders { get; set; } 20 | public DbSet Products { get; set; } 21 | public DbSet Categories { get; set; } 22 | public DbSet Suppliers { get; set; } 23 | 24 | protected override void OnModelCreating(ModelBuilder modelBuilder) 25 | { 26 | modelBuilder.Entity().HasKey(o => new { o.OrderId, o.ProductId }); 27 | } 28 | 29 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 30 | { 31 | if (!optionsBuilder.IsConfigured) 32 | { 33 | optionsBuilder 34 | .UseSqlServer("DataSource=app.db"); 35 | } 36 | 37 | } 38 | 39 | public async Task SaveChangesAsync() 40 | { 41 | return await base.SaveChangesAsync(); 42 | } 43 | } -------------------------------------------------------------------------------- /src/OA.Persistence/ApplicationDbContext.dgml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 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 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 156 | 160 | 164 | 168 | 172 | 176 | 180 | 184 | 185 | -------------------------------------------------------------------------------- /src/OA.Persistence/IApplicationDbContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using OA.Domain.Entities; 3 | 4 | namespace OA.Persistence; 5 | 6 | public interface IApplicationDbContext 7 | { 8 | DbSet Categories { get; set; } 9 | DbSet Customers { get; set; } 10 | DbSet Orders { get; set; } 11 | DbSet Products { get; set; } 12 | DbSet Suppliers { get; set; } 13 | 14 | Task SaveChangesAsync(); 15 | } -------------------------------------------------------------------------------- /src/OA.Persistence/IdentityContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.Identity.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore; 4 | using OA.Domain.Auth; 5 | using OA.Persistence.Seeds; 6 | 7 | namespace OA.Persistence; 8 | 9 | public class IdentityContext(DbContextOptions options) 10 | : IdentityDbContext(options) 11 | { 12 | 13 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 14 | { 15 | if (!optionsBuilder.IsConfigured) 16 | { 17 | optionsBuilder 18 | .UseSqlServer("DataSource=app.db"); 19 | } 20 | 21 | } 22 | protected override void OnModelCreating(ModelBuilder modelBuilder) 23 | { 24 | base.OnModelCreating(modelBuilder); 25 | modelBuilder.HasDefaultSchema("Identity"); 26 | modelBuilder.Entity(entity => 27 | { 28 | entity.ToTable(name: "User"); 29 | }); 30 | 31 | modelBuilder.Entity(entity => 32 | { 33 | entity.ToTable(name: "Role"); 34 | }); 35 | modelBuilder.Entity>(entity => 36 | { 37 | entity.ToTable("UserRoles"); 38 | }); 39 | 40 | modelBuilder.Entity>(entity => 41 | { 42 | entity.ToTable("UserClaims"); 43 | }); 44 | 45 | modelBuilder.Entity>(entity => 46 | { 47 | entity.ToTable("UserLogins"); 48 | }); 49 | 50 | modelBuilder.Entity>(entity => 51 | { 52 | entity.ToTable("RoleClaims"); 53 | }); 54 | 55 | modelBuilder.Entity>(entity => 56 | { 57 | entity.ToTable("UserTokens"); 58 | }); 59 | 60 | modelBuilder.Seed(); 61 | } 62 | } -------------------------------------------------------------------------------- /src/OA.Persistence/OA.Persistence.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/OA.Persistence/Seeds/ContextSeed.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.EntityFrameworkCore; 3 | using OA.Domain.Auth; 4 | 5 | namespace OA.Persistence.Seeds; 6 | 7 | public static class ContextSeed 8 | { 9 | public static void Seed(this ModelBuilder modelBuilder) 10 | { 11 | CreateRoles(modelBuilder); 12 | 13 | CreateBasicUsers(modelBuilder); 14 | 15 | MapUserRole(modelBuilder); 16 | } 17 | 18 | private static void CreateRoles(ModelBuilder modelBuilder) 19 | { 20 | List roles = DefaultRoles.IdentityRoleList(); 21 | modelBuilder.Entity().HasData(roles); 22 | } 23 | 24 | private static void CreateBasicUsers(ModelBuilder modelBuilder) 25 | { 26 | List users = DefaultUser.IdentityBasicUserList(); 27 | modelBuilder.Entity().HasData(users); 28 | } 29 | 30 | private static void MapUserRole(ModelBuilder modelBuilder) 31 | { 32 | var identityUserRoles = MappingUserRole.IdentityUserRoleList(); 33 | modelBuilder.Entity>().HasData(identityUserRoles); 34 | } 35 | } -------------------------------------------------------------------------------- /src/OA.Persistence/Seeds/DefaultRoles.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using OA.Domain.Enum; 3 | 4 | namespace OA.Persistence.Seeds; 5 | 6 | 7 | public static class DefaultRoles 8 | { 9 | public static List IdentityRoleList() 10 | { 11 | return new List() 12 | { 13 | new IdentityRole 14 | { 15 | Id = Constants.SuperAdmin, 16 | Name = Roles.SuperAdmin.ToString(), 17 | NormalizedName = Roles.SuperAdmin.ToString() 18 | }, 19 | new IdentityRole 20 | { 21 | Id = Constants.Admin, 22 | Name = Roles.Admin.ToString(), 23 | NormalizedName = Roles.Admin.ToString() 24 | }, 25 | new IdentityRole 26 | { 27 | Id = Constants.Moderator, 28 | Name = Roles.Moderator.ToString(), 29 | NormalizedName = Roles.Moderator.ToString() 30 | }, 31 | new IdentityRole 32 | { 33 | Id = Constants.Basic, 34 | Name = Roles.Basic.ToString(), 35 | NormalizedName = Roles.Basic.ToString() 36 | } 37 | }; 38 | } 39 | } -------------------------------------------------------------------------------- /src/OA.Persistence/Seeds/DefaultUser.cs: -------------------------------------------------------------------------------- 1 | using OA.Domain.Auth; 2 | using OA.Domain.Enum; 3 | 4 | namespace OA.Persistence.Seeds; 5 | 6 | public static class DefaultUser 7 | { 8 | public static List IdentityBasicUserList() 9 | { 10 | return new List() 11 | { 12 | new ApplicationUser 13 | { 14 | Id = Constants.SuperAdminUser, 15 | UserName = "superadmin", 16 | Email = "superadmin@gmail.com", 17 | FirstName = "Amit", 18 | LastName = "Naik", 19 | EmailConfirmed = true, 20 | PhoneNumberConfirmed = true, 21 | // Password@123 22 | PasswordHash = "AQAAAAEAACcQAAAAEBLjouNqaeiVWbN0TbXUS3+ChW3d7aQIk/BQEkWBxlrdRRngp14b0BIH0Rp65qD6mA==", 23 | NormalizedEmail= "SUPERADMIN@GMAIL.COM", 24 | NormalizedUserName="SUPERADMIN" 25 | }, 26 | new ApplicationUser 27 | { 28 | Id = Constants.BasicUser, 29 | UserName = "basicuser", 30 | Email = "basicuser@gmail.com", 31 | FirstName = "Basic", 32 | LastName = "User", 33 | EmailConfirmed = true, 34 | PhoneNumberConfirmed = true, 35 | // Password@123 36 | PasswordHash = "AQAAAAEAACcQAAAAEBLjouNqaeiVWbN0TbXUS3+ChW3d7aQIk/BQEkWBxlrdRRngp14b0BIH0Rp65qD6mA==", 37 | NormalizedEmail= "BASICUSER@GMAIL.COM", 38 | NormalizedUserName="BASICUSER" 39 | }, 40 | }; 41 | } 42 | } -------------------------------------------------------------------------------- /src/OA.Persistence/Seeds/MappingUserRole.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using OA.Domain.Enum; 3 | 4 | namespace OA.Persistence.Seeds; 5 | 6 | public static class MappingUserRole 7 | { 8 | public static List> IdentityUserRoleList() 9 | { 10 | return new List>() 11 | { 12 | new IdentityUserRole 13 | { 14 | RoleId = Constants.Basic, 15 | UserId = Constants.BasicUser 16 | }, 17 | new IdentityUserRole 18 | { 19 | RoleId = Constants.SuperAdmin, 20 | UserId = Constants.SuperAdminUser 21 | }, 22 | new IdentityUserRole 23 | { 24 | RoleId = Constants.Admin, 25 | UserId = Constants.SuperAdminUser 26 | }, 27 | new IdentityUserRole 28 | { 29 | RoleId = Constants.Moderator, 30 | UserId = Constants.SuperAdminUser 31 | }, 32 | new IdentityUserRole 33 | { 34 | RoleId = Constants.Basic, 35 | UserId = Constants.SuperAdminUser 36 | } 37 | }; 38 | } 39 | } -------------------------------------------------------------------------------- /src/OA.Service/Contract/IAccountService.cs: -------------------------------------------------------------------------------- 1 | using OA.Domain.Auth; 2 | using OA.Domain.Common; 3 | 4 | namespace OA.Service.Contract; 5 | 6 | public interface IAccountService 7 | { 8 | Task> AuthenticateAsync(AuthenticationRequest request, string ipAddress); 9 | Task> RegisterAsync(RegisterRequest request, string origin); 10 | Task> ConfirmEmailAsync(string userId, string code); 11 | Task ForgotPassword(ForgotPasswordRequest model, string origin); 12 | Task> ResetPassword(ResetPasswordRequest model); 13 | } -------------------------------------------------------------------------------- /src/OA.Service/Contract/IAuthenticatedUserService.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Service.Contract; 2 | 3 | public interface IAuthenticatedUserService 4 | { 5 | string UserId { get; } 6 | } -------------------------------------------------------------------------------- /src/OA.Service/Contract/IDateTimeService.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Service.Contract; 2 | 3 | public interface IDateTimeService 4 | { 5 | DateTime NowUtc { get; } 6 | } -------------------------------------------------------------------------------- /src/OA.Service/Contract/IEmailService.cs: -------------------------------------------------------------------------------- 1 | using OA.Domain.Settings; 2 | 3 | namespace OA.Service.Contract; 4 | 5 | public interface IEmailService 6 | { 7 | Task SendEmailAsync(MailRequest mailRequest); 8 | 9 | } -------------------------------------------------------------------------------- /src/OA.Service/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Authentication.JwtBearer; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Identity; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.IdentityModel.Tokens; 9 | using Newtonsoft.Json; 10 | using OA.Domain.Auth; 11 | using OA.Domain.Common; 12 | using OA.Domain.Settings; 13 | using OA.Persistence; 14 | using OA.Service.Contract; 15 | using OA.Service.Implementation; 16 | using System.Reflection; 17 | using System.Text; 18 | 19 | namespace OA.Service; 20 | 21 | public static class DependencyInjection 22 | { 23 | public static void AddServiceLayer(this IServiceCollection services) 24 | { 25 | // or you can use assembly in Extension method in Infra layer with below command 26 | services.AddMediatR(Assembly.GetExecutingAssembly()); 27 | services.AddTransient(); 28 | } 29 | 30 | public static void AddIdentityService(this IServiceCollection services, IConfiguration configuration) 31 | { 32 | if (configuration.GetValue("UseInMemoryDatabase")) 33 | { 34 | services.AddDbContext(options => 35 | options.UseInMemoryDatabase(nameof(IdentityContext))); 36 | } 37 | else 38 | { 39 | services.AddDbContext(options => 40 | options.UseSqlServer( 41 | configuration.GetConnectionString("IdentityConnection"), 42 | b => b.MigrationsAssembly(typeof(IdentityContext).Assembly.FullName))); 43 | } 44 | 45 | services.AddIdentity().AddEntityFrameworkStores() 46 | .AddDefaultTokenProviders(); 47 | #region Services 48 | services.AddTransient(); 49 | #endregion 50 | services.Configure(configuration.GetSection("JWTSettings")); 51 | services.AddAuthentication(options => 52 | { 53 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 54 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 55 | }) 56 | .AddJwtBearer(o => 57 | { 58 | o.RequireHttpsMetadata = false; 59 | o.SaveToken = false; 60 | o.TokenValidationParameters = new TokenValidationParameters 61 | { 62 | ValidateIssuerSigningKey = true, 63 | ValidateIssuer = true, 64 | ValidateAudience = true, 65 | ValidateLifetime = true, 66 | ClockSkew = TimeSpan.Zero, 67 | ValidIssuer = configuration["JWTSettings:Issuer"], 68 | ValidAudience = configuration["JWTSettings:Audience"], 69 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["JWTSettings:Key"])) 70 | }; 71 | o.Events = new JwtBearerEvents() 72 | { 73 | OnAuthenticationFailed = c => 74 | { 75 | c.NoResult(); 76 | c.Response.StatusCode = 500; 77 | c.Response.ContentType = "text/plain"; 78 | return c.Response.WriteAsync(c.Exception.ToString()); 79 | }, 80 | OnChallenge = context => 81 | { 82 | context.HandleResponse(); 83 | context.Response.StatusCode = 401; 84 | context.Response.ContentType = "application/json"; 85 | var result = JsonConvert.SerializeObject(new Response("You are not Authorized")); 86 | return context.Response.WriteAsync(result); 87 | }, 88 | OnForbidden = context => 89 | { 90 | context.Response.StatusCode = 403; 91 | context.Response.ContentType = "application/json"; 92 | var result = JsonConvert.SerializeObject(new Response("You are not authorized to access this resource")); 93 | return context.Response.WriteAsync(result); 94 | }, 95 | }; 96 | }); 97 | } 98 | } -------------------------------------------------------------------------------- /src/OA.Service/Exceptions/ApiException.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace OA.Service.Exceptions; 4 | 5 | public class ApiException : Exception 6 | { 7 | public ApiException() : base() { } 8 | 9 | public ApiException(string message) : base(message) { } 10 | 11 | public ApiException(string message, params object[] args) 12 | : base(string.Format(CultureInfo.CurrentCulture, message, args)) 13 | { 14 | } 15 | } -------------------------------------------------------------------------------- /src/OA.Service/Exceptions/BadRequestException.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace OA.Service.Exceptions; 4 | 5 | [Serializable] 6 | public class BadRequestException : Exception 7 | { 8 | public BadRequestException(string message) 9 | : base(message) 10 | { 11 | } 12 | 13 | protected BadRequestException(SerializationInfo info, StreamingContext context) 14 | : base(info, context) 15 | { 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /src/OA.Service/Exceptions/DeleteFailureException.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Service.Exceptions; 2 | 3 | public class DeleteFailureException(string name, object key, string message) 4 | : Exception($"Deletion of entity \"{name}\" ({key}) failed. {message}"); -------------------------------------------------------------------------------- /src/OA.Service/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Service.Exceptions; 2 | 3 | public class NotFoundException(string name, object key) : Exception($"Entity \"{name}\" ({key}) was not found."); -------------------------------------------------------------------------------- /src/OA.Service/Exceptions/ValidationException.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.Results; 2 | 3 | namespace OA.Service.Exceptions; 4 | 5 | public class ValidationException() : Exception("One or more validation failures have occurred.") 6 | { 7 | public ValidationException(List failures) 8 | : this() 9 | { 10 | var propertyNames = failures 11 | .Select(e => e.PropertyName) 12 | .Distinct(); 13 | 14 | foreach (var propertyName in propertyNames) 15 | { 16 | var propertyFailures = failures 17 | .Where(e => e.PropertyName == propertyName) 18 | .Select(e => e.ErrorMessage) 19 | .ToArray(); 20 | 21 | Failures.Add(propertyName, propertyFailures); 22 | } 23 | } 24 | 25 | public IDictionary Failures { get; } = new Dictionary(); 26 | } -------------------------------------------------------------------------------- /src/OA.Service/Features/CustomerFeatures/Commands/CreateCustomerCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using OA.Domain.Entities; 3 | using OA.Persistence; 4 | 5 | namespace OA.Service.Features.CustomerFeatures.Commands; 6 | 7 | public class CreateCustomerCommand : IRequest 8 | { 9 | public string CustomerName { get; set; } 10 | public string ContactName { get; set; } 11 | public string ContactTitle { get; set; } 12 | public string Address { get; set; } 13 | public string City { get; set; } 14 | public string Region { get; set; } 15 | public string PostalCode { get; set; } 16 | public string Country { get; set; } 17 | public string Phone { get; set; } 18 | public string Fax { get; set; } 19 | 20 | } 21 | 22 | public class CreateCustomerCommandHandler(IApplicationDbContext context) 23 | : IRequestHandler 24 | { 25 | public async Task Handle(CreateCustomerCommand request, CancellationToken cancellationToken) 26 | { 27 | var customer = new Customer 28 | { 29 | CustomerName = request.CustomerName, 30 | ContactName = request.ContactName, 31 | Address = request.Address, 32 | City = request.City, 33 | Region = request.Region, 34 | PostalCode = request.PostalCode, 35 | Country = request.Country, 36 | Phone = request.Phone, 37 | Fax = request.Fax, 38 | ContactTitle = request.ContactTitle 39 | }; 40 | 41 | context.Customers.Add(customer); 42 | await context.SaveChangesAsync(); 43 | return customer.Id; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/OA.Service/Features/CustomerFeatures/Commands/DeleteCustomerByIdCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.EntityFrameworkCore; 3 | using OA.Persistence; 4 | 5 | namespace OA.Service.Features.CustomerFeatures.Commands; 6 | 7 | public class DeleteCustomerByIdCommand : IRequest 8 | { 9 | public int Id { get; set; } 10 | } 11 | 12 | public class DeleteCustomerByIdCommandHandler(IApplicationDbContext context) 13 | : IRequestHandler 14 | { 15 | public async Task Handle(DeleteCustomerByIdCommand request, CancellationToken cancellationToken) 16 | { 17 | var customer = await context.Customers.FirstOrDefaultAsync(a => a.Id == request.Id, cancellationToken: cancellationToken); 18 | if (customer == null) return default; 19 | context.Customers.Remove(customer); 20 | await context.SaveChangesAsync(); 21 | return customer.Id; 22 | } 23 | } -------------------------------------------------------------------------------- /src/OA.Service/Features/CustomerFeatures/Commands/UpdateCustomerCommand.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.EntityFrameworkCore; 3 | using OA.Persistence; 4 | 5 | namespace OA.Service.Features.CustomerFeatures.Commands; 6 | 7 | public class UpdateCustomerCommand : IRequest 8 | { 9 | public int Id { get; set; } 10 | public string CustomerName { get; set; } 11 | public string ContactName { get; set; } 12 | public string ContactTitle { get; set; } 13 | public string Address { get; set; } 14 | public string City { get; set; } 15 | public string Region { get; set; } 16 | public string PostalCode { get; set; } 17 | public string Country { get; set; } 18 | public string Phone { get; set; } 19 | public string Fax { get; set; } 20 | 21 | } 22 | 23 | public class UpdateCustomerCommandHandler(IApplicationDbContext context) 24 | : IRequestHandler 25 | { 26 | public async Task Handle(UpdateCustomerCommand request, CancellationToken cancellationToken) 27 | { 28 | var customer = await context.Customers.FirstOrDefaultAsync(a => a.Id == request.Id, cancellationToken: cancellationToken); 29 | 30 | if (customer == null) 31 | return default; 32 | 33 | customer.CustomerName = request.CustomerName; 34 | customer.ContactName = request.ContactName; 35 | customer.ContactTitle = request.ContactTitle; 36 | customer.Address = request.Address; 37 | customer.City = request.City; 38 | customer.Region = request.Region; 39 | customer.PostalCode = request.PostalCode; 40 | customer.Country = request.Country; 41 | customer.Fax = request.Fax; 42 | customer.Phone = request.Phone; 43 | 44 | context.Customers.Update(customer); 45 | await context.SaveChangesAsync(); 46 | 47 | return customer.Id; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/OA.Service/Features/CustomerFeatures/Queries/GetAllCustomerQuery.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.EntityFrameworkCore; 3 | using OA.Domain.Entities; 4 | using OA.Persistence; 5 | 6 | namespace OA.Service.Features.CustomerFeatures.Queries; 7 | 8 | public class GetAllCustomerQuery : IRequest> 9 | { 10 | } 11 | 12 | public class GetAllCustomerQueryHandler(IApplicationDbContext context) 13 | : IRequestHandler> 14 | { 15 | public async Task> Handle(GetAllCustomerQuery request, CancellationToken cancellationToken) 16 | { 17 | var customerList = await context.Customers.ToListAsync(cancellationToken: cancellationToken); 18 | return customerList.AsReadOnly(); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/OA.Service/Features/CustomerFeatures/Queries/GetCustomerByIdQuery.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.EntityFrameworkCore; 3 | using OA.Domain.Entities; 4 | using OA.Persistence; 5 | 6 | namespace OA.Service.Features.CustomerFeatures.Queries; 7 | 8 | public class GetCustomerByIdQuery : IRequest 9 | { 10 | public int Id { get; set; } 11 | } 12 | 13 | public class GetCustomerByIdQueryHandler(IApplicationDbContext context) 14 | : IRequestHandler 15 | { 16 | public async Task Handle(GetCustomerByIdQuery request, CancellationToken cancellationToken) 17 | { 18 | return await context.Customers.FirstOrDefaultAsync(a => a.Id == request.Id, cancellationToken: cancellationToken); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/OA.Service/Implementation/AccountService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Identity; 2 | using Microsoft.AspNetCore.WebUtilities; 3 | using Microsoft.Extensions.Options; 4 | using Microsoft.FeatureManagement; 5 | using Microsoft.IdentityModel.Tokens; 6 | using OA.Domain.Auth; 7 | using OA.Domain.Common; 8 | using OA.Domain.Enum; 9 | using OA.Domain.Settings; 10 | using OA.Service.Contract; 11 | using OA.Service.Exceptions; 12 | using System.IdentityModel.Tokens.Jwt; 13 | using System.Security.Claims; 14 | using System.Security.Cryptography; 15 | using System.Text; 16 | 17 | namespace OA.Service.Implementation; 18 | 19 | public class AccountService( 20 | UserManager userManager, 21 | IOptions jwtSettings, 22 | SignInManager signInManager, 23 | IEmailService emailService, 24 | IFeatureManager featureManager) 25 | : IAccountService 26 | { 27 | private readonly JWTSettings _jwtSettings = jwtSettings.Value; 28 | 29 | public async Task> AuthenticateAsync(AuthenticationRequest request, string ipAddress) 30 | { 31 | var user = await userManager.FindByEmailAsync(request.Email); 32 | if (user == null) 33 | { 34 | throw new ApiException($"No Accounts Registered with {request.Email}."); 35 | } 36 | var result = await signInManager.PasswordSignInAsync(user.UserName, request.Password, false, lockoutOnFailure: false); 37 | if (!result.Succeeded) 38 | { 39 | throw new ApiException($"Invalid Credentials for '{request.Email}'."); 40 | } 41 | if (!user.EmailConfirmed) 42 | { 43 | throw new ApiException($"Account Not Confirmed for '{request.Email}'."); 44 | } 45 | JwtSecurityToken jwtSecurityToken = await GenerateJWToken(user); 46 | AuthenticationResponse response = new AuthenticationResponse(); 47 | response.Id = user.Id; 48 | response.JWToken = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken); 49 | response.Email = user.Email; 50 | response.UserName = user.UserName; 51 | var rolesList = await userManager.GetRolesAsync(user).ConfigureAwait(false); 52 | response.Roles = rolesList.ToList(); 53 | response.IsVerified = user.EmailConfirmed; 54 | var refreshToken = GenerateRefreshToken(ipAddress); 55 | response.RefreshToken = refreshToken.Token; 56 | return new Response(response, $"Authenticated {user.UserName}"); 57 | } 58 | 59 | public async Task> RegisterAsync(RegisterRequest request, string origin) 60 | { 61 | var userWithSameUserName = await userManager.FindByNameAsync(request.UserName); 62 | if (userWithSameUserName != null) 63 | { 64 | throw new ApiException($"Username '{request.UserName}' is already taken."); 65 | } 66 | var user = new ApplicationUser 67 | { 68 | Email = request.Email, 69 | FirstName = request.FirstName, 70 | LastName = request.LastName, 71 | UserName = request.UserName 72 | }; 73 | var userWithSameEmail = await userManager.FindByEmailAsync(request.Email); 74 | if (userWithSameEmail == null) 75 | { 76 | var result = await userManager.CreateAsync(user, request.Password); 77 | if (result.Succeeded) 78 | { 79 | await userManager.AddToRoleAsync(user, Roles.Basic.ToString()); 80 | var verificationUri = await SendVerificationEmail(user, origin); 81 | 82 | if (await featureManager.IsEnabledAsync(nameof(FeatureManagement.EnableEmailService))) 83 | { 84 | await emailService.SendEmailAsync(new MailRequest() { From = "amit.naik8103@gmail.com", ToEmail = user.Email, Body = $"Please confirm your account by visiting this URL {verificationUri}", Subject = "Confirm Registration" }); 85 | } 86 | return new Response(user.Id, message: $"User Registered. Please confirm your account by visiting this URL {verificationUri}"); 87 | } 88 | else 89 | { 90 | throw new ApiException($"{result.Errors.ToList()[0].Description}"); 91 | } 92 | } 93 | else 94 | { 95 | throw new ApiException($"Email {request.Email} is already registered."); 96 | } 97 | } 98 | 99 | private async Task GenerateJWToken(ApplicationUser user) 100 | { 101 | var userClaims = await userManager.GetClaimsAsync(user); 102 | var roles = await userManager.GetRolesAsync(user); 103 | 104 | var roleClaims = roles.Select(t => new Claim("roles", t)).ToList(); 105 | 106 | string ipAddress = IpHelper.GetIpAddress(); 107 | 108 | var claims = new[] 109 | { 110 | new Claim(JwtRegisteredClaimNames.Sub, user.UserName), 111 | new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()), 112 | new Claim(JwtRegisteredClaimNames.Email, user.Email), 113 | new Claim("uid", user.Id), 114 | new Claim("ip", ipAddress) 115 | } 116 | .Union(userClaims) 117 | .Union(roleClaims); 118 | 119 | var symmetricSecurityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.Key)); 120 | var signingCredentials = new SigningCredentials(symmetricSecurityKey, SecurityAlgorithms.HmacSha256); 121 | 122 | var jwtSecurityToken = new JwtSecurityToken( 123 | issuer: _jwtSettings.Issuer, 124 | audience: _jwtSettings.Audience, 125 | claims: claims, 126 | expires: DateTime.UtcNow.AddMinutes(_jwtSettings.DurationInMinutes), 127 | signingCredentials: signingCredentials); 128 | return jwtSecurityToken; 129 | } 130 | 131 | private string RandomTokenString() 132 | { 133 | using var rngCryptoServiceProvider = new RNGCryptoServiceProvider(); 134 | var randomBytes = new byte[40]; 135 | rngCryptoServiceProvider.GetBytes(randomBytes); 136 | // convert random bytes to hex string 137 | return BitConverter.ToString(randomBytes).Replace("-", ""); 138 | } 139 | 140 | private async Task SendVerificationEmail(ApplicationUser user, string origin) 141 | { 142 | var code = await userManager.GenerateEmailConfirmationTokenAsync(user); 143 | code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); 144 | var route = "api/account/confirm-email/"; 145 | var endpointUri = new Uri(string.Concat($"{origin}/", route)); 146 | var verificationUri = QueryHelpers.AddQueryString(endpointUri.ToString(), "userId", user.Id); 147 | verificationUri = QueryHelpers.AddQueryString(verificationUri, "code", code); 148 | //Email Service Call Here 149 | return verificationUri; 150 | } 151 | 152 | public async Task> ConfirmEmailAsync(string userId, string code) 153 | { 154 | var user = await userManager.FindByIdAsync(userId); 155 | code = Encoding.UTF8.GetString(WebEncoders.Base64UrlDecode(code)); 156 | var result = await userManager.ConfirmEmailAsync(user, code); 157 | if (result.Succeeded) 158 | { 159 | return new Response(user.Id, message: $"Account Confirmed for {user.Email}. You can now use the /api/Account/authenticate endpoint."); 160 | } 161 | else 162 | { 163 | throw new ApiException($"An error occured while confirming {user.Email}."); 164 | } 165 | } 166 | 167 | private RefreshToken GenerateRefreshToken(string ipAddress) 168 | { 169 | return new RefreshToken 170 | { 171 | Token = RandomTokenString(), 172 | Expires = DateTime.UtcNow.AddDays(7), 173 | Created = DateTime.UtcNow, 174 | CreatedByIp = ipAddress 175 | }; 176 | } 177 | 178 | public async Task ForgotPassword(ForgotPasswordRequest model, string origin) 179 | { 180 | var account = await userManager.FindByEmailAsync(model.Email); 181 | 182 | // always return ok response to prevent email enumeration 183 | if (account == null) return; 184 | 185 | var code = await userManager.GeneratePasswordResetTokenAsync(account); 186 | var emailRequest = new MailRequest() 187 | { 188 | Body = $"You reset token is - {code}", 189 | ToEmail = model.Email, 190 | Subject = "Reset Password", 191 | }; 192 | await emailService.SendEmailAsync(emailRequest); 193 | } 194 | 195 | public async Task> ResetPassword(ResetPasswordRequest model) 196 | { 197 | var account = await userManager.FindByEmailAsync(model.Email); 198 | if (account == null) throw new ApiException($"No Accounts Registered with {model.Email}."); 199 | var result = await userManager.ResetPasswordAsync(account, model.Token, model.Password); 200 | if (result.Succeeded) 201 | { 202 | return new Response(model.Email, message: $"Password Resetted."); 203 | } 204 | else 205 | { 206 | throw new ApiException($"Error occured while reseting the password."); 207 | } 208 | } 209 | } -------------------------------------------------------------------------------- /src/OA.Service/Implementation/DateTimeService.cs: -------------------------------------------------------------------------------- 1 | using OA.Service.Contract; 2 | 3 | namespace OA.Service.Implementation; 4 | 5 | public class DateTimeService : IDateTimeService 6 | { 7 | public DateTime NowUtc => DateTime.UtcNow; 8 | } -------------------------------------------------------------------------------- /src/OA.Service/Implementation/MailService.cs: -------------------------------------------------------------------------------- 1 | using MailKit.Net.Smtp; 2 | using MailKit.Security; 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.Extensions.Options; 5 | using MimeKit; 6 | using OA.Domain.Settings; 7 | using OA.Service.Contract; 8 | using OA.Service.Exceptions; 9 | 10 | namespace OA.Service.Implementation; 11 | 12 | public class MailService(IOptions mailSettings, ILogger logger) 13 | : IEmailService 14 | { 15 | public async Task SendEmailAsync(MailRequest mailRequest) 16 | { 17 | try 18 | { 19 | // create message 20 | var email = new MimeMessage(); 21 | email.Sender = MailboxAddress.Parse(mailRequest.From ?? mailSettings.Value.EmailFrom); 22 | email.To.Add(MailboxAddress.Parse(mailRequest.ToEmail)); 23 | email.Subject = mailRequest.Subject; 24 | var builder = new BodyBuilder(); 25 | builder.HtmlBody = mailRequest.Body; 26 | email.Body = builder.ToMessageBody(); 27 | using var smtp = new SmtpClient(); 28 | await smtp.ConnectAsync(mailSettings.Value.SmtpHost, mailSettings.Value.SmtpPort, SecureSocketOptions.StartTls); 29 | await smtp.AuthenticateAsync(mailSettings.Value.SmtpUser, mailSettings.Value.SmtpPass); 30 | await smtp.SendAsync(email); 31 | await smtp.DisconnectAsync(true); 32 | 33 | } 34 | catch (System.Exception ex) 35 | { 36 | logger.LogError(ex.Message, ex); 37 | throw new ApiException(ex.Message); 38 | } 39 | } 40 | 41 | } -------------------------------------------------------------------------------- /src/OA.Service/Middleware/CustomExceptionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Logging; 3 | using Newtonsoft.Json; 4 | using OA.Service.Exceptions; 5 | using System.Net; 6 | 7 | namespace OA.Service.Middleware; 8 | 9 | public class CustomExceptionMiddleware(RequestDelegate next, ILogger logger) 10 | { 11 | public async Task Invoke(HttpContext context) 12 | { 13 | try 14 | { 15 | await next(context); 16 | } 17 | catch (Exception exceptionObj) 18 | { 19 | await HandleExceptionAsync(context, exceptionObj, logger); 20 | } 21 | } 22 | 23 | private static Task HandleExceptionAsync(HttpContext context, Exception exception, ILogger logger) 24 | { 25 | int code; 26 | var result = exception.Message; 27 | 28 | switch (exception) 29 | { 30 | case ValidationException validationException: 31 | code = (int)HttpStatusCode.BadRequest; 32 | result = JsonConvert.SerializeObject(validationException.Failures); 33 | break; 34 | case BadRequestException badRequestException: 35 | code = (int)HttpStatusCode.BadRequest; 36 | result = badRequestException.Message; 37 | break; 38 | case DeleteFailureException deleteFailureException: 39 | code = (int)HttpStatusCode.BadRequest; 40 | result = deleteFailureException.Message; 41 | break; 42 | case NotFoundException _: 43 | code = (int)HttpStatusCode.NotFound; 44 | break; 45 | default: 46 | code = (int)HttpStatusCode.InternalServerError; 47 | break; 48 | } 49 | 50 | logger.LogError(result); 51 | 52 | context.Response.ContentType = "application/json"; 53 | context.Response.StatusCode = code; 54 | return context.Response.WriteAsync(JsonConvert.SerializeObject(new { StatusCode = code, ErrorMessage = exception.Message })); 55 | } 56 | } -------------------------------------------------------------------------------- /src/OA.Service/OA.Service.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/OA.Test.Integration/ApiEndpoints/ApiCustomerTest.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using OA.Service.Features.CustomerFeatures.Commands; 3 | using System.Net; 4 | 5 | namespace OA.Test.Integration.ApiEndpoints; 6 | 7 | public class ApiCustomerTest : TestClientProvider 8 | { 9 | [TestCase("api/v1/Customer", HttpStatusCode.OK)] 10 | [TestCase("api/v1/Customer/100", HttpStatusCode.NoContent)] 11 | public async Task GetAllCustomerTestAsync(string url, HttpStatusCode result) 12 | { 13 | var request = new HttpRequestMessage(HttpMethod.Get, url); 14 | 15 | var response = await Client 16 | .AddBearerTokenAuthorization() 17 | .SendAsync(request); 18 | 19 | Assert.That(response.StatusCode, Is.EqualTo(result)); 20 | } 21 | 22 | [Test] 23 | public async Task CreateCustomerTestAsync() 24 | { 25 | // Create a customer model 26 | var model = new CreateCustomerCommand 27 | { 28 | CustomerName = "John Wick", 29 | ContactName = "John Wick", 30 | ContactTitle = "Manager", 31 | Address = "123 Main St", 32 | City = "New York", 33 | Region = "NY", 34 | PostalCode = "10001", 35 | Country = "USA", 36 | Phone = "123-456-7890", 37 | Fax = "987-654-3210" 38 | }; 39 | 40 | // Create POST request 41 | var request = new HttpRequestMessage(HttpMethod.Post, "api/v1/Customer") 42 | { 43 | Content = HttpClientExtensions.SerializeAndCreateContent(model) 44 | }; 45 | 46 | // Send request and check response 47 | var response = await Client 48 | .AddBearerTokenAuthorization() 49 | .SendAsync(request); 50 | 51 | Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK)); 52 | } 53 | 54 | [Test] 55 | public async Task UpdateCustomerTestAsync() 56 | { 57 | // First, create a new customer 58 | var createModel = new CreateCustomerCommand 59 | { 60 | CustomerName = "John Wick", 61 | ContactName = "John Wick", 62 | ContactTitle = "Manager", 63 | Address = "123 Main St", 64 | City = "New York", 65 | Region = "NY", 66 | PostalCode = "10001", 67 | Country = "USA", 68 | Phone = "123-456-7890", 69 | Fax = "987-654-3210" 70 | }; 71 | 72 | var createRequest = new HttpRequestMessage(HttpMethod.Post, "api/v1/Customer") 73 | { 74 | Content = HttpClientExtensions.SerializeAndCreateContent(createModel) 75 | }; 76 | 77 | var createResponse = await Client 78 | .AddBearerTokenAuthorization() 79 | .SendAsync(createRequest); 80 | 81 | Assert.That(createResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK)); 82 | 83 | // Now update the new customer 84 | var updateModel = new UpdateCustomerCommand 85 | { 86 | Id = int.Parse(await createResponse.Content.ReadAsStringAsync()), 87 | CustomerName = "Updated John Wick", 88 | ContactName = "Jane Wick", 89 | ContactTitle = "Director", 90 | Address = "456 Another St", 91 | City = "Los Angeles", 92 | Region = "CA", 93 | PostalCode = "90001", 94 | Country = "USA", 95 | Phone = "987-654-3210", 96 | Fax = "123-456-7890" 97 | }; 98 | 99 | var updateRequest = new HttpRequestMessage(HttpMethod.Put, $"api/v1/Customer/{updateModel.Id}") 100 | { 101 | Content = HttpClientExtensions.SerializeAndCreateContent(updateModel) 102 | }; 103 | 104 | // Send update request and check response status 105 | var updateResponse = await Client 106 | .AddBearerTokenAuthorization() 107 | .SendAsync(updateRequest); 108 | 109 | Assert.That(updateResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK)); 110 | } 111 | 112 | [Test] 113 | public async Task DeleteCustomerTestAsync() 114 | { 115 | // First, create a new customer 116 | var createModel = new CreateCustomerCommand 117 | { 118 | CustomerName = "John Wick", 119 | ContactName = "John Wick", 120 | ContactTitle = "Manager", 121 | Address = "123 Main St", 122 | City = "New York", 123 | Region = "NY", 124 | PostalCode = "10001", 125 | Country = "USA", 126 | Phone = "123-456-7890", 127 | Fax = "987-654-3210" 128 | }; 129 | 130 | var createRequest = new HttpRequestMessage(HttpMethod.Post, "api/v1/Customer") 131 | { 132 | Content = HttpClientExtensions.SerializeAndCreateContent(createModel) 133 | }; 134 | 135 | var createResponse = await Client 136 | .AddBearerTokenAuthorization() 137 | .SendAsync(createRequest); 138 | 139 | Assert.That(createResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK)); 140 | 141 | // Get the created customer's ID 142 | int customerId = int.Parse(await createResponse.Content.ReadAsStringAsync()); 143 | 144 | // Now delete the customer 145 | var deleteRequest = new HttpRequestMessage(HttpMethod.Delete, $"api/v1/Customer/{customerId}"); 146 | 147 | // Send delete request and check response status 148 | var deleteResponse = await Client 149 | .AddBearerTokenAuthorization() 150 | .SendAsync(deleteRequest); 151 | 152 | Assert.That(deleteResponse.StatusCode, Is.EqualTo(HttpStatusCode.OK)); 153 | 154 | // Optionally, you can check if the customer has been actually deleted by trying to retrieve it 155 | var getRequest = new HttpRequestMessage(HttpMethod.Get, $"api/v1/Customer/{customerId}"); 156 | var getResponse = await Client.AddBearerTokenAuthorization().SendAsync(getRequest); 157 | Assert.That(getResponse.StatusCode, Is.EqualTo(HttpStatusCode.NoContent)); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/OA.Test.Integration/HttpClientAuthExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace OA.Test.Integration; 2 | 3 | internal static class HttpClientAuthExtensions 4 | { 5 | const string AuthorizationKey = "Authorization"; 6 | 7 | // JWT generated for 'superadmin@gmail.com' with max expiry date 8 | const string Jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdXBlcmFkbWluIiwianRpIjoiNDIzNzhlNjktMmI0YS00ZTVhLWEyZjUtM2RjMTI4YTFhNGFiIiwiZW1haWwiOiJzdXBlcmFkbWluQGdtYWlsLmNvbSIsInVpZCI6Ijk3Y2Y0ZDkwLWYyY2EtNGEwZi04MjdhLWU2ZmVkZTE2ODQyYSIsImlwIjoiMTkyLjE2OC40NS4xMzUiLCJyb2xlcyI6WyJTdXBlckFkbWluIiwiQWRtaW4iLCJCYXNpYyIsIk1vZGVyYXRvciJdLCJleHAiOjI1MzQwMjI4ODIwMCwiaXNzIjoiSWRlbnRpdHkiLCJhdWQiOiJJZGVudGl0eVVzZXIifQ.sYDCw6R-HtNfC8xJYENmq39iYJtXiVrAh5dboTrGlX8"; 9 | 10 | public static HttpClient AddBearerTokenAuthorization(this HttpClient client) 11 | { 12 | // Check if the Authorization header is already present 13 | if (client.DefaultRequestHeaders.Any(p => p.Key == AuthorizationKey)) 14 | return client; 15 | 16 | client.DefaultRequestHeaders.Add(AuthorizationKey, $"Bearer {Jwt}"); 17 | 18 | return client; 19 | 20 | } 21 | } -------------------------------------------------------------------------------- /src/OA.Test.Integration/HttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text; 3 | 4 | namespace OA.Test.Integration; 5 | 6 | public static class HttpClientExtensions 7 | { 8 | static readonly JsonSerializerOptions DefaultJsonOptions = new JsonSerializerOptions 9 | { 10 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase 11 | }; 12 | 13 | public static StringContent SerializeAndCreateContent(T model) 14 | { 15 | string jsonContent = JsonSerializer.Serialize(model, DefaultJsonOptions); 16 | var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); 17 | return content; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/OA.Test.Integration/OA.Test.Integration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/OA.Test.Integration/TestClientProvider.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | 4 | namespace OA.Test.Integration; 5 | 6 | public class TestClientProvider : IDisposable 7 | { 8 | private readonly WebApplicationFactory _factory; 9 | public HttpClient Client { get; } 10 | 11 | public TestClientProvider() 12 | { 13 | // Initialize WebApplicationFactory with the Program class 14 | _factory = new WebApplicationFactory() 15 | .WithWebHostBuilder(builder => 16 | { 17 | // Set the environment to Test 18 | builder.UseEnvironment("Test"); 19 | }); 20 | 21 | Client = _factory.CreateClient(); 22 | } 23 | 24 | public void Dispose() 25 | { 26 | Client.Dispose(); 27 | _factory.Dispose(); 28 | } 29 | } -------------------------------------------------------------------------------- /src/OA.Test.Unit/OA.Test.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/OA.Test.Unit/Persistence/ApplicationDbContextTest.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using NUnit.Framework; 3 | using OA.Domain.Entities; 4 | using OA.Persistence; 5 | 6 | namespace OA.Test.Unit.Persistence; 7 | 8 | public class ApplicationDbContextTest 9 | { 10 | [Test] 11 | public void CanInsertCustomerIntoDatabase() 12 | { 13 | 14 | using var context = new ApplicationDbContext(); 15 | var customer = new Customer(); 16 | context.Customers.Add(customer); 17 | Assert.That(context.Entry(customer).State, Is.EqualTo(EntityState.Added)); 18 | } 19 | } -------------------------------------------------------------------------------- /src/OA.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30114.105 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA", "OA\OA.csproj", "{5CE8523B-CD37-4F3E-9F41-9A3D2DAB3D39}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Domain", "OA.Domain\OA.Domain.csproj", "{74D8BF98-D40C-447E-BB40-29B1BAA363AB}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Infrastructure", "OA.Infrastructure\OA.Infrastructure.csproj", "{97A14F11-44A9-443C-ADC4-CF5696BC64F7}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Persistence", "OA.Persistence\OA.Persistence.csproj", "{5F6B0320-95CE-4D4C-82D6-7A0972243716}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Service", "OA.Service\OA.Service.csproj", "{B20723E2-C6FC-41B2-8807-FC1E52B012F0}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Test.Unit", "OA.Test.Unit\OA.Test.Unit.csproj", "{FDDC1E0E-296B-448C-90C2-9364B118F5E2}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OA.Test.Integration", "OA.Test.Integration\OA.Test.Integration.csproj", "{4150259A-1CC5-4BB2-A0D4-891F56338028}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OATemplate", "OATemplate\OATemplate.csproj", "{A5926428-D707-4D5E-B785-82DFE8C5AC85}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 28 | {5CE8523B-CD37-4F3E-9F41-9A3D2DAB3D39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 29 | {5CE8523B-CD37-4F3E-9F41-9A3D2DAB3D39}.Debug|Any CPU.Build.0 = Debug|Any CPU 30 | {5CE8523B-CD37-4F3E-9F41-9A3D2DAB3D39}.Release|Any CPU.ActiveCfg = Release|Any CPU 31 | {5CE8523B-CD37-4F3E-9F41-9A3D2DAB3D39}.Release|Any CPU.Build.0 = Release|Any CPU 32 | {74D8BF98-D40C-447E-BB40-29B1BAA363AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {74D8BF98-D40C-447E-BB40-29B1BAA363AB}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {74D8BF98-D40C-447E-BB40-29B1BAA363AB}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {74D8BF98-D40C-447E-BB40-29B1BAA363AB}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {97A14F11-44A9-443C-ADC4-CF5696BC64F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {97A14F11-44A9-443C-ADC4-CF5696BC64F7}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {97A14F11-44A9-443C-ADC4-CF5696BC64F7}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {97A14F11-44A9-443C-ADC4-CF5696BC64F7}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {5F6B0320-95CE-4D4C-82D6-7A0972243716}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {5F6B0320-95CE-4D4C-82D6-7A0972243716}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {5F6B0320-95CE-4D4C-82D6-7A0972243716}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {5F6B0320-95CE-4D4C-82D6-7A0972243716}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {B20723E2-C6FC-41B2-8807-FC1E52B012F0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {B20723E2-C6FC-41B2-8807-FC1E52B012F0}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {B20723E2-C6FC-41B2-8807-FC1E52B012F0}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {B20723E2-C6FC-41B2-8807-FC1E52B012F0}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {FDDC1E0E-296B-448C-90C2-9364B118F5E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {FDDC1E0E-296B-448C-90C2-9364B118F5E2}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {FDDC1E0E-296B-448C-90C2-9364B118F5E2}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {FDDC1E0E-296B-448C-90C2-9364B118F5E2}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {4150259A-1CC5-4BB2-A0D4-891F56338028}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {4150259A-1CC5-4BB2-A0D4-891F56338028}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {4150259A-1CC5-4BB2-A0D4-891F56338028}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {4150259A-1CC5-4BB2-A0D4-891F56338028}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {A5926428-D707-4D5E-B785-82DFE8C5AC85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {A5926428-D707-4D5E-B785-82DFE8C5AC85}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {A5926428-D707-4D5E-B785-82DFE8C5AC85}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {A5926428-D707-4D5E-B785-82DFE8C5AC85}.Release|Any CPU.Build.0 = Release|Any CPU 60 | EndGlobalSection 61 | GlobalSection(SolutionProperties) = preSolution 62 | HideSolutionNode = FALSE 63 | EndGlobalSection 64 | GlobalSection(ExtensibilityGlobals) = postSolution 65 | SolutionGuid = {5E5A61BE-464E-48CF-88FA-7982CFBAED9E} 66 | EndGlobalSection 67 | EndGlobal 68 | -------------------------------------------------------------------------------- /src/OA/Controllers/AccountController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OA.Domain.Auth; 3 | using OA.Service.Contract; 4 | 5 | namespace OA.Controllers; 6 | 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | public class AccountController(IAccountService accountService) : ControllerBase 10 | { 11 | [HttpPost("authenticate")] 12 | public async Task AuthenticateAsync(AuthenticationRequest request) 13 | { 14 | return Ok(await accountService.AuthenticateAsync(request, GenerateIPAddress())); 15 | } 16 | [HttpPost("register")] 17 | public async Task RegisterAsync(RegisterRequest request) 18 | { 19 | var origin = Request.Headers["origin"]; 20 | return Ok(await accountService.RegisterAsync(request, origin)); 21 | } 22 | [HttpGet("confirm-email")] 23 | public async Task ConfirmEmailAsync([FromQuery] string userId, [FromQuery] string code) 24 | { 25 | var origin = Request.Headers["origin"]; 26 | return Ok(await accountService.ConfirmEmailAsync(userId, code)); 27 | } 28 | [HttpPost("forgot-password")] 29 | public async Task ForgotPassword(ForgotPasswordRequest model) 30 | { 31 | await accountService.ForgotPassword(model, Request.Headers["origin"]); 32 | return Ok(); 33 | } 34 | [HttpPost("reset-password")] 35 | public async Task ResetPassword(ResetPasswordRequest model) 36 | { 37 | 38 | return Ok(await accountService.ResetPassword(model)); 39 | } 40 | private string GenerateIPAddress() 41 | { 42 | if (Request.Headers.ContainsKey("X-Forwarded-For")) 43 | return Request.Headers["X-Forwarded-For"]; 44 | else 45 | return HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(); 46 | } 47 | } -------------------------------------------------------------------------------- /src/OA/Controllers/CustomerController.cs: -------------------------------------------------------------------------------- 1 | using MediatR; 2 | using Microsoft.AspNetCore.Authorization; 3 | using Microsoft.AspNetCore.Mvc; 4 | using OA.Service.Features.CustomerFeatures.Commands; 5 | using OA.Service.Features.CustomerFeatures.Queries; 6 | 7 | namespace OA.Controllers; 8 | 9 | [Authorize] 10 | [ApiController] 11 | [Route("api/v{version:apiVersion}/Customer")] 12 | [ApiVersion("1.0")] 13 | public class CustomerController : ControllerBase 14 | { 15 | private IMediator _mediator; 16 | protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService(); 17 | 18 | [HttpPost] 19 | public async Task Create(CreateCustomerCommand command) 20 | { 21 | return Ok(await Mediator.Send(command)); 22 | } 23 | 24 | [HttpGet] 25 | [Route("")] 26 | public async Task GetAll() 27 | { 28 | return Ok(await Mediator.Send(new GetAllCustomerQuery())); 29 | } 30 | 31 | [HttpGet("{id}")] 32 | public async Task GetById(int id) 33 | { 34 | return Ok(await Mediator.Send(new GetCustomerByIdQuery { Id = id })); 35 | } 36 | 37 | [HttpDelete("{id}")] 38 | public async Task Delete(int id) 39 | { 40 | return Ok(await Mediator.Send(new DeleteCustomerByIdCommand { Id = id })); 41 | } 42 | 43 | 44 | [HttpPut("{id}")] 45 | public async Task Update(int id, UpdateCustomerCommand command) 46 | { 47 | if (id != command.Id) 48 | { 49 | return BadRequest(); 50 | } 51 | return Ok(await Mediator.Send(command)); 52 | } 53 | } -------------------------------------------------------------------------------- /src/OA/Controllers/MailController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using OA.Domain.Settings; 3 | using OA.Service.Contract; 4 | 5 | namespace OA.Controllers; 6 | 7 | [ApiController] 8 | [Route("api/v{version:apiVersion}/Mail")] 9 | [ApiVersion("1.0")] 10 | public class MailController(IEmailService mailService) : ControllerBase 11 | { 12 | [HttpPost("send")] 13 | public async Task SendMail([FromForm] MailRequest request) 14 | { 15 | await mailService.SendEmailAsync(request); 16 | return Ok(); 17 | } 18 | 19 | } -------------------------------------------------------------------------------- /src/OA/Controllers/MetaController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using System.Diagnostics; 3 | 4 | namespace OA.Controllers; 5 | 6 | public class MetaController : ControllerBase 7 | { 8 | [HttpGet("/info")] 9 | public ActionResult Info() 10 | { 11 | var assembly = typeof(Program).Assembly; 12 | 13 | var lastUpdate = System.IO.File.GetLastWriteTime(assembly.Location); 14 | var version = FileVersionInfo.GetVersionInfo(assembly.Location).ProductVersion; 15 | 16 | return Ok($"Version: {version}, Last Updated: {lastUpdate}"); 17 | } 18 | } -------------------------------------------------------------------------------- /src/OA/Customization/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primaryColor: #262f3d; 3 | --secondaryColor: #c40606; 4 | --bgMenuActive: #262f3d; 5 | --bgButton: #c40606; 6 | --logoImageUrl: url('https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/master/docs/img/OnionArchitecture_icon.png'); 7 | --bgAside: var(--primaryColor); 8 | } 9 | -------------------------------------------------------------------------------- /src/OA/OA.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/OA/Program.cs: -------------------------------------------------------------------------------- 1 | using HealthChecks.UI.Client; 2 | using Microsoft.AspNetCore.Diagnostics.HealthChecks; 3 | using Microsoft.Extensions.Diagnostics.HealthChecks; 4 | using Microsoft.FeatureManagement; 5 | using OA.Domain.Settings; 6 | using OA.Infrastructure.Extension; 7 | using OA.Service; 8 | using Serilog; 9 | 10 | var builder = WebApplication.CreateBuilder(args); 11 | 12 | 13 | // Bind AppSettings 14 | var appSettings = new AppSettings(); 15 | builder.Configuration.Bind(appSettings); 16 | 17 | // Add services to the container. 18 | builder.Services.AddControllers(); 19 | builder.Services.AddDbContext(builder.Configuration); 20 | builder.Services.AddIdentityService(builder.Configuration); 21 | builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); 22 | builder.Services.AddScopedServices(); 23 | builder.Services.AddTransientServices(); 24 | builder.Services.AddSwaggerOpenAPI(); 25 | builder.Services.AddMailSetting(builder.Configuration); 26 | builder.Services.AddServiceLayer(); 27 | builder.Services.AddVersion(); 28 | builder.Services.AddHealthCheck(appSettings, builder.Configuration); 29 | builder.Services.AddFeatureManagement(); 30 | 31 | //Setup Serilog 32 | builder.Host.UseSerilog((context, services, configuration) => 33 | { 34 | configuration.ReadFrom.Configuration(context.Configuration); 35 | }); 36 | 37 | var app = builder.Build(); 38 | 39 | // Configure the HTTP request pipeline. 40 | if (app.Environment.IsDevelopment()) 41 | { 42 | app.UseDeveloperExceptionPage(); 43 | } 44 | 45 | // Setup CORS 46 | app.UseCors(options => 47 | options.WithOrigins("http://localhost:3000") 48 | .AllowAnyHeader() 49 | .AllowAnyMethod()); 50 | 51 | // Configure custom exception middleware 52 | app.ConfigureCustomExceptionMiddleware(); 53 | 54 | // Setup Serilog logging 55 | app.Logger.LogInformation("Starting the application with Serilog logging."); 56 | 57 | // Health check configuration 58 | app.UseHealthChecks("/healthz", new HealthCheckOptions 59 | { 60 | Predicate = _ => true, 61 | ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, 62 | ResultStatusCodes = 63 | { 64 | [HealthStatus.Healthy] = StatusCodes.Status200OK, 65 | [HealthStatus.Degraded] = StatusCodes.Status500InternalServerError, 66 | [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable, 67 | }, 68 | }); 69 | 70 | app.UseHealthChecksUI(setup => 71 | { 72 | setup.ApiPath = "/healthcheck"; 73 | setup.UIPath = "/healthcheck-ui"; 74 | // setup.AddCustomStylesheet("Customization/custom.css"); 75 | }); 76 | 77 | // Setup routing 78 | app.UseRouting(); 79 | 80 | // Enable authentication and authorization 81 | app.UseAuthentication(); 82 | app.UseAuthorization(); 83 | 84 | // Enable Swagger UI 85 | app.ConfigureSwagger(); 86 | 87 | // Map controllers 88 | app.MapControllers(); 89 | 90 | // Run the application 91 | app.Run(); 92 | 93 | 94 | public partial class Program 95 | { 96 | } -------------------------------------------------------------------------------- /src/OA/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:53623", 8 | "sslPort": 44356 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "OpenAPI/index.html", 17 | "applicationUrl": "http://localhost:5000", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "OpenAPI/index.html", 27 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "OpenAPI/index.html", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/OA/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/OA/appsettings.Test.json: -------------------------------------------------------------------------------- 1 | { 2 | "UseInMemoryDatabase": true, 3 | "Serilog": { 4 | "MinimumLevel": "Information", 5 | "WriteTo": [ "Console", "DiagnosticTrace" ], 6 | "Properties": { 7 | "Application": "Onion Architecture application" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/OA/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ApplicationDetail": { 3 | "ApplicationName": "Onion Architecture API", 4 | "Description": "Through this WebAPI you can access details", 5 | "ContactWebsite": "https://amitpnk.github.io/", 6 | "LicenseDetail": "https://opensource.org/licenses/MIT" 7 | }, 8 | "Serilog": { 9 | "MinimumLevel": "Information", 10 | "WriteTo": [ 11 | { 12 | "Name": "RollingFile", 13 | "Args": { 14 | "pathFormat": "D:\\Logs\\log-{Date}.log", 15 | "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level}] {Message}{NewLine}{Exception}" 16 | } 17 | }, 18 | { 19 | "Name": "MSSqlServer", 20 | "Args": { 21 | "connectionString": "Data Source=.;Initial Catalog=Onion;Integrated Security=True;MultipleActiveResultSets=True;TrustServerCertificate=True", 22 | "sinkOptionsSection": { 23 | "tableName": "Logs", 24 | "schemaName": "EventLogging", 25 | "autoCreateSqlTable": true 26 | }, 27 | "restrictedToMinimumLevel": "Warning" 28 | } 29 | } 30 | ], 31 | "Properties": { 32 | "Application": "Onion Architecture application" 33 | } 34 | }, 35 | "AllowedHosts": "*", 36 | "UseInMemoryDatabase": false, 37 | "ConnectionStrings": { 38 | "OnionArchConn": "Data Source=.;Initial Catalog=Onion;Integrated Security=True;MultipleActiveResultSets=True;TrustServerCertificate=True", 39 | "IdentityConnection": "Data Source=.;Initial Catalog=OnionIde;Integrated Security=True;MultipleActiveResultSets=True;TrustServerCertificate=True" 40 | }, 41 | "FeatureManagement": { 42 | "EnableEmailService": false 43 | }, 44 | "MailSettings": { 45 | "Mail": "amit.naik6226@gmail.com", 46 | "DisplayName": "Amit Naik", 47 | "Password": "YourGmailPassword", 48 | "Host": "smtp.gmail.com", 49 | "Port": 587 50 | }, 51 | "JWTSettings": { 52 | "Key": "1105D15CB0D48F5781C103A18D5599E4FF25C9102FA694ABDF1DA6828BF153DE", 53 | "Issuer": "Identity", 54 | "Audience": "IdentityUser", 55 | "DurationInMinutes": 60 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/OATemplate/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Amit P Naik 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/OATemplate/OATemplate.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 15.0 6 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 7 | Program 8 | $(DevEnvDir)\devenv.exe 9 | /rootsuffix Exp 10 | 11 | 12 | 13 | Debug 14 | AnyCPU 15 | 2.0 16 | {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 17 | {A5926428-D707-4D5E-B785-82DFE8C5AC85} 18 | Library 19 | Properties 20 | OATemplate 21 | OATemplate 22 | v4.6 23 | false 24 | false 25 | false 26 | false 27 | false 28 | false 29 | 30 | 31 | true 32 | full 33 | false 34 | bin\Debug\ 35 | DEBUG;TRACE 36 | prompt 37 | 4 38 | 39 | 40 | pdbonly 41 | true 42 | bin\Release\ 43 | TRACE 44 | prompt 45 | 4 46 | 47 | 48 | 49 | 50 | 51 | 52 | Designer 53 | 54 | 55 | Designer 56 | 57 | 58 | 59 | 60 | 61 | Always 62 | true 63 | 64 | 65 | Always 66 | true 67 | 68 | 69 | 70 | Always 71 | true 72 | 73 | 74 | 75 | true 76 | 77 | 78 | 79 | 80 | 81 | $(MSBuildProjectDirectory)\Properties\wafflebuilder.targets 82 | 83 | 84 | 85 | 86 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 87 | 88 | 89 | 90 | 97 | -------------------------------------------------------------------------------- /src/OATemplate/OnionArchitecture_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/src/OATemplate/OnionArchitecture_icon.png -------------------------------------------------------------------------------- /src/OATemplate/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("OATemplateProject")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("OATemplateProject")] 12 | [assembly: AssemblyCopyright("")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // Version information for an assembly consists of the following four values: 22 | // 23 | // Major Version 24 | // Minor Version 25 | // Build Number 26 | // Revision 27 | // 28 | // You can specify all the values or you can default the Build and Revision Numbers 29 | // by using the '*' as shown below: 30 | // [assembly: AssemblyVersion("1.0.*")] 31 | [assembly: AssemblyVersion("1.0.0.0")] 32 | [assembly: AssemblyFileVersion("1.0.0.0")] 33 | -------------------------------------------------------------------------------- /src/OATemplate/Properties/project-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/src/OATemplate/Properties/project-icon.png -------------------------------------------------------------------------------- /src/OATemplate/Properties/wafflebuilder.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | 16 | $(MSBuildThisFileFullPath).props 17 | 18 | 19 | 21 | 22 | 23 | Debug 24 | $(MSBuildProjectDirectory)\..\ 25 | bin\$(Configuration)\templates\ 26 | CSharp\Web\SideWaffle 27 | 28 | 29 | 32 | 33 | $(MSBuildThisFileDirectory) 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | $(ProcessTemplatesDependsOn); 42 | BuildTemplateNuGetPackages; 43 | BuildVsTemplateFiles; 44 | 45 | 46 | 47 | 50 | 51 | 54 | 55 | 56 | $(BuildTemplateNuGetPackagesDependsOn); 57 | FindTemplatePackProjFiles; 58 | BuildTemplatePackNuGetProjFiles; 59 | AddTemplateNuGetPackagesToVsix; 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 75 | 76 | 77 | <_cTemplateProj Condition=" '%(TemplatePackNuGetProj.Identity)' != '' ">%(TemplatePackNuGetProj.Identity) 78 | <_cTemplateProj Condition=" '$(_cTemplateProj)' != '' ">$([System.IO.Path]::GetFullPath('$(_cTemplateProj)')) 79 | <_templateOutputPathFull>$([System.IO.Path]::GetFullPath('$(TemplateOutputPath)')) 80 | 81 | 82 | 83 | 84 | 85 | 86 | 90 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | <_templateOutputFullpath>$([System.IO.Path]::GetFullPath('$(TemplateOutputPath)')) 102 | 103 | 104 | 105 | <_templateNuGetPkgs Include="$(_templateOutputFullpath)**/*.nupkg" 106 | Exclude="$(TemplateNuGetPackagesToExcludeFromVsix)" /> 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 119 | 120 | 121 | $(BuildVsTemplateFilesDependsOn); 122 | FindVstemplateFiles; 123 | BuildZipForVstemplateFiles; 124 | 125 | 126 | $(VsTemplateFilesExclude); 127 | $(TemplateSourceRoot)**\bin\**\*; 128 | $(TemplateSourceRoot)**\obj\**\*; 129 | 130 | 131 | 132 | 133 | 134 | 136 | 137 | <_vsTemplateExcludeFiles Include="$(VsTemplateFilesExclude)"/> 138 | <_vstemplateTemp Include="$(TemplateSourceRoot)**/*.vstemplate" Exclude="@(_vsTemplateExcludeFiles)" /> 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | <_templateGetNewProjectNodeXpath Condition=" '$(_templateGetNewProjectNodeXpath)'=='' ">dft:VSTemplate/dft:TemplateContent/dft:CustomParameters/dft:CustomParameter[@Name='SideWaffleNewProjNode']/@Value 149 | 150 | 151 | 153 | 154 | 155 | <_filename>%(VsTemplateFiles.Filename) 156 | <_zipoutfile>$(TemplateOutputPath)$(_filename).zip 157 | <_ziprootdir>%(VsTemplateZipDefaultFiles.RootDir)%(VsTemplateZipDefaultFiles.Directory) 158 | <_vstemplatedir>$([System.IO.Path]::GetFullPath('%(VsTemplateFiles.RootDir)%(VsTemplateFiles.Directory)')) 159 | <_vstemplatefullpath>$([System.IO.Path]::GetFullPath('%(VsTemplateFiles.Fullpath)')) 160 | 161 | 168 | 169 | 170 | 172 | 173 | 177 | 178 | <_filename>%(VsTemplateFiles.Filename) 179 | <_zipoutfile>$([System.IO.Path]::GetFullPath('$(TemplateOutputPath)$(_filename).zip')) 180 | <_ziprootdir>$([System.IO.Path]::GetFullPath('%(VsTemplateZipDefaultFiles.RootDir)%(VsTemplateZipDefaultFiles.Directory)')) 181 | <_vstemplatedir>$([System.IO.Path]::GetFullPath('%(VsTemplateFiles.RootDir)%(VsTemplateFiles.Directory)')) 182 | <_vstemplatefullpath>$([System.IO.Path]::GetFullPath('%(VsTemplateFiles.Fullpath)')) 183 | 184 | 185 | 186 | 187 | 191 | 192 | 193 | <_templatefilestoadd Remove="@(_templatefilestoadd)"/> 194 | <_templatefilestoadd Include="$(_vstemplatedir)**/*"/> 195 | 196 | 201 | 202 | 203 | <_npdNodeXpath>dft:VSTemplate/dft:TemplateContent/dft:CustomParameters/dft:CustomParameter[@Name='SideWaffleNewProjNode']/@Value 204 | <_ls-templateFilePath>%(ls-VsNewProjTemplateFiles.FullPath) 205 | http://schemas.microsoft.com/developer/vstemplate/2005 206 | 207 | 210 | 211 | 212 | 213 | 214 | <_npdNode Condition=" '$(_npdNode)'=='' ">$(DefaultNewProjectNode) 215 | <_npdNode Condition="!HasTrailingSlash('$_npdNode')">$(_npdNode)\ 216 | <_fullNpdNode>Output\Templates\$(_npdNode) 217 | 218 | 219 | 220 | 221 | 222 | $(_fullNpdNode) 223 | 224 | 225 | 226 | 227 | 230 | 231 | $(MSBuildToolsPath)\Microsoft.Build.Tasks.Core.dll 232 | $(MSBuildToolsPath)\Microsoft.Build.Tasks.v12.0.dll 233 | $(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll 234 | $(MSBuildFrameworkToolsPath)\Microsoft.Build.Tasks.v4.0.dll 235 | $(windir)\Microsoft.NET\Framework\v4.0.30319\Microsoft.Build.Tasks.v4.0.dll 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | (); 287 | // if the file is already in the zip remove it and add again 288 | if (zip.Entries != null && zip.Entries.Count > 0) { 289 | List entries = zip.Entries.ToList(); 290 | foreach (var entry in entries) { 291 | if (entry.FullName.Equals(relpath, StringComparison.OrdinalIgnoreCase)) { 292 | // entriesToDelete.Add(entry); 293 | Log.LogMessage(MessageImportance.Low, "deleting zip entry for [{0}]", relpath); 294 | entry.Delete(); 295 | } 296 | } 297 | } 298 | //if(entriesToDelete != null && entriesToDelete.Count > 0){ 299 | // foreach(var entry in entriesToDelete) { 300 | // try { 301 | // entry.Delete(); 302 | // } 303 | // catch(Exception ex){ 304 | // Log.LogMessage(MessageImportance.Low, "Unable to delete entry from zip. {0}", ex.ToString()); 305 | // } 306 | // } 307 | //} 308 | ZipFileExtensions.CreateEntryFromFile(zip, filePath, relpath, level); 309 | } 310 | } 311 | } 312 | catch(Exception ex){ 313 | Log.LogError(ex.ToString()); 314 | return false; 315 | } 316 | 317 | return true; 318 | ]]> 319 | 320 | 321 | 322 | 323 | -------------------------------------------------------------------------------- /src/OATemplate/Resources/OnionArchitecture_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Amitpnk/Onion-architecture-ASP.NET-Core/7b36c72dc718ca1fffd200c487a282df361af859/src/OATemplate/Resources/OnionArchitecture_icon.png -------------------------------------------------------------------------------- /src/OATemplate/index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | Getting Started 9 | 10 | 11 | 12 |
13 | 17 | 18 |
19 |
20 |

Creating a Visual Studio Extension

21 | 22 |

This project enables developers to create an extension for Visual Studio. The solution contains a VSIX project that packages the extension into a VSIX file. This file is used to install an extension for Visual Studio.

23 |

Add new features

24 | 25 |
    26 |
  1. Right-click the project node in Solution Explorer and select Add>New Item.
  2. 27 |
  3. In the Add New Item dialog box, expand the Extensibility node under Visual C# or Visual Basic.
  4. 28 |
  5. Choose from the available item templates: Visual Studio Package, Editor Items (Classifier, Margin, Text Adornment, Viewport Adornment), Command, Tool Window, Toolbox Control, and then click Add.
  6. 29 |
30 | 31 |

The files for the template that you selected are added to the project. You can start adding functionality to your item template, press F5 to run the project, or add additional item templates.

32 | 33 |

Run and debug

34 |

To run the project, press F5. Visual Studio will:

35 | 36 |
    37 |
  • Build the extension from the VSIX project.
  • 38 |
  • Create a VSIX package from the VSIX project.
  • 39 |
  • When debugging, start an experimental instance of Visual Studio with the VSIX package installed.
  • 40 |
41 | 42 |

In the experimental instance of Visual Studio you can test out the functionality of your extension without affecting your Visual Studio installation.

43 | 44 |
45 |
46 |
47 |

Visual Studio Extensibility Resources

48 | 49 |
    50 |
  1. Visual Studio documentation
    Detailed documentation and API reference material for building extensions.
  2. 51 |
  3. Extension samples on GitHub
    Use a sample project to kickstart your development.
  4. 52 |
  5. Extensibility chat room on Gitter
    Meet other extension developers and exchange tips and tricks for extension development.
  6. 53 |
  7. Channel 9 videos on extensibility
    Watch videos from the product team on Visual Studio extensibility.
  8. 54 |
  9. Extensibility Tools
    Install an optional helper tool that adds extra IDE support for extension authors.
  10. 55 |
56 |

Give us feedback

57 | 60 |
61 |
62 |
63 |
64 | 65 | 66 | -------------------------------------------------------------------------------- /src/OATemplate/source.extension.vsixmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Onion Architecture 6 | WhiteApp or QuickApp API solution template which is built on Onion Architecture with all essential feature using .NET Core 7 | LICENSE.txt 8 | https://github.com/Amitpnk/Onion-architecture-ASP.NET-Core/blob/master/README.md 9 | Resources\OnionArchitecture_icon.png 10 | Resources\OnionArchitecture_icon.png 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/OATemplate/stylesheet.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | border: 0; 5 | color: #1E1E1E; 6 | font-size: 13px; 7 | font-family: "Segoe UI", Helvetica, Arial, sans-serif; 8 | line-height: 1.45; 9 | word-wrap: break-word; 10 | } 11 | 12 | /* General & 'Reset' Stuff */ 13 | 14 | 15 | .container { 16 | width: 980px; 17 | margin: 0 auto; 18 | } 19 | 20 | section { 21 | display: block; 22 | margin: 0; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | margin: 0; 27 | } 28 | 29 | /* Header,
30 | header - container 31 | h1 - project name 32 | h2 - project description 33 | */ 34 | 35 | #header { 36 | color: #FFF; 37 | background: #68217a; 38 | position:relative; 39 | } 40 | #hangcloud { 41 | width: 190px; 42 | height: 160px; 43 | background: url("../images/bannerart03.png"); 44 | position: absolute; 45 | top: 0; 46 | right: -30px; 47 | } 48 | h1, h2 { 49 | font-family: "Segoe UI Light", "Segoe UI", Helvetica, Arial, sans-serif; 50 | line-height: 1; 51 | margin: 0 18px; 52 | padding: 0; 53 | } 54 | #header h1 { 55 | font-size: 3.4em; 56 | padding-top: 18px; 57 | font-weight: normal; 58 | margin-left: 15px; 59 | } 60 | 61 | #header h2 { 62 | font-size: 1.5em; 63 | margin-top: 10px; 64 | padding-bottom: 18px; 65 | font-weight: normal; 66 | } 67 | 68 | 69 | #main_content { 70 | width: 100%; 71 | display: flex; 72 | flex-direction: row; 73 | } 74 | 75 | 76 | h1, h2, h3, h4, h5, h6 { 77 | font-weight: bolder; 78 | } 79 | 80 | #main_content h1 { 81 | font-size: 1.8em; 82 | margin-top: 34px; 83 | } 84 | 85 | #main_content h1:first-child { 86 | margin-top: 30px; 87 | } 88 | 89 | #main_content h2 { 90 | font-size: 1.4em; 91 | font-weight: bold; 92 | } 93 | p, ul { 94 | margin: 11px 18px; 95 | } 96 | 97 | #main_content a { 98 | color: #06C; 99 | text-decoration: none; 100 | } 101 | ul { 102 | margin-top: 13px; 103 | margin-left: 18px; 104 | padding-left: 0; 105 | } 106 | ul li { 107 | margin-left: 18px; 108 | padding-left: 0; 109 | } 110 | #lpanel { 111 | width: 620px; 112 | float: left; 113 | } 114 | #rpanel ul { 115 | list-style-type: none; 116 | width: 300px; 117 | } 118 | #rpanel ul li { 119 | line-height: 1.8em; 120 | } 121 | #rpanel { 122 | background: #e7e7e7; 123 | width: 360px; 124 | float: right; 125 | } 126 | 127 | #rpanel div { 128 | width: 300px; 129 | } 130 | -------------------------------------------------------------------------------- /src/OATemplate/template.pkgdef: -------------------------------------------------------------------------------- 1 | [$RootKey$\TemplateEngine\Templates\17FA3FFF-C4C9-429D-AD1D-73EE87DA4E2D] 2 | "InstalledPath"="$PackageFolder$" -------------------------------------------------------------------------------- /src/OATemplate/template/templatepack.OATemplate.proj: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MSBuildThisFileFullPath).props 4 | 5 | 6 | 8 | 9 | 10 | Your name here 11 | A description for your template pack 12 | 1.0.0 13 | 14 | 15 | 16 | netstandard1.0 17 | Template 18 | $(MSBuildThisFileDirectory) 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 32 | 33 | 34 | 35 | True 36 | False 37 | False 38 | True 39 | $(MSBuildProjectFullPath) 40 | bin/$(Configuration)/templates/ 41 | 42 | 43 | 44 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | --------------------------------------------------------------------------------