├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── UserManagement.API.IntegrationTests ├── CustomWebApplicationFactory.cs ├── IntegrationTestHelper.cs ├── User │ └── UserAPI.cs └── UserManagement.API.IntegrationTests.csproj ├── UserManagement.API ├── ClientApp │ ├── .browserslistrc │ ├── .editorconfig │ ├── .gitignore │ ├── README.md │ ├── angular.json │ ├── e2e │ │ ├── protractor.conf.js │ │ ├── src │ │ │ ├── app.e2e-spec.ts │ │ │ └── app.po.ts │ │ └── tsconfig.json │ ├── karma.conf.js │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── app │ │ │ ├── app-routing.module.ts │ │ │ ├── app.component.css │ │ │ ├── app.component.html │ │ │ ├── app.component.spec.ts │ │ │ ├── app.component.ts │ │ │ ├── app.module.ts │ │ │ ├── material-module.ts │ │ │ ├── shared │ │ │ │ ├── enum.ts │ │ │ │ └── util.service.ts │ │ │ ├── user-management-api.ts │ │ │ └── usermanagement │ │ │ │ ├── list-users │ │ │ │ ├── list-users.component.css │ │ │ │ ├── list-users.component.html │ │ │ │ ├── list-users.component.spec.ts │ │ │ │ └── list-users.component.ts │ │ │ │ └── manage-user │ │ │ │ ├── manage-user.component.css │ │ │ │ ├── manage-user.component.html │ │ │ │ ├── manage-user.component.spec.ts │ │ │ │ └── manage-user.component.ts │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── main.ts │ │ ├── polyfills.ts │ │ ├── styles.css │ │ └── test.ts │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── tslint.json ├── Common │ └── CustomExceptionHandlerMiddleware.cs ├── Controllers │ ├── BaseController.cs │ └── UserController.cs ├── Filters │ └── ApiExceptionFilterAttribute.cs ├── Pages │ ├── Error.cshtml │ ├── Error.cshtml.cs │ └── _ViewImports.cshtml ├── Program.cs ├── Properties │ └── launchSettings.json ├── Startup.cs ├── UserManagement.API.csproj ├── appsettings.Development.json ├── appsettings.json ├── nswag.json └── wwwroot │ ├── api │ └── specification.json │ └── favicon.ico ├── UserManagement.Application.UnitTests ├── BaseFixture.cs ├── User │ ├── Commands │ │ ├── AddUserCommandTest.cs │ │ ├── AddUserCommandValidatorTest.cs │ │ ├── DeleteUserCommandTest.cs │ │ ├── DeleteUserCommandValidatorTest.cs │ │ ├── UpdateUserCommandTest.cs │ │ └── UpdateUserCommandValidatorTest.cs │ ├── Queries │ │ ├── GetAllUserQueryTest.cs │ │ ├── GetSingleUserQueryTest.cs │ │ └── GetSingleUserQueryValidator.cs │ └── UserFixture.cs └── UserManagement.Application.UnitTests.csproj ├── UserManagement.Application ├── Common │ ├── BaseClass │ │ └── ApplicationBase.cs │ ├── Behaviors │ │ ├── LoggingBehaviour.cs │ │ ├── PerformanceBehaviour.cs │ │ ├── UnhandledExceptionBehaviour.cs │ │ └── ValidationBehaviour.cs │ ├── Exceptions │ │ ├── NotFoundException.cs │ │ └── ValidationException.cs │ ├── Interfaces │ │ └── IConfigConstants.cs │ └── Mappings │ │ ├── IMapFrom.cs │ │ └── MappingProfile.cs ├── DependencyInjection.cs ├── User │ ├── Commands │ │ ├── AddUserCommand.cs │ │ ├── AddUserCommandValidator.cs │ │ ├── DeleteUserCommand.cs │ │ ├── DeleteUserCommandValidator.cs │ │ ├── UpdateUserCommand.cs │ │ └── UpdateUserCommandValidator.cs │ ├── DTO │ │ └── UserDTO.cs │ ├── Queries │ │ ├── GetAllUserQuery.cs │ │ ├── GetSingleUserQuery.cs │ │ └── GetSingleUserQueryValidator.cs │ └── VM │ │ └── UserVM.cs └── UserManagement.Application.csproj ├── UserManagement.Domain ├── Entities │ └── User.cs ├── Repositories │ └── IUserRepository.cs ├── UnitOfWork │ └── IUnitOfWork.cs └── UserManagement.Domain.csproj ├── UserManagement.Persistence.IntegrationTests ├── User │ └── UserRepositoryTest.cs ├── UserFixture.cs └── UserManagement.Persistence.IntegrationTests.csproj ├── UserManagement.Persistence ├── Constant │ └── ConfigConstants.cs ├── DependencyInjection.cs ├── Repositories │ ├── Repository.cs │ └── UserRepository.cs ├── UnitOfWork │ └── UnitOfWork.cs └── UserManagement.Persistence.csproj └── UserManagement.sln /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Fullstack Hub 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ASP.NET Web API & Angular 10 Clean Architecture 2 | 3 | I recently implemented an application using Jason Taylor Clean Architecture with a [.NET Core](https://jasontaylor.dev/clean-architecture-getting-started/) article and thought it would be a good idea to write another article to break it down into steps so that it would be easy to follow for beginners. Obisvoulsy, there would be some differences e.g. I will be using Dapper micro ORM, repository, and unit of work design pattern instead of Entity Framework .NET Core and will use a separate database for integration testing instead of an in-memory database and also if time permits, I will add more detailed Angular unit and integration tests. 4 | 5 | We are going to develop one of a favorite application called User Management where you would be able to add, update, delete and view the users, currently, I am going to skip user authentication, you can find it in Jason’s [Github repository](https://github.com/jasontaylordev/CleanArchitecture). 6 | 7 | Learning Path Link: https://fullstackhub.io/category/learning-path/asp-net-web-api-angular-10-clean-architecture/ 8 | -------------------------------------------------------------------------------- /UserManagement.API.IntegrationTests/CustomWebApplicationFactory.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.API.IntegrationTests 2 | { 3 | using Microsoft.AspNetCore.Hosting; 4 | using Microsoft.AspNetCore.Mvc.Testing; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using System.Data; 7 | using System.Data.SqlClient; 8 | using System.Net.Http; 9 | using UserManagement.Application.Common.Interfaces; 10 | using UserManagement.Domain.UnitOfWork; 11 | using UserManagement.Persistence.Constant; 12 | using UserManagement.Persistence.UnitOfWork; 13 | 14 | /// 15 | /// The APIs test setup. 16 | /// 17 | /// The input object. 18 | public class CustomWebApplicationFactory : WebApplicationFactory 19 | where TStartup : class 20 | { 21 | /// 22 | /// Get the anonymous client for testing. 23 | /// 24 | /// The anonymous client. 25 | public HttpClient GetAnonymousClient() 26 | { 27 | return this.CreateClient(); 28 | } 29 | 30 | /// 31 | /// Setup for middleware. 32 | /// 33 | /// Take the WebHostBuilder object. 34 | protected override void ConfigureWebHost(IWebHostBuilder builder) 35 | { 36 | builder 37 | .ConfigureServices(services => 38 | { 39 | services.AddSingleton(); 40 | services.AddSingleton(conn => new SqlConnection(conn.GetService().TestFullStackConnection)); 41 | services.AddTransient(uof => new UnitOfWork(uof.GetService())); 42 | }); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /UserManagement.API.IntegrationTests/IntegrationTestHelper.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.API.IntegrationTests 2 | { 3 | using System.Net.Http; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Newtonsoft.Json; 7 | 8 | /// 9 | /// Generic methods for JSON serialization. 10 | /// 11 | public static class IntegrationTestHelper 12 | { 13 | /// 14 | /// Serialize the JSON in API response. 15 | /// 16 | /// The input object to be serialized. 17 | /// Return the string conent the JSON. 18 | public static StringContent GetRequestContent(object obj) 19 | { 20 | return new StringContent(JsonConvert.SerializeObject(obj), Encoding.UTF8, "application/json"); 21 | } 22 | 23 | /// 24 | /// Deserialize the JSON. 25 | /// 26 | /// Input genric class object. 27 | /// The HttpResponseMessage object. 28 | /// The result. 29 | public static async Task GetResponseContent(HttpResponseMessage response) 30 | { 31 | var stringResponse = await response.Content.ReadAsStringAsync(); 32 | 33 | var result = JsonConvert.DeserializeObject(stringResponse); 34 | 35 | return result; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /UserManagement.API.IntegrationTests/User/UserAPI.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.API.IntegrationTests.User 2 | { 3 | using Xunit; 4 | using System.Threading.Tasks; 5 | using Shouldly; 6 | using UserManagement.Application.User.Commands; 7 | using System.Net; 8 | using System; 9 | 10 | public class UserAPI : IClassFixture> 11 | { 12 | private readonly CustomWebApplicationFactory factory; 13 | 14 | public UserAPI(CustomWebApplicationFactory factory) 15 | { 16 | this.factory = factory; 17 | } 18 | 19 | [Fact] 20 | public async Task GivenValidGetAllUserQuery_ReturnSuccessObject() 21 | { 22 | await AddNewUser(); 23 | var client = this.factory.GetAnonymousClient(); 24 | var response = await client.GetAsync($"api/User/GetAll"); 25 | response.EnsureSuccessStatusCode(); 26 | } 27 | 28 | [Fact] 29 | public async Task GivenValidGetUserQuery_ReturnSuccessObject() 30 | { 31 | var res = await AddNewUser(); 32 | var client = this.factory.GetAnonymousClient(); 33 | var response = await client.GetAsync($"api/User/Get?userID={res}"); 34 | var result = response.Content.ReadAsStringAsync().Result; 35 | response.EnsureSuccessStatusCode(); 36 | } 37 | 38 | [Fact] 39 | public async Task GivenValidAddUserQuery_ReturnSuccessObject() 40 | { 41 | var res = await AddNewUser(); 42 | res.ShouldBeGreaterThan(0); 43 | } 44 | 45 | [Fact] 46 | public async Task GivenValidUpdateUserQuery_ReturnSuccessObject() 47 | { 48 | var res = await AddNewUser(); 49 | var client = this.factory.GetAnonymousClient(); 50 | var command = new UpdateUserCommand 51 | { 52 | UserID = res, 53 | City = "SpringField", 54 | Country = "USA", 55 | State = "VA", 56 | Zip = "66006", 57 | PhoneNumber = "888-88-8888", 58 | }; 59 | 60 | var content = IntegrationTestHelper.GetRequestContent(command); 61 | var response = await client.PutAsync($"api/User/Put", content); 62 | response.EnsureSuccessStatusCode(); 63 | 64 | } 65 | 66 | [Fact] 67 | public async Task GivenValidDeleteUserQuery_ReturnSuccessObject() 68 | { 69 | var res = await AddNewUser(); 70 | var client = this.factory.GetAnonymousClient(); 71 | var response = await client.DeleteAsync($"api/User/Delete?userID={res}"); 72 | response.EnsureSuccessStatusCode(); 73 | } 74 | 75 | [Fact] 76 | public async Task GivenValidGetUserQuery_SendNullUserID_ReturnErrorCode() 77 | { 78 | var client = this.factory.GetAnonymousClient(); 79 | var response = await client.GetAsync($"api/User/Get"); 80 | var result = response.Content.ReadAsStringAsync().Result; 81 | result.ShouldContain("User ID is required"); 82 | response.StatusCode.ShouldBe(HttpStatusCode.BadRequest); 83 | } 84 | 85 | private async Task AddNewUser() 86 | { 87 | var client = this.factory.GetAnonymousClient(); 88 | var command = new AddUserCommand 89 | { 90 | FirstName = "Test", 91 | LastName = "Doe", 92 | City = "Falls Chruch", 93 | Country = "USA", 94 | State = "VA", 95 | Zip = "22044", 96 | DOB = new DateTime(1980, 01, 01), 97 | EmailAddress = "jdoe@fullstackhub.io", 98 | Gender = "M", 99 | PhoneNumber = "444-443-4444", 100 | }; 101 | 102 | var content = IntegrationTestHelper.GetRequestContent(command); 103 | var response = await client.PostAsync($"api/User/Post", content); 104 | return Convert.ToInt32(response.Content.ReadAsStringAsync().Result); 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /UserManagement.API.IntegrationTests/UserManagement.API.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/.browserslistrc: -------------------------------------------------------------------------------- 1 | # This file is used by the build system to adjust CSS and JS output to support the specified browsers below. 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | 5 | # For the full list of supported browsers by the Angular framework, please see: 6 | # https://angular.io/guide/browser-support 7 | 8 | # You can see what browsers were selected by your queries by running: 9 | # npx browserslist 10 | 11 | last 1 Chrome version 12 | last 1 Firefox version 13 | last 2 Edge major versions 14 | last 2 Safari major versions 15 | last 2 iOS major versions 16 | Firefox ESR 17 | not IE 9-10 # Angular support for IE 9-10 has been deprecated and will be removed as of Angular v11. To opt-in, remove the 'not' prefix on this line. 18 | not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line. 19 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | /out-tsc 7 | # Only exists if Bazel was run 8 | /bazel-out 9 | 10 | # dependencies 11 | /node_modules 12 | 13 | # profiling files 14 | chrome-profiler-events*.json 15 | speed-measure-plugin*.json 16 | 17 | # IDEs and editors 18 | /.idea 19 | .project 20 | .classpath 21 | .c9/ 22 | *.launch 23 | .settings/ 24 | *.sublime-workspace 25 | 26 | # IDE - VSCode 27 | .vscode/* 28 | !.vscode/settings.json 29 | !.vscode/tasks.json 30 | !.vscode/launch.json 31 | !.vscode/extensions.json 32 | .history/* 33 | 34 | # misc 35 | /.sass-cache 36 | /connect.lock 37 | /coverage 38 | /libpeerconnection.log 39 | npm-debug.log 40 | yarn-error.log 41 | testem.log 42 | /typings 43 | 44 | # System Files 45 | .DS_Store 46 | Thumbs.db 47 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/README.md: -------------------------------------------------------------------------------- 1 | # ClientApp 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 10.1.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `--prod` flag for a production build. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/). 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md). 28 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "ClientApp": { 7 | "projectType": "application", 8 | "schematics": {}, 9 | "root": "", 10 | "sourceRoot": "src", 11 | "prefix": "app", 12 | "architect": { 13 | "build": { 14 | "builder": "@angular-devkit/build-angular:browser", 15 | "options": { 16 | "outputPath": "dist/ClientApp", 17 | "index": "src/index.html", 18 | "main": "src/main.ts", 19 | "polyfills": "src/polyfills.ts", 20 | "tsConfig": "tsconfig.app.json", 21 | "aot": true, 22 | "assets": [ 23 | "src/favicon.ico", 24 | "src/assets" 25 | ], 26 | "styles": [ 27 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 28 | "src/styles.css" 29 | ], 30 | "scripts": [] 31 | }, 32 | "configurations": { 33 | "production": { 34 | "fileReplacements": [ 35 | { 36 | "replace": "src/environments/environment.ts", 37 | "with": "src/environments/environment.prod.ts" 38 | } 39 | ], 40 | "optimization": true, 41 | "outputHashing": "all", 42 | "sourceMap": false, 43 | "extractCss": true, 44 | "namedChunks": false, 45 | "extractLicenses": true, 46 | "vendorChunk": false, 47 | "buildOptimizer": true, 48 | "budgets": [ 49 | { 50 | "type": "initial", 51 | "maximumWarning": "2mb", 52 | "maximumError": "5mb" 53 | }, 54 | { 55 | "type": "anyComponentStyle", 56 | "maximumWarning": "6kb", 57 | "maximumError": "10kb" 58 | } 59 | ] 60 | } 61 | } 62 | }, 63 | "serve": { 64 | "builder": "@angular-devkit/build-angular:dev-server", 65 | "options": { 66 | "browserTarget": "ClientApp:build" 67 | }, 68 | "configurations": { 69 | "production": { 70 | "browserTarget": "ClientApp:build:production" 71 | } 72 | } 73 | }, 74 | "extract-i18n": { 75 | "builder": "@angular-devkit/build-angular:extract-i18n", 76 | "options": { 77 | "browserTarget": "ClientApp:build" 78 | } 79 | }, 80 | "test": { 81 | "builder": "@angular-devkit/build-angular:karma", 82 | "options": { 83 | "main": "src/test.ts", 84 | "polyfills": "src/polyfills.ts", 85 | "tsConfig": "tsconfig.spec.json", 86 | "karmaConfig": "karma.conf.js", 87 | "assets": [ 88 | "src/favicon.ico", 89 | "src/assets" 90 | ], 91 | "styles": [ 92 | "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", 93 | "src/styles.css" 94 | ], 95 | "scripts": [] 96 | } 97 | }, 98 | "lint": { 99 | "builder": "@angular-devkit/build-angular:tslint", 100 | "options": { 101 | "tsConfig": [ 102 | "tsconfig.app.json", 103 | "tsconfig.spec.json", 104 | "e2e/tsconfig.json" 105 | ], 106 | "exclude": [ 107 | "**/node_modules/**" 108 | ] 109 | } 110 | }, 111 | "e2e": { 112 | "builder": "@angular-devkit/build-angular:protractor", 113 | "options": { 114 | "protractorConfig": "e2e/protractor.conf.js", 115 | "devServerTarget": "ClientApp:serve" 116 | }, 117 | "configurations": { 118 | "production": { 119 | "devServerTarget": "ClientApp:serve:production" 120 | } 121 | } 122 | } 123 | } 124 | } 125 | }, 126 | "defaultProject": "ClientApp", 127 | "cli": { 128 | "analytics": false 129 | } 130 | } -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Protractor configuration file, see link for more information 3 | // https://github.com/angular/protractor/blob/master/lib/config.ts 4 | 5 | const { SpecReporter, StacktraceOption } = require('jasmine-spec-reporter'); 6 | 7 | /** 8 | * @type { import("protractor").Config } 9 | */ 10 | exports.config = { 11 | allScriptsTimeout: 11000, 12 | specs: [ 13 | './src/**/*.e2e-spec.ts' 14 | ], 15 | capabilities: { 16 | browserName: 'chrome' 17 | }, 18 | directConnect: true, 19 | baseUrl: 'http://localhost:4200/', 20 | framework: 'jasmine', 21 | jasmineNodeOpts: { 22 | showColors: true, 23 | defaultTimeoutInterval: 30000, 24 | print: function() {} 25 | }, 26 | onPrepare() { 27 | require('ts-node').register({ 28 | project: require('path').join(__dirname, './tsconfig.json') 29 | }); 30 | jasmine.getEnv().addReporter(new SpecReporter({ 31 | spec: { 32 | displayStacktrace: StacktraceOption.PRETTY 33 | } 34 | })); 35 | } 36 | }; -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | import { browser, logging } from 'protractor'; 3 | 4 | describe('workspace-project App', () => { 5 | let page: AppPage; 6 | 7 | beforeEach(() => { 8 | page = new AppPage(); 9 | }); 10 | 11 | it('should display welcome message', () => { 12 | page.navigateTo(); 13 | expect(page.getTitleText()).toEqual('ClientApp app is running!'); 14 | }); 15 | 16 | afterEach(async () => { 17 | // Assert that there are no errors emitted from the browser 18 | const logs = await browser.manage().logs().get(logging.Type.BROWSER); 19 | expect(logs).not.toContain(jasmine.objectContaining({ 20 | level: logging.Level.SEVERE, 21 | } as logging.Entry)); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo(): Promise { 5 | return browser.get(browser.baseUrl) as Promise; 6 | } 7 | 8 | getTitleText(): Promise { 9 | return element(by.css('app-root .content span')).getText() as Promise; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "../tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "../out-tsc/e2e", 6 | "module": "commonjs", 7 | "target": "es2018", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, './coverage/ClientApp'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false, 30 | restartOnFileChange: true 31 | }); 32 | }; 33 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client-app", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "test": "ng test", 9 | "lint": "ng lint", 10 | "e2e": "ng e2e" 11 | }, 12 | "private": true, 13 | "dependencies": { 14 | "@angular/animations": "~10.2.0", 15 | "@angular/cdk": "^10.2.7", 16 | "@angular/common": "~10.2.0", 17 | "@angular/compiler": "~10.2.0", 18 | "@angular/core": "~10.2.0", 19 | "@angular/forms": "~10.2.0", 20 | "@angular/material": "^10.2.7", 21 | "@angular/platform-browser": "~10.2.0", 22 | "@angular/platform-browser-dynamic": "~10.2.0", 23 | "@angular/router": "~10.2.0", 24 | "rxjs": "~6.6.0", 25 | "tslib": "^2.0.0", 26 | "zone.js": "~0.10.2" 27 | }, 28 | "devDependencies": { 29 | "@angular-devkit/build-angular": "^0.1002.0", 30 | "@angular/cli": "~10.2.0", 31 | "@angular/compiler-cli": "~10.2.0", 32 | "@types/jasmine": "~3.5.0", 33 | "@types/jasminewd2": "~2.0.3", 34 | "@types/node": "^12.11.1", 35 | "codelyzer": "^6.0.0", 36 | "jasmine-core": "~3.6.0", 37 | "jasmine-spec-reporter": "~5.0.0", 38 | "karma": "~5.0.0", 39 | "karma-chrome-launcher": "~3.1.0", 40 | "karma-coverage-istanbul-reporter": "~3.0.2", 41 | "karma-jasmine": "~4.0.0", 42 | "karma-jasmine-html-reporter": "^1.5.0", 43 | "protractor": "~7.0.0", 44 | "ts-node": "~8.3.0", 45 | "tslint": "~6.1.0", 46 | "typescript": "~4.0.2" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { Routes, RouterModule } from '@angular/router'; 3 | import { ListUsersComponent } from './usermanagement/list-users/list-users.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | redirectTo: 'home', 9 | pathMatch: 'full' 10 | }, 11 | { 12 | path: 'home', 13 | component: ListUsersComponent, 14 | } 15 | , { path: '**', redirectTo: 'home' } 16 | ]; 17 | 18 | @NgModule({ 19 | imports: [RouterModule.forRoot(routes)], 20 | exports: [RouterModule] 21 | }) 22 | export class AppRoutingModule { } -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/app.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhub-io/UserManagement/4e230acbdc07142dd87c05ed56fe4459f9299be8/UserManagement.API/ClientApp/src/app/app.component.css -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 4 |
5 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async () => { 7 | await TestBed.configureTestingModule({ 8 | imports: [ 9 | RouterTestingModule 10 | ], 11 | declarations: [ 12 | AppComponent 13 | ], 14 | }).compileComponents(); 15 | }); 16 | 17 | it('should create the app', () => { 18 | const fixture = TestBed.createComponent(AppComponent); 19 | const app = fixture.componentInstance; 20 | expect(app).toBeTruthy(); 21 | }); 22 | 23 | it(`should have as title 'ClientApp'`, () => { 24 | const fixture = TestBed.createComponent(AppComponent); 25 | const app = fixture.componentInstance; 26 | expect(app.title).toEqual('ClientApp'); 27 | }); 28 | 29 | it('should render title', () => { 30 | const fixture = TestBed.createComponent(AppComponent); 31 | fixture.detectChanges(); 32 | const compiled = fixture.nativeElement; 33 | expect(compiled.querySelector('.content span').textContent).toContain('ClientApp app is running!'); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.css'] 7 | }) 8 | export class AppComponent { 9 | title = 'ClientApp'; 10 | } 11 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { AppRoutingModule } from './app-routing.module'; 5 | import { AppComponent } from './app.component'; 6 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 7 | import { ListUsersComponent } from './usermanagement/list-users/list-users.component'; 8 | import { ManageUserComponent } from './usermanagement/manage-user/manage-user.component'; 9 | import { HttpClientModule } from '@angular/common/http'; 10 | import { CommonModule } from '@angular/common'; 11 | import { MaterialModule } from './material-module'; 12 | 13 | @NgModule({ 14 | declarations: [ 15 | AppComponent, 16 | ListUsersComponent, 17 | ManageUserComponent 18 | ], 19 | imports: [ 20 | CommonModule, 21 | BrowserModule, 22 | AppRoutingModule, 23 | BrowserAnimationsModule, 24 | HttpClientModule, 25 | MaterialModule, 26 | FormsModule, 27 | ReactiveFormsModule 28 | ], 29 | providers: [], 30 | entryComponents:[ManageUserComponent], 31 | bootstrap: [AppComponent] 32 | }) 33 | export class AppModule { } 34 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/material-module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {A11yModule} from '@angular/cdk/a11y'; 3 | import {DragDropModule} from '@angular/cdk/drag-drop'; 4 | import {PortalModule} from '@angular/cdk/portal'; 5 | import {ScrollingModule} from '@angular/cdk/scrolling'; 6 | import {CdkStepperModule} from '@angular/cdk/stepper'; 7 | import {CdkTableModule} from '@angular/cdk/table'; 8 | import {CdkTreeModule} from '@angular/cdk/tree'; 9 | import {MatAutocompleteModule} from '@angular/material/autocomplete'; 10 | import {MatBadgeModule} from '@angular/material/badge'; 11 | import {MatBottomSheetModule} from '@angular/material/bottom-sheet'; 12 | import {MatButtonModule} from '@angular/material/button'; 13 | import {MatButtonToggleModule} from '@angular/material/button-toggle'; 14 | import {MatCardModule} from '@angular/material/card'; 15 | import {MatCheckboxModule} from '@angular/material/checkbox'; 16 | import {MatChipsModule} from '@angular/material/chips'; 17 | import {MatStepperModule} from '@angular/material/stepper'; 18 | import {MatDatepickerModule} from '@angular/material/datepicker'; 19 | import {MatDialogModule} from '@angular/material/dialog'; 20 | import {MatDividerModule} from '@angular/material/divider'; 21 | import {MatExpansionModule} from '@angular/material/expansion'; 22 | import {MatGridListModule} from '@angular/material/grid-list'; 23 | import {MatIconModule} from '@angular/material/icon'; 24 | import {MatInputModule} from '@angular/material/input'; 25 | import {MatListModule} from '@angular/material/list'; 26 | import {MatMenuModule} from '@angular/material/menu'; 27 | import {MatNativeDateModule, MatRippleModule} from '@angular/material/core'; 28 | import {MatPaginatorModule} from '@angular/material/paginator'; 29 | import {MatProgressBarModule} from '@angular/material/progress-bar'; 30 | import {MatProgressSpinnerModule} from '@angular/material/progress-spinner'; 31 | import {MatRadioModule} from '@angular/material/radio'; 32 | import {MatSelectModule} from '@angular/material/select'; 33 | import {MatSidenavModule} from '@angular/material/sidenav'; 34 | import {MatSliderModule} from '@angular/material/slider'; 35 | import {MatSlideToggleModule} from '@angular/material/slide-toggle'; 36 | import {MatSnackBarModule} from '@angular/material/snack-bar'; 37 | import {MatSortModule} from '@angular/material/sort'; 38 | import {MatTableModule} from '@angular/material/table'; 39 | import {MatTabsModule} from '@angular/material/tabs'; 40 | import {MatToolbarModule} from '@angular/material/toolbar'; 41 | import {MatTooltipModule} from '@angular/material/tooltip'; 42 | import {MatTreeModule} from '@angular/material/tree'; 43 | 44 | @NgModule({ 45 | exports: [ 46 | A11yModule, 47 | CdkStepperModule, 48 | CdkTableModule, 49 | CdkTreeModule, 50 | DragDropModule, 51 | MatAutocompleteModule, 52 | MatBadgeModule, 53 | MatBottomSheetModule, 54 | MatButtonModule, 55 | MatButtonToggleModule, 56 | MatCardModule, 57 | MatCheckboxModule, 58 | MatChipsModule, 59 | MatStepperModule, 60 | MatDatepickerModule, 61 | MatDialogModule, 62 | MatDividerModule, 63 | MatExpansionModule, 64 | MatGridListModule, 65 | MatIconModule, 66 | MatInputModule, 67 | MatListModule, 68 | MatMenuModule, 69 | MatNativeDateModule, 70 | MatPaginatorModule, 71 | MatProgressBarModule, 72 | MatProgressSpinnerModule, 73 | MatRadioModule, 74 | MatRippleModule, 75 | MatSelectModule, 76 | MatSidenavModule, 77 | MatSliderModule, 78 | MatSlideToggleModule, 79 | MatSnackBarModule, 80 | MatSortModule, 81 | MatTableModule, 82 | MatTabsModule, 83 | MatToolbarModule, 84 | MatTooltipModule, 85 | MatTreeModule, 86 | PortalModule, 87 | ScrollingModule, 88 | ] 89 | }) 90 | export class MaterialModule {} -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/shared/enum.ts: -------------------------------------------------------------------------------- 1 | export enum DBOperation { 2 | create, 3 | update, 4 | delete 5 | } -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/shared/util.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatSnackBar } from "@angular/material/snack-bar"; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | 8 | export class UtilService { 9 | 10 | constructor(private _snackBar: MatSnackBar) { } 11 | 12 | openCrudDialog(dialog, component, width) { 13 | const dialogRef = dialog.open(component, { 14 | disableClose: true, 15 | width: width 16 | }); 17 | return dialogRef; 18 | } 19 | 20 | openSnackBar(message: string) { 21 | this._snackBar.open(message, 'Close', { 22 | duration: 3000, 23 | }); 24 | } 25 | } -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/user-management-api.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | //---------------------- 4 | // 5 | // Generated using the NSwag toolchain v13.9.4.0 (NJsonSchema v10.3.1.0 (Newtonsoft.Json v12.0.0.0)) (http://NSwag.org) 6 | // 7 | //---------------------- 8 | // ReSharper disable InconsistentNaming 9 | 10 | import { mergeMap as _observableMergeMap, catchError as _observableCatch } from 'rxjs/operators'; 11 | import { Observable, throwError as _observableThrow, of as _observableOf } from 'rxjs'; 12 | import { Injectable, Inject, Optional, InjectionToken } from '@angular/core'; 13 | import { HttpClient, HttpHeaders, HttpResponse, HttpResponseBase } from '@angular/common/http'; 14 | 15 | export const API_BASE_URL = new InjectionToken('API_BASE_URL'); 16 | 17 | export interface IUserService { 18 | get(userID: number | undefined): Observable; 19 | getAll(): Observable; 20 | post(command: AddUserCommand): Observable; 21 | put(command: UpdateUserCommand): Observable; 22 | delete(userID: number | undefined): Observable; 23 | } 24 | 25 | @Injectable({ 26 | providedIn: 'root' 27 | }) 28 | export class UserService implements IUserService { 29 | private http: HttpClient; 30 | private baseUrl: string; 31 | protected jsonParseReviver: ((key: string, value: any) => any) | undefined = undefined; 32 | 33 | constructor(@Inject(HttpClient) http: HttpClient, @Optional() @Inject(API_BASE_URL) baseUrl?: string) { 34 | this.http = http; 35 | this.baseUrl = baseUrl !== undefined && baseUrl !== null ? baseUrl : ""; 36 | } 37 | 38 | get(userID: number | undefined): Observable { 39 | let url_ = this.baseUrl + "/api/User/Get?"; 40 | if (userID === null) 41 | throw new Error("The parameter 'userID' cannot be null."); 42 | else if (userID !== undefined) 43 | url_ += "userID=" + encodeURIComponent("" + userID) + "&"; 44 | url_ = url_.replace(/[?&]$/, ""); 45 | 46 | let options_ : any = { 47 | observe: "response", 48 | responseType: "blob", 49 | withCredentials: true, 50 | headers: new HttpHeaders({ 51 | "Accept": "application/json" 52 | }) 53 | }; 54 | 55 | return this.http.request("get", url_, options_).pipe(_observableMergeMap((response_ : any) => { 56 | return this.processGet(response_); 57 | })).pipe(_observableCatch((response_: any) => { 58 | if (response_ instanceof HttpResponseBase) { 59 | try { 60 | return this.processGet(response_); 61 | } catch (e) { 62 | return >_observableThrow(e); 63 | } 64 | } else 65 | return >_observableThrow(response_); 66 | })); 67 | } 68 | 69 | protected processGet(response: HttpResponseBase): Observable { 70 | const status = response.status; 71 | const responseBlob = 72 | response instanceof HttpResponse ? response.body : 73 | (response).error instanceof Blob ? (response).error : undefined; 74 | 75 | let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }} 76 | if (status === 200) { 77 | return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => { 78 | let result200: any = null; 79 | let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); 80 | result200 = UserVM.fromJS(resultData200); 81 | return _observableOf(result200); 82 | })); 83 | } else if (status !== 200 && status !== 204) { 84 | return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => { 85 | return throwException("An unexpected server error occurred.", status, _responseText, _headers); 86 | })); 87 | } 88 | return _observableOf(null); 89 | } 90 | 91 | getAll(): Observable { 92 | let url_ = this.baseUrl + "/api/User/GetAll"; 93 | url_ = url_.replace(/[?&]$/, ""); 94 | 95 | let options_ : any = { 96 | observe: "response", 97 | responseType: "blob", 98 | withCredentials: true, 99 | headers: new HttpHeaders({ 100 | "Accept": "application/json" 101 | }) 102 | }; 103 | 104 | return this.http.request("get", url_, options_).pipe(_observableMergeMap((response_ : any) => { 105 | return this.processGetAll(response_); 106 | })).pipe(_observableCatch((response_: any) => { 107 | if (response_ instanceof HttpResponseBase) { 108 | try { 109 | return this.processGetAll(response_); 110 | } catch (e) { 111 | return >_observableThrow(e); 112 | } 113 | } else 114 | return >_observableThrow(response_); 115 | })); 116 | } 117 | 118 | protected processGetAll(response: HttpResponseBase): Observable { 119 | const status = response.status; 120 | const responseBlob = 121 | response instanceof HttpResponse ? response.body : 122 | (response).error instanceof Blob ? (response).error : undefined; 123 | 124 | let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }} 125 | if (status === 200) { 126 | return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => { 127 | let result200: any = null; 128 | let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); 129 | result200 = UserVM.fromJS(resultData200); 130 | return _observableOf(result200); 131 | })); 132 | } else if (status !== 200 && status !== 204) { 133 | return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => { 134 | return throwException("An unexpected server error occurred.", status, _responseText, _headers); 135 | })); 136 | } 137 | return _observableOf(null); 138 | } 139 | 140 | post(command: AddUserCommand): Observable { 141 | let url_ = this.baseUrl + "/api/User/Post"; 142 | url_ = url_.replace(/[?&]$/, ""); 143 | 144 | const content_ = JSON.stringify(command); 145 | 146 | let options_ : any = { 147 | body: content_, 148 | observe: "response", 149 | responseType: "blob", 150 | withCredentials: true, 151 | headers: new HttpHeaders({ 152 | "Content-Type": "application/json", 153 | "Accept": "application/json" 154 | }) 155 | }; 156 | 157 | return this.http.request("post", url_, options_).pipe(_observableMergeMap((response_ : any) => { 158 | return this.processPost(response_); 159 | })).pipe(_observableCatch((response_: any) => { 160 | if (response_ instanceof HttpResponseBase) { 161 | try { 162 | return this.processPost(response_); 163 | } catch (e) { 164 | return >_observableThrow(e); 165 | } 166 | } else 167 | return >_observableThrow(response_); 168 | })); 169 | } 170 | 171 | protected processPost(response: HttpResponseBase): Observable { 172 | const status = response.status; 173 | const responseBlob = 174 | response instanceof HttpResponse ? response.body : 175 | (response).error instanceof Blob ? (response).error : undefined; 176 | 177 | let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }} 178 | if (status === 200) { 179 | return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => { 180 | let result200: any = null; 181 | let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); 182 | result200 = resultData200 !== undefined ? resultData200 : null; 183 | return _observableOf(result200); 184 | })); 185 | } else if (status !== 200 && status !== 204) { 186 | return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => { 187 | return throwException("An unexpected server error occurred.", status, _responseText, _headers); 188 | })); 189 | } 190 | return _observableOf(null); 191 | } 192 | 193 | put(command: UpdateUserCommand): Observable { 194 | let url_ = this.baseUrl + "/api/User/Put"; 195 | url_ = url_.replace(/[?&]$/, ""); 196 | 197 | const content_ = JSON.stringify(command); 198 | 199 | let options_ : any = { 200 | body: content_, 201 | observe: "response", 202 | responseType: "blob", 203 | withCredentials: true, 204 | headers: new HttpHeaders({ 205 | "Content-Type": "application/json", 206 | "Accept": "application/json" 207 | }) 208 | }; 209 | 210 | return this.http.request("put", url_, options_).pipe(_observableMergeMap((response_ : any) => { 211 | return this.processPut(response_); 212 | })).pipe(_observableCatch((response_: any) => { 213 | if (response_ instanceof HttpResponseBase) { 214 | try { 215 | return this.processPut(response_); 216 | } catch (e) { 217 | return >_observableThrow(e); 218 | } 219 | } else 220 | return >_observableThrow(response_); 221 | })); 222 | } 223 | 224 | protected processPut(response: HttpResponseBase): Observable { 225 | const status = response.status; 226 | const responseBlob = 227 | response instanceof HttpResponse ? response.body : 228 | (response).error instanceof Blob ? (response).error : undefined; 229 | 230 | let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }} 231 | if (status === 200) { 232 | return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => { 233 | let result200: any = null; 234 | let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); 235 | result200 = resultData200 !== undefined ? resultData200 : null; 236 | return _observableOf(result200); 237 | })); 238 | } else if (status !== 200 && status !== 204) { 239 | return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => { 240 | return throwException("An unexpected server error occurred.", status, _responseText, _headers); 241 | })); 242 | } 243 | return _observableOf(null); 244 | } 245 | 246 | delete(userID: number | undefined): Observable { 247 | let url_ = this.baseUrl + "/api/User/Delete?"; 248 | if (userID === null) 249 | throw new Error("The parameter 'userID' cannot be null."); 250 | else if (userID !== undefined) 251 | url_ += "userID=" + encodeURIComponent("" + userID) + "&"; 252 | url_ = url_.replace(/[?&]$/, ""); 253 | 254 | let options_ : any = { 255 | observe: "response", 256 | responseType: "blob", 257 | withCredentials: true, 258 | headers: new HttpHeaders({ 259 | "Accept": "application/json" 260 | }) 261 | }; 262 | 263 | return this.http.request("delete", url_, options_).pipe(_observableMergeMap((response_ : any) => { 264 | return this.processDelete(response_); 265 | })).pipe(_observableCatch((response_: any) => { 266 | if (response_ instanceof HttpResponseBase) { 267 | try { 268 | return this.processDelete(response_); 269 | } catch (e) { 270 | return >_observableThrow(e); 271 | } 272 | } else 273 | return >_observableThrow(response_); 274 | })); 275 | } 276 | 277 | protected processDelete(response: HttpResponseBase): Observable { 278 | const status = response.status; 279 | const responseBlob = 280 | response instanceof HttpResponse ? response.body : 281 | (response).error instanceof Blob ? (response).error : undefined; 282 | 283 | let _headers: any = {}; if (response.headers) { for (let key of response.headers.keys()) { _headers[key] = response.headers.get(key); }} 284 | if (status === 200) { 285 | return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => { 286 | let result200: any = null; 287 | let resultData200 = _responseText === "" ? null : JSON.parse(_responseText, this.jsonParseReviver); 288 | result200 = resultData200 !== undefined ? resultData200 : null; 289 | return _observableOf(result200); 290 | })); 291 | } else if (status !== 200 && status !== 204) { 292 | return blobToText(responseBlob).pipe(_observableMergeMap(_responseText => { 293 | return throwException("An unexpected server error occurred.", status, _responseText, _headers); 294 | })); 295 | } 296 | return _observableOf(null); 297 | } 298 | } 299 | 300 | export class UserVM implements IUserVM { 301 | userList?: UserDTO[] | undefined; 302 | 303 | constructor(data?: IUserVM) { 304 | if (data) { 305 | for (var property in data) { 306 | if (data.hasOwnProperty(property)) 307 | (this)[property] = (data)[property]; 308 | } 309 | } 310 | } 311 | 312 | init(_data?: any) { 313 | if (_data) { 314 | if (Array.isArray(_data["userList"])) { 315 | this.userList = [] as any; 316 | for (let item of _data["userList"]) 317 | this.userList!.push(UserDTO.fromJS(item)); 318 | } 319 | } 320 | } 321 | 322 | static fromJS(data: any): UserVM { 323 | data = typeof data === 'object' ? data : {}; 324 | let result = new UserVM(); 325 | result.init(data); 326 | return result; 327 | } 328 | 329 | toJSON(data?: any) { 330 | data = typeof data === 'object' ? data : {}; 331 | if (Array.isArray(this.userList)) { 332 | data["userList"] = []; 333 | for (let item of this.userList) 334 | data["userList"].push(item.toJSON()); 335 | } 336 | return data; 337 | } 338 | } 339 | 340 | export interface IUserVM { 341 | userList?: UserDTO[] | undefined; 342 | } 343 | 344 | export class UserDTO implements IUserDTO { 345 | userID?: number; 346 | salutation?: string | undefined; 347 | firstName?: string | undefined; 348 | lastName?: string | undefined; 349 | dob?: Date; 350 | age?: number; 351 | gender?: string | undefined; 352 | emailAddress?: string | undefined; 353 | phoneNumber?: string | undefined; 354 | city?: string | undefined; 355 | state?: string | undefined; 356 | zip?: string | undefined; 357 | country?: string | undefined; 358 | 359 | constructor(data?: IUserDTO) { 360 | if (data) { 361 | for (var property in data) { 362 | if (data.hasOwnProperty(property)) 363 | (this)[property] = (data)[property]; 364 | } 365 | } 366 | } 367 | 368 | init(_data?: any) { 369 | if (_data) { 370 | this.userID = _data["userID"]; 371 | this.salutation = _data["salutation"]; 372 | this.firstName = _data["firstName"]; 373 | this.lastName = _data["lastName"]; 374 | this.dob = _data["dob"] ? new Date(_data["dob"].toString()) : undefined; 375 | this.age = _data["age"]; 376 | this.gender = _data["gender"]; 377 | this.emailAddress = _data["emailAddress"]; 378 | this.phoneNumber = _data["phoneNumber"]; 379 | this.city = _data["city"]; 380 | this.state = _data["state"]; 381 | this.zip = _data["zip"]; 382 | this.country = _data["country"]; 383 | } 384 | } 385 | 386 | static fromJS(data: any): UserDTO { 387 | data = typeof data === 'object' ? data : {}; 388 | let result = new UserDTO(); 389 | result.init(data); 390 | return result; 391 | } 392 | 393 | toJSON(data?: any) { 394 | data = typeof data === 'object' ? data : {}; 395 | data["userID"] = this.userID; 396 | data["salutation"] = this.salutation; 397 | data["firstName"] = this.firstName; 398 | data["lastName"] = this.lastName; 399 | data["dob"] = this.dob ? this.dob.toISOString() : undefined; 400 | data["age"] = this.age; 401 | data["gender"] = this.gender; 402 | data["emailAddress"] = this.emailAddress; 403 | data["phoneNumber"] = this.phoneNumber; 404 | data["city"] = this.city; 405 | data["state"] = this.state; 406 | data["zip"] = this.zip; 407 | data["country"] = this.country; 408 | return data; 409 | } 410 | } 411 | 412 | export interface IUserDTO { 413 | userID?: number; 414 | salutation?: string | undefined; 415 | firstName?: string | undefined; 416 | lastName?: string | undefined; 417 | dob?: Date; 418 | age?: number; 419 | gender?: string | undefined; 420 | emailAddress?: string | undefined; 421 | phoneNumber?: string | undefined; 422 | city?: string | undefined; 423 | state?: string | undefined; 424 | zip?: string | undefined; 425 | country?: string | undefined; 426 | } 427 | 428 | export class AddUserCommand implements IAddUserCommand { 429 | firstName?: string | undefined; 430 | lastName?: string | undefined; 431 | dob?: Date; 432 | gender?: string | undefined; 433 | emailAddress?: string | undefined; 434 | phoneNumber?: string | undefined; 435 | city?: string | undefined; 436 | state?: string | undefined; 437 | zip?: string | undefined; 438 | country?: string | undefined; 439 | 440 | constructor(data?: IAddUserCommand) { 441 | if (data) { 442 | for (var property in data) { 443 | if (data.hasOwnProperty(property)) 444 | (this)[property] = (data)[property]; 445 | } 446 | } 447 | } 448 | 449 | init(_data?: any) { 450 | if (_data) { 451 | this.firstName = _data["firstName"]; 452 | this.lastName = _data["lastName"]; 453 | this.dob = _data["dob"] ? new Date(_data["dob"].toString()) : undefined; 454 | this.gender = _data["gender"]; 455 | this.emailAddress = _data["emailAddress"]; 456 | this.phoneNumber = _data["phoneNumber"]; 457 | this.city = _data["city"]; 458 | this.state = _data["state"]; 459 | this.zip = _data["zip"]; 460 | this.country = _data["country"]; 461 | } 462 | } 463 | 464 | static fromJS(data: any): AddUserCommand { 465 | data = typeof data === 'object' ? data : {}; 466 | let result = new AddUserCommand(); 467 | result.init(data); 468 | return result; 469 | } 470 | 471 | toJSON(data?: any) { 472 | data = typeof data === 'object' ? data : {}; 473 | data["firstName"] = this.firstName; 474 | data["lastName"] = this.lastName; 475 | data["dob"] = this.dob ? this.dob.toISOString() : undefined; 476 | data["gender"] = this.gender; 477 | data["emailAddress"] = this.emailAddress; 478 | data["phoneNumber"] = this.phoneNumber; 479 | data["city"] = this.city; 480 | data["state"] = this.state; 481 | data["zip"] = this.zip; 482 | data["country"] = this.country; 483 | return data; 484 | } 485 | } 486 | 487 | export interface IAddUserCommand { 488 | firstName?: string | undefined; 489 | lastName?: string | undefined; 490 | dob?: Date; 491 | gender?: string | undefined; 492 | emailAddress?: string | undefined; 493 | phoneNumber?: string | undefined; 494 | city?: string | undefined; 495 | state?: string | undefined; 496 | zip?: string | undefined; 497 | country?: string | undefined; 498 | } 499 | 500 | export class UpdateUserCommand implements IUpdateUserCommand { 501 | userID?: number; 502 | phoneNumber?: string | undefined; 503 | city?: string | undefined; 504 | state?: string | undefined; 505 | zip?: string | undefined; 506 | country?: string | undefined; 507 | 508 | constructor(data?: IUpdateUserCommand) { 509 | if (data) { 510 | for (var property in data) { 511 | if (data.hasOwnProperty(property)) 512 | (this)[property] = (data)[property]; 513 | } 514 | } 515 | } 516 | 517 | init(_data?: any) { 518 | if (_data) { 519 | this.userID = _data["userID"]; 520 | this.phoneNumber = _data["phoneNumber"]; 521 | this.city = _data["city"]; 522 | this.state = _data["state"]; 523 | this.zip = _data["zip"]; 524 | this.country = _data["country"]; 525 | } 526 | } 527 | 528 | static fromJS(data: any): UpdateUserCommand { 529 | data = typeof data === 'object' ? data : {}; 530 | let result = new UpdateUserCommand(); 531 | result.init(data); 532 | return result; 533 | } 534 | 535 | toJSON(data?: any) { 536 | data = typeof data === 'object' ? data : {}; 537 | data["userID"] = this.userID; 538 | data["phoneNumber"] = this.phoneNumber; 539 | data["city"] = this.city; 540 | data["state"] = this.state; 541 | data["zip"] = this.zip; 542 | data["country"] = this.country; 543 | return data; 544 | } 545 | } 546 | 547 | export interface IUpdateUserCommand { 548 | userID?: number; 549 | phoneNumber?: string | undefined; 550 | city?: string | undefined; 551 | state?: string | undefined; 552 | zip?: string | undefined; 553 | country?: string | undefined; 554 | } 555 | 556 | export class SwaggerException extends Error { 557 | message: string; 558 | status: number; 559 | response: string; 560 | headers: { [key: string]: any; }; 561 | result: any; 562 | 563 | constructor(message: string, status: number, response: string, headers: { [key: string]: any; }, result: any) { 564 | super(); 565 | 566 | this.message = message; 567 | this.status = status; 568 | this.response = response; 569 | this.headers = headers; 570 | this.result = result; 571 | } 572 | 573 | protected isSwaggerException = true; 574 | 575 | static isSwaggerException(obj: any): obj is SwaggerException { 576 | return obj.isSwaggerException === true; 577 | } 578 | } 579 | 580 | function throwException(message: string, status: number, response: string, headers: { [key: string]: any; }, result?: any): Observable { 581 | if (result !== null && result !== undefined) 582 | return _observableThrow(result); 583 | else 584 | return _observableThrow(new SwaggerException(message, status, response, headers, null)); 585 | } 586 | 587 | function blobToText(blob: any): Observable { 588 | return new Observable((observer: any) => { 589 | if (!blob) { 590 | observer.next(""); 591 | observer.complete(); 592 | } else { 593 | let reader = new FileReader(); 594 | reader.onload = event => { 595 | observer.next((event.target).result); 596 | observer.complete(); 597 | }; 598 | reader.readAsText(blob); 599 | } 600 | }); 601 | } -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/usermanagement/list-users/list-users.component.css: -------------------------------------------------------------------------------- 1 | .condition-header { 2 | background-color: rgb(245, 245, 245) !important; 3 | border-bottom: 1px solid rgb(232, 232, 232) !important; 4 | } 5 | 6 | .condtion-header { 7 | font-size: 14pt !important; 8 | color: #221b1b; 9 | } 10 | 11 | .search-button{ 12 | width:85px; 13 | } 14 | 15 | .clear-button{ 16 | width:30px; 17 | text-align: right; 18 | } 19 | 20 | 21 | 22 | /* Structure */ 23 | .example-container { 24 | position: relative; 25 | /* min-height: 200px; */ 26 | } 27 | 28 | .example-table-container { 29 | position: relative; 30 | /* max-height: 200px; */ 31 | overflow-y: auto; 32 | } 33 | 34 | table { 35 | width: 100%; 36 | } 37 | 38 | .example-loading-shade { 39 | position: absolute; 40 | top: 0; 41 | left: 0; 42 | bottom: 56px; 43 | right: 0; 44 | background: rgba(0, 0, 0, 0.15); 45 | z-index: 1; 46 | display: flex; 47 | align-items: center; 48 | justify-content: center; 49 | } 50 | 51 | .example-rate-limit-reached { 52 | color: #980000; 53 | max-width: 360px; 54 | text-align: center; 55 | } 56 | 57 | /* Column Widths */ 58 | .mat-column-rentKey , 59 | .mat-column-rentLoc, 60 | .mat-column-rentFloor, 61 | .mat-column-rentOffice, 62 | .mat-column-empID, 63 | .mat-column-rentSqft, 64 | .mat-column-owningOrg 65 | { 66 | max-width: 90px !important; 67 | } 68 | 69 | .mat-column-created { 70 | max-width: 124px; 71 | } -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/usermanagement/list-users/list-users.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Users Management 4 | 5 | 6 |
7 |
8 |
9 | 10 |
11 |
12 | 13 | Filter 14 | 15 | 16 |
17 |
18 | 21 |
22 |
23 |
24 | 26 | 27 | 30 | 31 | 32 | 33 | 34 | 37 | 38 | 39 | 40 | 41 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | 53 | 54 | 55 | 58 | 59 | 60 | 61 | 62 | 65 | 66 | 67 | 68 | 69 | 72 | 73 | 74 | 75 | 76 | 79 | 80 | 81 | 82 | 83 | 86 | 87 | 88 | 89 | 90 | 93 | 94 | 95 | 96 | 97 | 102 | 103 | 104 | 105 | 106 | 111 | 112 | 113 | 114 |
28 | First Name 29 | {{ row.firstName }} 35 | Last Name 36 | {{ row.lastName }} 42 | Age 43 | {{ row.age }} 49 | Gender 50 | {{ row.gender }} 56 | Email Address 57 | {{ row.emailAddress }} 63 | Phone Number 64 | {{ row.phoneNumber }} 70 | City 71 | {{ row.city }} 77 | State 78 | {{ row.state }} 84 | Zip 85 | {{ row.zip }} 91 | Country 92 | {{ row.country }} 98 | 101 | 107 | 110 |
115 | 116 | 117 |
118 |
119 |
120 |
121 |
122 |
123 |
-------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/usermanagement/list-users/list-users.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ListUsersComponent } from './list-users.component'; 4 | 5 | describe('ListUsersComponent', () => { 6 | let component: ListUsersComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ListUsersComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ListUsersComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/usermanagement/list-users/list-users.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, ViewChild } from '@angular/core'; 2 | import { MatPaginator } from '@angular/material/paginator'; 3 | import { MatSort } from '@angular/material/sort'; 4 | import { MatTableDataSource } from '@angular/material/table'; 5 | import {UserDTO, UserService} from 'src/app/user-management-api'; 6 | import { DBOperation } from 'src/app/shared/enum'; 7 | import { MatDialog } from '@angular/material/dialog'; 8 | import { ManageUserComponent } from '../manage-user/manage-user.component'; 9 | import { UtilService } from 'src/app/shared/util.service'; 10 | 11 | @Component({ 12 | selector: 'app-list-users', 13 | templateUrl: './list-users.component.html', 14 | styleUrls: ['./list-users.component.css'] 15 | }) 16 | 17 | export class ListUsersComponent implements OnInit { 18 | 19 | @ViewChild(MatPaginator, {static: true}) paginator: MatPaginator; 20 | @ViewChild(MatSort, {static: true}) sort: MatSort; 21 | 22 | displayedColumns: string[] = ['firstName', 'lastName', 'age', 'gender', 'emailAddress', 'phoneNumber', 'city', 'state','zip', 'country','edit','delete']; 23 | data: MatTableDataSource; 24 | 25 | constructor(private userService:UserService, public dialog: MatDialog, private util: UtilService) { } 26 | 27 | ngOnInit(): void { 28 | this.loadUser(); 29 | } 30 | 31 | private loadUser(){ 32 | this.userService.getAll().subscribe(user => { 33 | this.data = new MatTableDataSource(user.userList); 34 | this.data.paginator = this.paginator; 35 | this.data.sort = this.sort; 36 | }) 37 | } 38 | 39 | public applyFilter(event: Event) { 40 | const filterValue = (event.target as HTMLInputElement).value; 41 | this.data.filter = filterValue.trim().toLowerCase(); 42 | if (this.data.paginator) { 43 | this.data.paginator.firstPage(); 44 | } 45 | } 46 | 47 | private openManageUser(user: UserDTO = null, dbops:DBOperation, modalTitle:string, modalBtnTitle:string) 48 | { 49 | let dialogRef = this.util.openCrudDialog(this.dialog, ManageUserComponent, '70%'); 50 | dialogRef.componentInstance.dbops = dbops; 51 | dialogRef.componentInstance.modalTitle = modalTitle; 52 | dialogRef.componentInstance.modalBtnTitle = modalBtnTitle; 53 | dialogRef.componentInstance.user = user; 54 | 55 | dialogRef.afterClosed().subscribe(result => { 56 | this.loadUser(); 57 | }); 58 | } 59 | 60 | public addUser() 61 | { 62 | this.openManageUser(null, DBOperation.create, 'Add New User', 'Add'); 63 | } 64 | 65 | public editUser(user: UserDTO) 66 | { 67 | this.openManageUser(user, DBOperation.update, 'Update User', 'Update'); 68 | } 69 | 70 | public deleteUser(user: UserDTO) 71 | { 72 | this.openManageUser(user, DBOperation.delete, 'Delete User', 'Delete'); 73 | } 74 | 75 | } -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/usermanagement/manage-user/manage-user.component.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhub-io/UserManagement/4e230acbdc07142dd87c05ed56fe4459f9299be8/UserManagement.API/ClientApp/src/app/usermanagement/manage-user/manage-user.component.css -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/usermanagement/manage-user/manage-user.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{ modalTitle }} 4 | 5 | 6 |
7 |
8 |
9 |
10 |
11 | 12 | 13 | 14 | First Name is required! 15 | 16 | 17 |
18 |
19 | 20 | 21 | 22 | Last Name is required! 23 | 24 | 25 |
26 |
27 | 28 | Choose a Date of Birth 29 | 30 | 31 | 32 | 33 | DOB is required! 34 | 35 | 36 |
37 |
38 |
39 |
40 | 41 | Male 42 | Female 43 | 44 | 45 | Gender is required! 46 | 47 |
48 |
49 | 50 | 51 | 52 | Email Address is required! 53 | 54 | 55 |
56 |
57 | 58 | 59 | 60 | Phone # is required! 61 | 62 | 63 |
64 |
65 |
66 |
67 | 68 | 69 | 70 | City is required! 71 | 72 | 73 |
74 |
75 | 76 | State 77 | 78 | None 79 | 80 | {{ state.viewValue }} 81 | 82 | 83 | 84 | State is required! 85 | 86 | 87 |
88 |
89 | 90 | 91 | 92 | Zip is required! 93 | 94 | 95 |
96 |
97 |
98 |
99 | 100 | Country 101 | 102 | None 103 | 104 | {{ country.viewValue }} 105 | 106 | 107 | 108 | Country is required! 109 | 110 | 111 |
112 |
113 |
114 |
115 |
116 | 117 | 124 |
125 |
126 |
127 |
-------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/usermanagement/manage-user/manage-user.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { ComponentFixture, TestBed } from '@angular/core/testing'; 2 | 3 | import { ManageUserComponent } from './manage-user.component'; 4 | 5 | describe('ManageUserComponent', () => { 6 | let component: ManageUserComponent; 7 | let fixture: ComponentFixture; 8 | 9 | beforeEach(async () => { 10 | await TestBed.configureTestingModule({ 11 | declarations: [ ManageUserComponent ] 12 | }) 13 | .compileComponents(); 14 | }); 15 | 16 | beforeEach(() => { 17 | fixture = TestBed.createComponent(ManageUserComponent); 18 | component = fixture.componentInstance; 19 | fixture.detectChanges(); 20 | }); 21 | 22 | it('should create', () => { 23 | expect(component).toBeTruthy(); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/app/usermanagement/manage-user/manage-user.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { AddUserCommand, UpdateUserCommand, UserDTO, UserService } from 'src/app/user-management-api'; 3 | import { DBOperation } from 'src/app/shared/enum'; 4 | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; 5 | import { MatDialogRef } from '@angular/material/dialog'; 6 | import { UtilService } from 'src/app/shared/util.service'; 7 | 8 | interface SelectValue { 9 | value: string; 10 | viewValue: string; 11 | } 12 | 13 | @Component({ 14 | selector: 'app-manage-user', 15 | templateUrl: './manage-user.component.html', 16 | styleUrls: ['./manage-user.component.css'] 17 | }) 18 | export class ManageUserComponent implements OnInit { 19 | 20 | modalTitle: string; 21 | modalBtnTitle: string; 22 | dbops: DBOperation; 23 | user: UserDTO; 24 | minDate: Date; 25 | maxDate: Date; 26 | 27 | userFrm: FormGroup = this.fb.group({ 28 | userID: [''], 29 | firstName: ['', [Validators.required, Validators.max(50)]], 30 | lastName: ['', [Validators.required, Validators.max(50)]], 31 | dob: ['', [Validators.required]], 32 | gender: ['', [Validators.required]], 33 | emailAddress: ['', [Validators.required, Validators.email]], 34 | phoneNumber: ['', [Validators.required, Validators.required]], 35 | city: ['', [Validators.required, Validators.max(100)]], 36 | state: ['', [Validators.required, Validators.required]], 37 | zip: ['', [Validators.required, Validators.required]], 38 | country: ['', [Validators.required]] 39 | }); 40 | 41 | states: SelectValue[] = [ 42 | { value: 'AL', viewValue: 'Alabama' }, 43 | { value: 'AK', viewValue: 'Alaska' }, 44 | { value: 'AS', viewValue: 'American Samoa' }, 45 | { value: 'AZ', viewValue: 'Arizona' }, 46 | { value: 'AR', viewValue: 'Arkansas' }, 47 | { value: 'CA', viewValue: 'California' } 48 | ]; 49 | 50 | countries: SelectValue[] = [ 51 | { value: 'US', viewValue: 'United States' }, 52 | { value: 'CA', viewValue: 'Canada' }, 53 | ]; 54 | 55 | gender: SelectValue[] = [ 56 | { value: 'M', viewValue: 'Male' }, 57 | { value: 'F', viewValue: 'Female' }, 58 | ]; 59 | 60 | constructor(private utilService: UtilService, private userService: UserService, private fb: FormBuilder, public dialogRef: MatDialogRef) { 61 | const currentYear = new Date().getFullYear(); 62 | this.minDate = new Date(currentYear - 60, 0, 1); 63 | this.maxDate = new Date(currentYear - 10, 11, 31); 64 | } 65 | 66 | ngOnInit(): void { 67 | debugger 68 | if (this.dbops != DBOperation.create) 69 | this.userFrm.patchValue(this.user); 70 | 71 | if (this.dbops == DBOperation.delete) 72 | this.userFrm.disable(); 73 | 74 | if (this.dbops == DBOperation.update) { 75 | this.userFrm.controls["firstName"].disable(); 76 | this.userFrm.controls["lastName"].disable(); 77 | this.userFrm.controls["dob"].disable(); 78 | this.userFrm.controls["gender"].disable(); 79 | this.userFrm.controls["emailAddress"].disable(); 80 | } 81 | } 82 | 83 | onSubmit() { 84 | switch (this.dbops) { 85 | case DBOperation.create: 86 | if (this.userFrm.valid) { 87 | this.userService.post({ 88 | firstName: this.userFrm.value.firstName, 89 | lastName: this.userFrm.value.lastName, 90 | dob: this.userFrm.value.dob, 91 | gender: this.userFrm.value.gender, 92 | emailAddress: this.userFrm.value.emailAddress, 93 | phoneNumber: this.userFrm.value.phoneNumber, 94 | city: this.userFrm.value.city, 95 | state: this.userFrm.value.state, 96 | zip: this.userFrm.value.zip, 97 | country: this.userFrm.value.country 98 | }).subscribe( 99 | data => { 100 | if (data > 0) { 101 | this.utilService.openSnackBar("Successfully added the user!"); 102 | this.dialogRef.close() 103 | } 104 | else { 105 | this.utilService.openSnackBar("Error adding user, contact your system administrator!"); 106 | } 107 | } 108 | ); 109 | } 110 | break; 111 | case DBOperation.update: 112 | if (this.userFrm.valid) { 113 | this.userService.put({ 114 | userID: this.userFrm.value.userID, 115 | phoneNumber: this.userFrm.value.phoneNumber, 116 | city: this.userFrm.value.city, 117 | state: this.userFrm.value.state, 118 | zip: this.userFrm.value.zip, 119 | country: this.userFrm.value.country 120 | }).subscribe( 121 | data => { 122 | if (data == true) { 123 | this.utilService.openSnackBar("Successfully updated the user!"); 124 | this.dialogRef.close() 125 | } 126 | else { 127 | this.utilService.openSnackBar("Error updating user, contact your system administrator!"); 128 | } 129 | } 130 | ); 131 | } 132 | break; 133 | case DBOperation.delete: 134 | this.userService.delete(this.userFrm.value.userID).subscribe( 135 | data => { 136 | debugger 137 | if (data == true) { 138 | this.utilService.openSnackBar("Successfully deleted the user!"); 139 | this.dialogRef.close() 140 | } 141 | else { 142 | this.utilService.openSnackBar("Error deleting user, contact your system administrator!"); 143 | } 144 | } 145 | ); 146 | break; 147 | } 148 | } 149 | } -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhub-io/UserManagement/4e230acbdc07142dd87c05ed56fe4459f9299be8/UserManagement.API/ClientApp/src/assets/.gitkeep -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhub-io/UserManagement/4e230acbdc07142dd87c05ed56fe4459f9299be8/UserManagement.API/ClientApp/src/favicon.ico -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ClientApp 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.error(err)); 13 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 22 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 23 | 24 | /** 25 | * Web Animations `@angular/platform-browser/animations` 26 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 27 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 28 | */ 29 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 30 | 31 | /** 32 | * By default, zone.js will patch all possible macroTask and DomEvents 33 | * user can disable parts of macroTask/DomEvents patch by setting following flags 34 | * because those flags need to be set before `zone.js` being loaded, and webpack 35 | * will put import in the top of bundle, so user need to create a separate file 36 | * in this directory (for example: zone-flags.ts), and put the following flags 37 | * into that file, and then add the following code before importing zone.js. 38 | * import './zone-flags'; 39 | * 40 | * The flags allowed in zone-flags.ts are listed here. 41 | * 42 | * The following flags will work for all browsers. 43 | * 44 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 45 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 46 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 47 | * 48 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 49 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 50 | * 51 | * (window as any).__Zone_enable_cross_context_check = true; 52 | * 53 | */ 54 | 55 | /*************************************************************************************************** 56 | * Zone JS is required by default for Angular itself. 57 | */ 58 | import 'zone.js/dist/zone'; // Included with Angular CLI. 59 | 60 | 61 | /*************************************************************************************************** 62 | * APPLICATION IMPORTS 63 | */ 64 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 3 | /* Provide sufficient contrast against white background */ 4 | 5 | html, 6 | body { 7 | height: 100%; 8 | overflow: auto; 9 | } 10 | body { 11 | margin: 0; 12 | font-family: Roboto, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | .container { 16 | width: 100% !important; 17 | } 18 | 19 | /*Custom*/ 20 | 21 | .section-padding { 22 | padding-top: 25px; 23 | } 24 | 25 | mat-card { 26 | margin: 0px !important; 27 | padding: 0px !important; 28 | background: rgb(253, 253, 253) !important; 29 | } 30 | 31 | mat-card-noshadow { 32 | background: #ececf4; 33 | box-shadow: none !important; 34 | } 35 | 36 | mat-card-header { 37 | background: #0a5b97; 38 | border-bottom: 5px solid #bbd1f1; 39 | height: 50px; 40 | padding-left: 5px; 41 | } 42 | 43 | mat-card-title { 44 | vertical-align: baseline; 45 | padding-top: 10px; 46 | padding-bottom: 0px; 47 | padding-left: 10px; 48 | font-size: 14pt; 49 | font-family: Arial, Helvetica, sans-serif; 50 | color: #ffffff; 51 | } 52 | 53 | mat-card-content { 54 | padding: 10px; 55 | padding-left: 17px; 56 | padding-bottom: 5px; 57 | color: #000 !important; 58 | } 59 | 60 | .mat-card-popup-width { 61 | width: 95vw !important; 62 | } 63 | 64 | mat-grid-tile { 65 | padding-bottom: 20px !important; 66 | } 67 | 68 | mat-grid-tile .mat-figure { 69 | align-items: flex-start !important; 70 | height: 100% !important; 71 | } 72 | 73 | .mat-dialog-container { 74 | padding: 0 !important; 75 | margin: 0 !important; 76 | overflow: visible !important; 77 | } 78 | 79 | mat-form-field { 80 | width: 100%; 81 | } 82 | 83 | .dialog-footer { 84 | padding-top: 10px; 85 | text-align: right; 86 | padding-bottom: 10px; 87 | } 88 | 89 | .footer 90 | { 91 | padding-top: 20px; 92 | padding-bottom: 10px; 93 | text-align: right; 94 | } 95 | 96 | .progress-loader 97 | { 98 | position: fixed; 99 | top: 50%; 100 | left: 50%; 101 | z-index: 999999999; 102 | } 103 | 104 | .overlay{ 105 | height:100vh; 106 | width:100%; 107 | background-color:rgba(207, 203, 203, 0.286); 108 | z-index: 9999999; 109 | top: 0; 110 | left: 0; 111 | position: fixed; 112 | } 113 | /*Custom*/ 114 | 115 | /******************Form**************************/ 116 | 117 | .frm-ctrl { 118 | width: 100%; 119 | padding-bottom: 15px; 120 | } 121 | 122 | /******************End Form**************************/ 123 | 124 | 125 | /******************Button**************************/ 126 | 127 | button { 128 | width: 100px; 129 | outline: none !important; 130 | } 131 | 132 | .button-lg { 133 | width: 165px; 134 | } 135 | 136 | .button-ex-lg { 137 | width: 300px; 138 | } 139 | 140 | /******************End Button**************************/ 141 | 142 | /******************Padding**************************/ 143 | 144 | .padding-15 { 145 | padding: 15px; 146 | } 147 | 148 | .padding-10 { 149 | padding: 10px; 150 | } 151 | 152 | .padding-left-10 153 | { 154 | padding-left: 10px; 155 | } 156 | 157 | .padding-bottom-15 158 | { 159 | padding-bottom: 15px; 160 | } 161 | 162 | .padding-bottom-10 163 | { 164 | padding-bottom: 10px; 165 | } 166 | 167 | .padding-top-5 168 | { 169 | padding-top: 5px; 170 | } 171 | 172 | .padding-top-15 173 | { 174 | padding-top: 15px; 175 | } 176 | 177 | /******************End Padding*************************/ 178 | 179 | /******************File Upload**************************/ 180 | input[type=file] { 181 | cursor: pointer; 182 | width: 180px; 183 | height: 34px; 184 | overflow: hidden; 185 | } 186 | 187 | input[type=file]:before { 188 | width: 180px; 189 | height: 35px; 190 | line-height: 32px; 191 | content: 'Select File to Upload'; 192 | display: inline-block; 193 | background: #3F51B5; 194 | border: 1px solid rgb(53, 69, 160); 195 | padding: 0 10px; 196 | text-align: center; 197 | color:#fff; 198 | border-radius: 2px; 199 | font-size: 11pt; 200 | } 201 | 202 | input[type=file]::-webkit-file-upload-button { 203 | visibility: hidden; 204 | } 205 | 206 | .upload-file-name 207 | { 208 | padding-left:10px; 209 | padding-top: 5px; 210 | font-size: 12pt; 211 | } 212 | 213 | /******************End File Upload*********************/ 214 | 215 | 216 | /******************Show Scrollbar for Modal*********************/ 217 | .cdk-global-overlay-wrapper { 218 | display: flex; 219 | position: absolute; 220 | z-index: 1000; 221 | overflow: auto; 222 | pointer-events: auto; 223 | } 224 | 225 | /******************End Show Scrollbar for Modal*********************/ 226 | 227 | .error-msg 228 | { 229 | font-size: 12pt; 230 | color:#02101b; 231 | } 232 | 233 | /******************Custom Error Message for Form*********************/ 234 | 235 | .custom-error-msg 236 | { 237 | font-size: 11px; 238 | } 239 | 240 | /******************End Custom Error Message for Form*********************/ -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: { 11 | context(path: string, deep?: boolean, filter?: RegExp): { 12 | keys(): string[]; 13 | (id: string): T; 14 | }; 15 | }; 16 | 17 | // First, initialize the Angular testing environment. 18 | getTestBed().initTestEnvironment( 19 | BrowserDynamicTestingModule, 20 | platformBrowserDynamicTesting() 21 | ); 22 | // Then we find all the tests. 23 | const context = require.context('./', true, /\.spec\.ts$/); 24 | // And load the modules. 25 | context.keys().map(context); 26 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "types": [] 7 | }, 8 | "files": [ 9 | "src/main.ts", 10 | "src/polyfills.ts" 11 | ], 12 | "include": [ 13 | "src/**/*.d.ts" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "baseUrl": "./", 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "downlevelIteration": true, 10 | "experimentalDecorators": true, 11 | "moduleResolution": "node", 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "module": "es2020", 15 | "lib": [ 16 | "es2018", 17 | "dom" 18 | ] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/spec", 6 | "types": [ 7 | "jasmine" 8 | ] 9 | }, 10 | "files": [ 11 | "src/test.ts", 12 | "src/polyfills.ts" 13 | ], 14 | "include": [ 15 | "src/**/*.spec.ts", 16 | "src/**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /UserManagement.API/ClientApp/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:recommended", 3 | "rulesDirectory": [ 4 | "codelyzer" 5 | ], 6 | "rules": { 7 | "align": { 8 | "options": [ 9 | "parameters", 10 | "statements" 11 | ] 12 | }, 13 | "array-type": false, 14 | "arrow-return-shorthand": true, 15 | "curly": true, 16 | "deprecation": { 17 | "severity": "warning" 18 | }, 19 | "eofline": true, 20 | "import-blacklist": [ 21 | true, 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": { 26 | "options": [ 27 | "spaces" 28 | ] 29 | }, 30 | "max-classes-per-file": false, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-console": [ 47 | true, 48 | "debug", 49 | "info", 50 | "time", 51 | "timeEnd", 52 | "trace" 53 | ], 54 | "no-empty": false, 55 | "no-inferrable-types": [ 56 | true, 57 | "ignore-params" 58 | ], 59 | "no-non-null-assertion": true, 60 | "no-redundant-jsdoc": true, 61 | "no-switch-case-fall-through": true, 62 | "no-var-requires": false, 63 | "object-literal-key-quotes": [ 64 | true, 65 | "as-needed" 66 | ], 67 | "quotemark": [ 68 | true, 69 | "single" 70 | ], 71 | "semicolon": { 72 | "options": [ 73 | "always" 74 | ] 75 | }, 76 | "space-before-function-paren": { 77 | "options": { 78 | "anonymous": "never", 79 | "asyncArrow": "always", 80 | "constructor": "never", 81 | "method": "never", 82 | "named": "never" 83 | } 84 | }, 85 | "typedef": [ 86 | true, 87 | "call-signature" 88 | ], 89 | "typedef-whitespace": { 90 | "options": [ 91 | { 92 | "call-signature": "nospace", 93 | "index-signature": "nospace", 94 | "parameter": "nospace", 95 | "property-declaration": "nospace", 96 | "variable-declaration": "nospace" 97 | }, 98 | { 99 | "call-signature": "onespace", 100 | "index-signature": "onespace", 101 | "parameter": "onespace", 102 | "property-declaration": "onespace", 103 | "variable-declaration": "onespace" 104 | } 105 | ] 106 | }, 107 | "variable-name": { 108 | "options": [ 109 | "ban-keywords", 110 | "check-format", 111 | "allow-pascal-case" 112 | ] 113 | }, 114 | "whitespace": { 115 | "options": [ 116 | "check-branch", 117 | "check-decl", 118 | "check-operator", 119 | "check-separator", 120 | "check-type", 121 | "check-typecast" 122 | ] 123 | }, 124 | "component-class-suffix": true, 125 | "contextual-lifecycle": true, 126 | "directive-class-suffix": true, 127 | "no-conflicting-lifecycle": true, 128 | "no-host-metadata-property": true, 129 | "no-input-rename": true, 130 | "no-inputs-metadata-property": true, 131 | "no-output-native": true, 132 | "no-output-on-prefix": true, 133 | "no-output-rename": true, 134 | "no-outputs-metadata-property": true, 135 | "template-banana-in-box": true, 136 | "template-no-negated-async": true, 137 | "use-lifecycle-interface": true, 138 | "use-pipe-transform-interface": true, 139 | "directive-selector": [ 140 | true, 141 | "attribute", 142 | "app", 143 | "camelCase" 144 | ], 145 | "component-selector": [ 146 | true, 147 | "element", 148 | "app", 149 | "kebab-case" 150 | ] 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /UserManagement.API/Common/CustomExceptionHandlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.API.Common 2 | { 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Http; 5 | using Newtonsoft.Json; 6 | using System; 7 | using System.Net; 8 | using System.Threading.Tasks; 9 | using UserManagement.Application.Common.Exceptions; 10 | 11 | public class CustomExceptionHandlerMiddleware 12 | { 13 | private readonly RequestDelegate _next; 14 | 15 | public CustomExceptionHandlerMiddleware(RequestDelegate next) 16 | { 17 | _next = next; 18 | } 19 | 20 | public async Task Invoke(HttpContext context) 21 | { 22 | try 23 | { 24 | await _next(context); 25 | } 26 | catch (Exception ex) 27 | { 28 | await HandleExceptionAsync(context, ex); 29 | } 30 | } 31 | 32 | private Task HandleExceptionAsync(HttpContext context, Exception exception) 33 | { 34 | var code = HttpStatusCode.InternalServerError; 35 | 36 | var result = string.Empty; 37 | 38 | switch (exception) 39 | { 40 | case ValidationException validationException: 41 | code = HttpStatusCode.BadRequest; 42 | result = JsonConvert.SerializeObject(validationException.Errors); 43 | break; 44 | case NotFoundException _: 45 | code = HttpStatusCode.NotFound; 46 | break; 47 | } 48 | 49 | context.Response.ContentType = "application/json"; 50 | context.Response.StatusCode = (int)code; 51 | 52 | if (string.IsNullOrEmpty(result)) 53 | { 54 | result = JsonConvert.SerializeObject(new { error = exception.Message }); 55 | } 56 | 57 | return context.Response.WriteAsync(result); 58 | } 59 | } 60 | 61 | public static class CustomExceptionHandlerMiddlewareExtensions 62 | { 63 | public static IApplicationBuilder UseCustomExceptionHandler(this IApplicationBuilder builder) 64 | { 65 | return builder.UseMiddleware(); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /UserManagement.API/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.API.Controllers 2 | { 3 | using MediatR; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | [Route("api/[controller]")] 8 | [ApiController] 9 | public class BaseController : ControllerBase 10 | { 11 | private IMediator mediator; 12 | 13 | /// 14 | /// Gets the Mediator. 15 | /// 16 | protected IMediator Mediator => this.mediator ??= this.HttpContext.RequestServices.GetService(); 17 | } 18 | } -------------------------------------------------------------------------------- /UserManagement.API/Controllers/UserController.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.API.Controllers 2 | { 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Mvc; 5 | using UserManagement.Application.User.Commands; 6 | using UserManagement.Application.User.Queries; 7 | using UserManagement.Application.User.VM; 8 | 9 | [Route("api/[controller]")] 10 | [ApiController] 11 | public class UserController : BaseController 12 | { 13 | [HttpGet("[action]")] 14 | public async Task> Get(int userID) 15 | { 16 | return await this.Mediator.Send(new GetSingleUserQuery { UserID = userID }); 17 | } 18 | 19 | [HttpGet("[action]")] 20 | public async Task> GetAll() 21 | { 22 | return await this.Mediator.Send(new GetAllUserQuery()); 23 | } 24 | 25 | [HttpPost("[action]")] 26 | public async Task> Post(AddUserCommand command) 27 | { 28 | return await this.Mediator.Send(command); 29 | } 30 | 31 | [HttpPut("[action]")] 32 | public async Task> Put(UpdateUserCommand command) 33 | { 34 | return await this.Mediator.Send(command); 35 | } 36 | 37 | [HttpDelete("[action]")] 38 | public async Task> Delete(int userID) 39 | { 40 | return await this.Mediator.Send(new DeleteUserCommand { UserID = userID }); 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /UserManagement.API/Filters/ApiExceptionFilterAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.API.Filters 2 | { 3 | using UserManagement.Application.Common.Exceptions; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Filters; 7 | using System; 8 | using System.Collections.Generic; 9 | public class ApiExceptionFilterAttribute : ExceptionFilterAttribute 10 | { 11 | 12 | private readonly IDictionary> _exceptionHandlers; 13 | 14 | public ApiExceptionFilterAttribute() 15 | { 16 | // Register known exception types and handlers. 17 | _exceptionHandlers = new Dictionary> 18 | { 19 | { typeof(ValidationException), HandleValidationException }, 20 | { typeof(NotFoundException), HandleNotFoundException }, 21 | }; 22 | } 23 | 24 | public override void OnException(ExceptionContext context) 25 | { 26 | HandleException(context); 27 | 28 | base.OnException(context); 29 | } 30 | 31 | private void HandleException(ExceptionContext context) 32 | { 33 | Type type = context.Exception.GetType(); 34 | if (_exceptionHandlers.ContainsKey(type)) 35 | { 36 | _exceptionHandlers[type].Invoke(context); 37 | return; 38 | } 39 | 40 | if (!context.ModelState.IsValid) 41 | { 42 | HandleInvalidModelStateException(context); 43 | return; 44 | } 45 | 46 | HandleUnknownException(context); 47 | } 48 | 49 | private void HandleUnknownException(ExceptionContext context) 50 | { 51 | var details = new ProblemDetails 52 | { 53 | Status = StatusCodes.Status500InternalServerError, 54 | Title = "An error occurred while processing your request.", 55 | Type = "https://tools.ietf.org/html/rfc7231#section-6.6.1" 56 | }; 57 | 58 | context.Result = new ObjectResult(details) 59 | { 60 | StatusCode = StatusCodes.Status500InternalServerError 61 | }; 62 | 63 | context.ExceptionHandled = true; 64 | } 65 | 66 | private void HandleValidationException(ExceptionContext context) 67 | { 68 | var exception = context.Exception as ValidationException; 69 | 70 | var details = new ValidationProblemDetails(exception.Errors) 71 | { 72 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1" 73 | }; 74 | 75 | context.Result = new BadRequestObjectResult(details); 76 | 77 | context.ExceptionHandled = true; 78 | } 79 | 80 | private void HandleInvalidModelStateException(ExceptionContext context) 81 | { 82 | var details = new ValidationProblemDetails(context.ModelState) 83 | { 84 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.1" 85 | }; 86 | 87 | context.Result = new BadRequestObjectResult(details); 88 | 89 | context.ExceptionHandled = true; 90 | } 91 | 92 | private void HandleNotFoundException(ExceptionContext context) 93 | { 94 | var exception = context.Exception as NotFoundException; 95 | 96 | var details = new ProblemDetails() 97 | { 98 | Type = "https://tools.ietf.org/html/rfc7231#section-6.5.4", 99 | Title = "The specified resource was not found.", 100 | Detail = exception.Message 101 | }; 102 | 103 | context.Result = new NotFoundObjectResult(details); 104 | 105 | context.ExceptionHandled = true; 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /UserManagement.API/Pages/Error.cshtml: -------------------------------------------------------------------------------- 1 | @page 2 | @model ErrorModel 3 | @{ 4 | ViewData["Title"] = "Error"; 5 | } 6 | 7 |

Error.

8 |

An error occurred while processing your request.

9 | 10 | @if (Model.ShowRequestId) 11 | { 12 |

13 | Request ID: @Model.RequestId 14 |

15 | } 16 | 17 |

Development Mode

18 |

19 | Swapping to the Development environment displays detailed information about the error that occurred. 20 |

21 |

22 | The Development environment shouldn't be enabled for deployed applications. 23 | It can result in displaying sensitive information from exceptions to end users. 24 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development 25 | and restarting the app. 26 |

27 | -------------------------------------------------------------------------------- /UserManagement.API/Pages/Error.cshtml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.RazorPages; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace UserManagement.API.Pages 11 | { 12 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] 13 | public class ErrorModel : PageModel 14 | { 15 | private readonly ILogger _logger; 16 | 17 | public ErrorModel(ILogger logger) 18 | { 19 | _logger = logger; 20 | } 21 | 22 | public string RequestId { get; set; } 23 | 24 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 25 | 26 | public void OnGet() 27 | { 28 | RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /UserManagement.API/Pages/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using UserManagement.API 2 | @namespace UserManagement.API.Pages 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /UserManagement.API/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Configuration; 7 | using Microsoft.Extensions.Hosting; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace UserManagement.API 11 | { 12 | public class Program 13 | { 14 | public static void Main(string[] args) 15 | { 16 | CreateHostBuilder(args).Build().Run(); 17 | } 18 | 19 | public static IHostBuilder CreateHostBuilder(string[] args) => 20 | Host.CreateDefaultBuilder(args) 21 | .ConfigureWebHostDefaults(webBuilder => 22 | { 23 | webBuilder.UseStartup(); 24 | }); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /UserManagement.API/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:64118", 7 | "sslPort": 44353 8 | } 9 | }, 10 | "profiles": { 11 | "IIS Express": { 12 | "commandName": "IISExpress", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | } 17 | }, 18 | "UserManagement.API": { 19 | "commandName": "Project", 20 | "launchBrowser": true, 21 | "applicationUrl": "https://localhost:5001;http://localhost:5000", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /UserManagement.API/Startup.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.API 2 | { 3 | using FluentValidation.AspNetCore; 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.SpaServices.AngularCli; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Hosting; 11 | using UserManagement.API.Filters; 12 | using UserManagement.Application; 13 | using UserManagement.Persistence; 14 | public class Startup 15 | { 16 | public Startup(IConfiguration configuration) 17 | { 18 | Configuration = configuration; 19 | } 20 | 21 | public IConfiguration Configuration { get; } 22 | 23 | // This method gets called by the runtime. Use this method to add services to the container. 24 | public void ConfigureServices(IServiceCollection services) 25 | { 26 | services.AddApplication(); 27 | services.AddPersistance(); 28 | 29 | services.AddHttpContextAccessor(); 30 | 31 | services.AddControllersWithViews(options => 32 | options.Filters.Add(new ApiExceptionFilterAttribute())) 33 | .AddFluentValidation(); 34 | 35 | services.AddRazorPages(); 36 | 37 | // Customise default API behaviour 38 | services.Configure(options => 39 | { 40 | options.SuppressModelStateInvalidFilter = true; 41 | }); 42 | 43 | // In production, the Angular files will be served from this directory 44 | services.AddSpaStaticFiles(configuration => 45 | { 46 | configuration.RootPath = "ClientApp/dist"; 47 | }); 48 | 49 | services.AddOpenApiDocument(configure => 50 | { 51 | configure.Title = "UserManagement API"; 52 | }); 53 | 54 | services.AddLogging(); 55 | } 56 | 57 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 58 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 59 | { 60 | if (env.IsDevelopment()) 61 | { 62 | app.UseDeveloperExceptionPage(); 63 | } 64 | else 65 | { 66 | app.UseExceptionHandler("/Error"); 67 | // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. 68 | app.UseHsts(); 69 | } 70 | 71 | app.UseHttpsRedirection(); 72 | app.UseStaticFiles(); 73 | if (!env.IsDevelopment()) 74 | { 75 | app.UseSpaStaticFiles(); 76 | } 77 | 78 | app.UseSwaggerUi3(settings => 79 | { 80 | settings.Path = "/api"; 81 | settings.DocumentPath = "/api/specification.json"; 82 | }); 83 | 84 | app.UseRouting(); 85 | 86 | app.UseEndpoints(endpoints => 87 | { 88 | endpoints.MapControllerRoute( 89 | name: "default", 90 | pattern: "{controller}/{action=Index}/{id?}"); 91 | endpoints.MapRazorPages(); 92 | }); 93 | 94 | app.UseSpa(spa => 95 | { 96 | // To learn more about options for serving an Angular SPA from ASP.NET Core, 97 | // see https://go.microsoft.com/fwlink/?linkid=864501 98 | 99 | spa.Options.SourcePath = "ClientApp"; 100 | 101 | if (env.IsDevelopment()) 102 | { 103 | spa.UseAngularCliServer(npmScript: "start"); 104 | } 105 | }); 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /UserManagement.API/UserManagement.API.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | true 6 | Latest 7 | false 8 | ClientApp\ 9 | $(DefaultItemExcludes);$(SpaRoot)node_modules\** 10 | 11 | 12 | false 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 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 | %(DistFiles.Identity) 70 | PreserveNewest 71 | true 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /UserManagement.API/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /UserManagement.API/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "FullStackConnection": "Data Source=localhost\\SQLEXPRESS;Persist Security Info=True;Integrated Security=SSPI;Initial Catalog=FullstackHub", 4 | "TestFullStackConnection": "Data Source=localhost\\SQLEXPRESS;Persist Security Info=True;Integrated Security=SSPI;Initial Catalog=TestFullstackHub" 5 | }, 6 | "AppSettings": { 7 | "LongRunningProcessMilliseconds": "1500", 8 | "MSG_USER_NULLUSERID": "User ID is required!", 9 | "MSG_USER_NULLFIRSTNAME": "First Name is required!", 10 | "MSG_USER_NULLLASTNAME": "Last Name is required!", 11 | "MSG_USER_NULLDOB": "Date of birth is required!", 12 | "MSG_USER_NULLGENDER": "Gender is required!", 13 | "MSG_USER_GENDER_LEN": "Gender can be only M/F!", 14 | "MSG_USER_NULLEMAILADDR": "Email Address is required!", 15 | "MSG_USER_NULLPHNUM": "Phone Number is required!", 16 | "MSG_USER_NULLCITY": "City is required!", 17 | "MSG_USER_NULLSTATE": "State is required!", 18 | "MSG_USER_NULLCOUNTRY": "Country is required!" 19 | }, 20 | "Logging": { 21 | "LogLevel": { 22 | "Default": "Information", 23 | "Microsoft": "Warning", 24 | "Microsoft.Hosting.Lifetime": "Information" 25 | } 26 | }, 27 | "AllowedHosts": "*" 28 | } 29 | -------------------------------------------------------------------------------- /UserManagement.API/nswag.json: -------------------------------------------------------------------------------- 1 | { 2 | "runtime": "NetCore31", 3 | "defaultVariables": null, 4 | "documentGenerator": { 5 | "aspNetCoreToOpenApi": { 6 | "project": "UserManagement.API.csproj", 7 | "msBuildProjectExtensionsPath": null, 8 | "configuration": null, 9 | "runtime": null, 10 | "targetFramework": null, 11 | "noBuild": true, 12 | "verbose": false, 13 | "workingDirectory": null, 14 | "requireParametersWithoutDefault": true, 15 | "apiGroupNames": null, 16 | "defaultPropertyNameHandling": "CamelCase", 17 | "defaultReferenceTypeNullHandling": "Null", 18 | "defaultDictionaryValueReferenceTypeNullHandling": "NotNull", 19 | "defaultResponseReferenceTypeNullHandling": "NotNull", 20 | "defaultEnumHandling": "String", 21 | "flattenInheritanceHierarchy": false, 22 | "generateKnownTypes": true, 23 | "generateEnumMappingDescription": false, 24 | "generateXmlObjects": false, 25 | "generateAbstractProperties": false, 26 | "generateAbstractSchemas": true, 27 | "ignoreObsoleteProperties": false, 28 | "allowReferencesWithProperties": false, 29 | "excludedTypeNames": [], 30 | "serviceHost": null, 31 | "serviceBasePath": null, 32 | "serviceSchemes": [], 33 | "infoTitle": "UserManagement APIs", 34 | "infoDescription": null, 35 | "infoVersion": "1.0.0", 36 | "documentTemplate": null, 37 | "documentProcessorTypes": [], 38 | "operationProcessorTypes": [], 39 | "typeNameGeneratorType": null, 40 | "schemaNameGeneratorType": null, 41 | "contractResolverType": null, 42 | "serializerSettingsType": null, 43 | "useDocumentProvider": true, 44 | "documentName": "v1", 45 | "aspNetCoreEnvironment": null, 46 | "createWebHostBuilderMethod": null, 47 | "startupType": null, 48 | "allowNullableBodyParameters": true, 49 | "output": "wwwroot/api/specification.json", 50 | "outputType": "OpenApi3", 51 | "assemblyPaths": [], 52 | "assemblyConfig": null, 53 | "referencePaths": [], 54 | "useNuGetCache": false 55 | } 56 | }, 57 | "codeGenerators": { 58 | "openApiToTypeScriptClient": { 59 | "className": "{controller}Service", 60 | "moduleName": "", 61 | "namespace": "", 62 | "typeScriptVersion": 2.7, 63 | "template": "Angular", 64 | "promiseType": "Promise", 65 | "httpClass": "HttpClient", 66 | "useSingletonProvider": true, 67 | "injectionTokenType": "InjectionToken", 68 | "rxJsVersion": 6.0, 69 | "dateTimeType": "Date", 70 | "nullValue": "Undefined", 71 | "generateClientClasses": true, 72 | "generateClientInterfaces": true, 73 | "generateOptionalParameters": false, 74 | "exportTypes": true, 75 | "wrapDtoExceptions": false, 76 | "exceptionClass": "SwaggerException", 77 | "clientBaseClass": null, 78 | "wrapResponses": false, 79 | "wrapResponseMethods": [], 80 | "generateResponseClasses": true, 81 | "responseClass": "SwaggerResponse", 82 | "protectedMethods": [], 83 | "configurationClass": null, 84 | "useTransformOptionsMethod": false, 85 | "useTransformResultMethod": false, 86 | "generateDtoTypes": true, 87 | "operationGenerationMode": "MultipleClientsFromOperationId", 88 | "markOptionalProperties": true, 89 | "generateCloneMethod": false, 90 | "typeStyle": "Class", 91 | "classTypes": [], 92 | "extendedClasses": [], 93 | "extensionCode": null, 94 | "generateDefaultValues": true, 95 | "excludedTypeNames": [], 96 | "excludedParameterNames": [], 97 | "handleReferences": false, 98 | "generateConstructorInterface": true, 99 | "convertConstructorInterfaceData": false, 100 | "importRequiredTypes": true, 101 | "useGetBaseUrlMethod": false, 102 | "baseUrlTokenName": "API_BASE_URL", 103 | "queryNullValue": "", 104 | "inlineNamedDictionaries": false, 105 | "inlineNamedAny": false, 106 | "templateDirectory": null, 107 | "typeNameGeneratorType": null, 108 | "propertyNameGeneratorType": null, 109 | "enumNameGeneratorType": null, 110 | "serviceHost": null, 111 | "serviceSchemes": null, 112 | "withCredentials": true, 113 | "output": "ClientApp/src/app/user-management-api.ts" 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /UserManagement.API/wwwroot/api/specification.json: -------------------------------------------------------------------------------- 1 | { 2 | "x-generator": "NSwag v13.9.4.0 (NJsonSchema v10.3.1.0 (Newtonsoft.Json v12.0.0.0))", 3 | "openapi": "3.0.0", 4 | "info": { 5 | "title": "UserManagement API", 6 | "version": "1.0.0" 7 | }, 8 | "paths": { 9 | "/api/User/Get": { 10 | "get": { 11 | "tags": [ 12 | "User" 13 | ], 14 | "operationId": "User_Get", 15 | "parameters": [ 16 | { 17 | "name": "userID", 18 | "in": "query", 19 | "schema": { 20 | "type": "integer", 21 | "format": "int32" 22 | }, 23 | "x-position": 1 24 | } 25 | ], 26 | "responses": { 27 | "200": { 28 | "description": "", 29 | "content": { 30 | "application/json": { 31 | "schema": { 32 | "$ref": "#/components/schemas/UserVM" 33 | } 34 | } 35 | } 36 | } 37 | } 38 | } 39 | }, 40 | "/api/User/GetAll": { 41 | "get": { 42 | "tags": [ 43 | "User" 44 | ], 45 | "operationId": "User_GetAll", 46 | "responses": { 47 | "200": { 48 | "description": "", 49 | "content": { 50 | "application/json": { 51 | "schema": { 52 | "$ref": "#/components/schemas/UserVM" 53 | } 54 | } 55 | } 56 | } 57 | } 58 | } 59 | }, 60 | "/api/User/Post": { 61 | "post": { 62 | "tags": [ 63 | "User" 64 | ], 65 | "operationId": "User_Post", 66 | "requestBody": { 67 | "x-name": "command", 68 | "content": { 69 | "application/json": { 70 | "schema": { 71 | "$ref": "#/components/schemas/AddUserCommand" 72 | } 73 | } 74 | }, 75 | "required": true, 76 | "x-position": 1 77 | }, 78 | "responses": { 79 | "200": { 80 | "description": "", 81 | "content": { 82 | "application/json": { 83 | "schema": { 84 | "type": "integer", 85 | "format": "int32" 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | }, 93 | "/api/User/Put": { 94 | "put": { 95 | "tags": [ 96 | "User" 97 | ], 98 | "operationId": "User_Put", 99 | "requestBody": { 100 | "x-name": "command", 101 | "content": { 102 | "application/json": { 103 | "schema": { 104 | "$ref": "#/components/schemas/UpdateUserCommand" 105 | } 106 | } 107 | }, 108 | "required": true, 109 | "x-position": 1 110 | }, 111 | "responses": { 112 | "200": { 113 | "description": "", 114 | "content": { 115 | "application/json": { 116 | "schema": { 117 | "type": "boolean" 118 | } 119 | } 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | "/api/User/Delete": { 126 | "delete": { 127 | "tags": [ 128 | "User" 129 | ], 130 | "operationId": "User_Delete", 131 | "parameters": [ 132 | { 133 | "name": "userID", 134 | "in": "query", 135 | "schema": { 136 | "type": "integer", 137 | "format": "int32" 138 | }, 139 | "x-position": 1 140 | } 141 | ], 142 | "responses": { 143 | "200": { 144 | "description": "", 145 | "content": { 146 | "application/json": { 147 | "schema": { 148 | "type": "boolean" 149 | } 150 | } 151 | } 152 | } 153 | } 154 | } 155 | } 156 | }, 157 | "components": { 158 | "schemas": { 159 | "UserVM": { 160 | "type": "object", 161 | "additionalProperties": false, 162 | "properties": { 163 | "userList": { 164 | "type": "array", 165 | "nullable": true, 166 | "items": { 167 | "$ref": "#/components/schemas/UserDTO" 168 | } 169 | } 170 | } 171 | }, 172 | "UserDTO": { 173 | "type": "object", 174 | "additionalProperties": false, 175 | "properties": { 176 | "userID": { 177 | "type": "integer", 178 | "format": "int32" 179 | }, 180 | "salutation": { 181 | "type": "string", 182 | "nullable": true 183 | }, 184 | "firstName": { 185 | "type": "string", 186 | "nullable": true 187 | }, 188 | "lastName": { 189 | "type": "string", 190 | "nullable": true 191 | }, 192 | "dob": { 193 | "type": "string", 194 | "format": "date-time" 195 | }, 196 | "age": { 197 | "type": "integer", 198 | "format": "int32" 199 | }, 200 | "gender": { 201 | "type": "string", 202 | "nullable": true 203 | }, 204 | "emailAddress": { 205 | "type": "string", 206 | "nullable": true 207 | }, 208 | "phoneNumber": { 209 | "type": "string", 210 | "nullable": true 211 | }, 212 | "city": { 213 | "type": "string", 214 | "nullable": true 215 | }, 216 | "state": { 217 | "type": "string", 218 | "nullable": true 219 | }, 220 | "zip": { 221 | "type": "string", 222 | "nullable": true 223 | }, 224 | "country": { 225 | "type": "string", 226 | "nullable": true 227 | } 228 | } 229 | }, 230 | "AddUserCommand": { 231 | "type": "object", 232 | "additionalProperties": false, 233 | "properties": { 234 | "firstName": { 235 | "type": "string", 236 | "nullable": true 237 | }, 238 | "lastName": { 239 | "type": "string", 240 | "nullable": true 241 | }, 242 | "dob": { 243 | "type": "string", 244 | "format": "date-time" 245 | }, 246 | "gender": { 247 | "type": "string", 248 | "nullable": true 249 | }, 250 | "emailAddress": { 251 | "type": "string", 252 | "nullable": true 253 | }, 254 | "phoneNumber": { 255 | "type": "string", 256 | "nullable": true 257 | }, 258 | "city": { 259 | "type": "string", 260 | "nullable": true 261 | }, 262 | "state": { 263 | "type": "string", 264 | "nullable": true 265 | }, 266 | "zip": { 267 | "type": "string", 268 | "nullable": true 269 | }, 270 | "country": { 271 | "type": "string", 272 | "nullable": true 273 | } 274 | } 275 | }, 276 | "UpdateUserCommand": { 277 | "type": "object", 278 | "additionalProperties": false, 279 | "properties": { 280 | "userID": { 281 | "type": "integer", 282 | "format": "int32" 283 | }, 284 | "phoneNumber": { 285 | "type": "string", 286 | "nullable": true 287 | }, 288 | "city": { 289 | "type": "string", 290 | "nullable": true 291 | }, 292 | "state": { 293 | "type": "string", 294 | "nullable": true 295 | }, 296 | "zip": { 297 | "type": "string", 298 | "nullable": true 299 | }, 300 | "country": { 301 | "type": "string", 302 | "nullable": true 303 | } 304 | } 305 | } 306 | } 307 | } 308 | } -------------------------------------------------------------------------------- /UserManagement.API/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fullstackhub-io/UserManagement/4e230acbdc07142dd87c05ed56fe4459f9299be8/UserManagement.API/wwwroot/favicon.ico -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/BaseFixture.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests 2 | { 3 | using AutoMapper; 4 | using System.Data; 5 | using UserManagement.Application.Common.Mappings; 6 | public class BaseFixture 7 | { 8 | public IMapper Mapper { get; } 9 | 10 | 11 | public IDbConnection DBConnection { get; } 12 | 13 | public BaseFixture() 14 | { 15 | var configurationProvider = new MapperConfiguration(cfg => 16 | { 17 | cfg.AddProfile(); 18 | }); 19 | 20 | this.Mapper = configurationProvider.CreateMapper(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/User/Commands/AddUserCommandTest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests.User.Commands 2 | { 3 | using AutoMapper; 4 | using Shouldly; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using UserManagement.Application.Common.Interfaces; 8 | using UserManagement.Application.User.Commands; 9 | using UserManagement.Domain.UnitOfWork; 10 | using Xunit; 11 | 12 | [Collection("UserCollection")] 13 | public class AddUserCommandTest 14 | { 15 | private readonly IConfigConstants constant; 16 | private readonly IMapper mapper; 17 | private readonly IUnitOfWork unitOfWork; 18 | 19 | public AddUserCommandTest(UserFixture userFixture) 20 | { 21 | constant = userFixture.Constant; 22 | mapper = userFixture.Mapper; 23 | unitOfWork = userFixture.UnitOfWork; 24 | } 25 | 26 | [Fact] 27 | public async Task Handle_ReturnsCorrectVM() 28 | { 29 | var command = new AddUserCommand 30 | { 31 | FirstName = "John", 32 | LastName = "Doe", 33 | City = "Falls Chruch", 34 | Country = "USA", 35 | State = "VA", 36 | Zip = "22044", 37 | DOB = new System.DateTime(1980, 01, 01), 38 | EmailAddress = "jdoe@fullstackhub.io", 39 | Gender = "M", 40 | PhoneNumber = "444-443-4444", 41 | }; 42 | 43 | var handler = new AddUserCommand.AddNewUserHandler(constant, mapper, unitOfWork); 44 | var result = await handler.Handle(command, CancellationToken.None); 45 | result.ShouldBeOfType(); 46 | } 47 | 48 | [Fact] 49 | public async Task Handle_ReturnCorrectUserID_WhenSendCorrectPayload() 50 | { 51 | var command = new AddUserCommand 52 | { 53 | FirstName = "John", 54 | LastName = "Doe", 55 | City = "Falls Chruch", 56 | Country = "USA", 57 | State = "VA", 58 | Zip = "22044", 59 | DOB = new System.DateTime(1980, 01, 01), 60 | EmailAddress = "jdoe@fullstackhub.io", 61 | Gender = "M", 62 | PhoneNumber = "444-443-4444", 63 | }; 64 | 65 | var handler = new AddUserCommand.AddNewUserHandler(constant, mapper, unitOfWork); 66 | var result = await handler.Handle(command, CancellationToken.None); 67 | result.ShouldBe(100); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/User/Commands/AddUserCommandValidatorTest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests.User.Commands 2 | { 3 | using Shouldly; 4 | using System.Linq; 5 | using UserManagement.Application.Common.Interfaces; 6 | using UserManagement.Application.User.Commands; 7 | using Xunit; 8 | 9 | [Collection("UserCollection")] 10 | public class AddUserCommandValidatorTest 11 | { 12 | private readonly IConfigConstants constant; 13 | 14 | public AddUserCommandValidatorTest(UserFixture userFixture) 15 | { 16 | constant = userFixture.Constant; 17 | } 18 | 19 | [Fact] 20 | public void Validate_ReturnTrue_WhenAllDataIsValid() 21 | { 22 | var command = new AddUserCommand 23 | { 24 | FirstName = "John", 25 | LastName = "Doe", 26 | City = "Falls Chruch", 27 | Country = "USA", 28 | State = "VA", 29 | Zip = "22044", 30 | DOB = new System.DateTime(1980, 01, 01), 31 | EmailAddress = "jdoe@fullstackhub.io", 32 | Gender = "M", 33 | PhoneNumber = "444-443-4444", 34 | }; 35 | 36 | var validator = new AddUserCommandValidator(constant); 37 | var result = validator.Validate(command); 38 | result.IsValid.ShouldBeTrue(); 39 | } 40 | 41 | [Fact] 42 | public void Validate_ReturnFalse_WhenAllDataIsInValid() 43 | { 44 | var command = new AddUserCommand 45 | { 46 | FirstName = string.Empty, 47 | LastName = string.Empty, 48 | City = string.Empty, 49 | Country = string.Empty, 50 | State = string.Empty, 51 | Zip = null, 52 | EmailAddress = string.Empty, 53 | Gender = string.Empty, 54 | PhoneNumber = string.Empty, 55 | }; 56 | 57 | var validator = new AddUserCommandValidator(constant); 58 | var result = validator.Validate(command); 59 | result.IsValid.ShouldBeFalse(); 60 | } 61 | 62 | [Fact] 63 | public void Validate_ReturnFalse_WhenFirstNameIsEmpty() 64 | { 65 | var command = new AddUserCommand 66 | { 67 | FirstName = string.Empty, 68 | LastName = "Doe", 69 | City = "Falls Chruch", 70 | Country = "USA", 71 | State = "VA", 72 | Zip = "22044", 73 | DOB = new System.DateTime(1980, 01, 01), 74 | EmailAddress = "jdoe@fullstackhub.io", 75 | Gender = "M", 76 | PhoneNumber = "444-443-4444", 77 | }; 78 | 79 | var validator = new AddUserCommandValidator(constant); 80 | var result = validator.Validate(command); 81 | result.Errors.FirstOrDefault(x => x.ErrorMessage == constant.MSG_USER_NULLFIRSTNAME).ErrorMessage.ShouldBe(constant.MSG_USER_NULLFIRSTNAME); 82 | result.IsValid.ShouldBeFalse(); 83 | } 84 | 85 | [Fact] 86 | public void Validate_ReturnFalse_WhenLastNameIsEmpty() 87 | { 88 | var command = new AddUserCommand 89 | { 90 | FirstName = "John", 91 | LastName = string.Empty, 92 | City = "Falls Chruch", 93 | Country = "USA", 94 | State = "VA", 95 | Zip = "22044", 96 | DOB = new System.DateTime(1980, 01, 01), 97 | EmailAddress = "jdoe@fullstackhub.io", 98 | Gender = "M", 99 | PhoneNumber = "444-443-4444", 100 | }; 101 | 102 | var validator = new AddUserCommandValidator(constant); 103 | var result = validator.Validate(command); 104 | result.Errors.FirstOrDefault(x => x.ErrorMessage == constant.MSG_USER_NULLLASTNAME).ErrorMessage.ShouldBe(constant.MSG_USER_NULLLASTNAME); 105 | result.IsValid.ShouldBeFalse(); 106 | } 107 | 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/User/Commands/DeleteUserCommandTest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests.User.Commands 2 | { 3 | using AutoMapper; 4 | using Shouldly; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using UserManagement.Application.Common.Interfaces; 8 | using UserManagement.Application.User.Commands; 9 | using UserManagement.Domain.UnitOfWork; 10 | using Xunit; 11 | 12 | [Collection("UserCollection")] 13 | public class DeleteUserCommandTest 14 | { 15 | private readonly IConfigConstants constant; 16 | private readonly IMapper mapper; 17 | private readonly IUnitOfWork unitOfWork; 18 | 19 | public DeleteUserCommandTest(UserFixture userFixture) 20 | { 21 | constant = userFixture.Constant; 22 | mapper = userFixture.Mapper; 23 | unitOfWork = userFixture.UnitOfWork; 24 | } 25 | 26 | [Fact] 27 | public async Task Handle_ReturnsCorrectVM() 28 | { 29 | var command = new DeleteUserCommand 30 | { 31 | UserID = 100, 32 | }; 33 | 34 | var handler = new DeleteUserCommand.DeleteUserHandler(constant, mapper, unitOfWork); 35 | var result = await handler.Handle(command, CancellationToken.None); 36 | result.ShouldBeOfType(); 37 | } 38 | 39 | [Fact] 40 | public async Task Handle_ReturnTrue_WhenSendCorrectUserIDIsSent() 41 | { 42 | var command = new DeleteUserCommand 43 | { 44 | UserID = 100, 45 | }; 46 | 47 | var handler = new DeleteUserCommand.DeleteUserHandler(constant, mapper, unitOfWork); 48 | var result = await handler.Handle(command, CancellationToken.None); 49 | result.ShouldBe(true); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/User/Commands/DeleteUserCommandValidatorTest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests.User.Commands 2 | { 3 | using Shouldly; 4 | using UserManagement.Application.Common.Interfaces; 5 | using UserManagement.Application.User.Commands; 6 | using Xunit; 7 | 8 | [Collection("UserCollection")] 9 | public class DeleteUserCommandValidatorTest 10 | { 11 | private readonly IConfigConstants constant; 12 | 13 | public DeleteUserCommandValidatorTest(UserFixture userFixture) 14 | { 15 | constant = userFixture.Constant; 16 | } 17 | 18 | [Fact] 19 | public void Validate_ReturnTrue_WhenAllDataIsValid() 20 | { 21 | var command = new DeleteUserCommand 22 | { 23 | UserID = 100, 24 | }; 25 | 26 | var validator = new DeleteUserCommandValidator(constant); 27 | var result = validator.Validate(command); 28 | result.IsValid.ShouldBeTrue(); 29 | } 30 | 31 | [Fact] 32 | public void Validate_ReturnFalse_WhenAllDataIsInValid() 33 | { 34 | var command = new DeleteUserCommand 35 | { 36 | UserID = 0, 37 | }; 38 | 39 | var validator = new DeleteUserCommandValidator(constant); 40 | var result = validator.Validate(command); 41 | result.IsValid.ShouldBeFalse(); 42 | } 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/User/Commands/UpdateUserCommandTest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests.User.Commands 2 | { 3 | using AutoMapper; 4 | using Shouldly; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using UserManagement.Application.Common.Interfaces; 8 | using UserManagement.Application.User.Commands; 9 | using UserManagement.Domain.UnitOfWork; 10 | using Xunit; 11 | 12 | [Collection("UserCollection")] 13 | public class UpdateUserCommandTest 14 | { 15 | private readonly IConfigConstants constant; 16 | private readonly IMapper mapper; 17 | private readonly IUnitOfWork unitOfWork; 18 | 19 | public UpdateUserCommandTest(UserFixture userFixture) 20 | { 21 | constant = userFixture.Constant; 22 | mapper = userFixture.Mapper; 23 | unitOfWork = userFixture.UnitOfWork; 24 | } 25 | 26 | [Fact] 27 | public async Task Handle_ReturnsCorrectVM() 28 | { 29 | var command = new UpdateUserCommand 30 | { 31 | UserID = 100, 32 | City = "SpringField", 33 | Country = "USA", 34 | State = "VA", 35 | Zip = "66006", 36 | PhoneNumber = "888-88-8888", 37 | }; 38 | 39 | var handler = new UpdateUserCommand.UpdateUserHandler(constant, mapper, unitOfWork); 40 | var result = await handler.Handle(command, CancellationToken.None); 41 | result.ShouldBeOfType(); 42 | } 43 | 44 | [Fact] 45 | public async Task Handle_ReturnTrue_WhenSendCorrectPayloadIsSent() 46 | { 47 | var command = new UpdateUserCommand 48 | { 49 | UserID = 100, 50 | City = "SpringField", 51 | Country = "USA", 52 | State = "VA", 53 | Zip = "66006", 54 | PhoneNumber = "888-88-8888", 55 | }; 56 | 57 | var handler = new UpdateUserCommand.UpdateUserHandler(constant, mapper, unitOfWork); 58 | var result = await handler.Handle(command, CancellationToken.None); 59 | result.ShouldBe(true); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/User/Commands/UpdateUserCommandValidatorTest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests.User.Commands 2 | { 3 | using Shouldly; 4 | using System.Linq; 5 | using UserManagement.Application.Common.Interfaces; 6 | using UserManagement.Application.User.Commands; 7 | using Xunit; 8 | 9 | [Collection("UserCollection")] 10 | public class UpdateUserCommandValidatorTest 11 | { 12 | private readonly IConfigConstants constant; 13 | 14 | public UpdateUserCommandValidatorTest(UserFixture userFixture) 15 | { 16 | constant = userFixture.Constant; 17 | } 18 | 19 | [Fact] 20 | public void Validate_ReturnTrue_WhenAllDataIsValid() 21 | { 22 | var command = new UpdateUserCommand 23 | { 24 | UserID = 100, 25 | City = "Falls Chruch", 26 | Country = "USA", 27 | State = "VA", 28 | Zip = "22044", 29 | PhoneNumber = "444-443-4444", 30 | }; 31 | 32 | var validator = new UpdateUserCommandValidator(constant); 33 | var result = validator.Validate(command); 34 | result.IsValid.ShouldBeTrue(); 35 | } 36 | 37 | [Fact] 38 | public void Validate_ReturnFalse_WhenAllDataIsInValid() 39 | { 40 | var command = new UpdateUserCommand{}; 41 | 42 | var validator = new UpdateUserCommandValidator(constant); 43 | var result = validator.Validate(command); 44 | result.Errors.FirstOrDefault(e => e.ErrorMessage.Equals(constant.MSG_USER_NULLUSERID)).ShouldNotBeNull(); 45 | result.Errors.FirstOrDefault(e => e.ErrorMessage.Equals(constant.MSG_USER_NULLPHNUM)).ShouldNotBeNull(); 46 | result.Errors.FirstOrDefault(e => e.ErrorMessage.Equals(constant.MSG_USER_NULLCITY)).ShouldNotBeNull(); 47 | result.Errors.FirstOrDefault(e => e.ErrorMessage.Equals(constant.MSG_USER_NULLSTATE)).ShouldNotBeNull(); 48 | result.Errors.FirstOrDefault(e => e.ErrorMessage.Equals(constant.MSG_USER_NULLCOUNTRY)).ShouldNotBeNull(); 49 | result.IsValid.ShouldBeFalse(); 50 | } 51 | 52 | [Fact] 53 | public void Validate_ReturnFalse_WhenUserIDIsZero() 54 | { 55 | var command = new UpdateUserCommand 56 | { 57 | UserID = 0, 58 | City = "Falls Chruch", 59 | Country = "USA", 60 | State = "VA", 61 | Zip = "22044", 62 | PhoneNumber = "444-443-4444", 63 | }; 64 | 65 | var validator = new UpdateUserCommandValidator(constant); 66 | var result = validator.Validate(command); 67 | result.Errors.FirstOrDefault(e => e.ErrorMessage.Equals(constant.MSG_USER_NULLUSERID)).ShouldNotBeNull(); 68 | result.IsValid.ShouldBeFalse(); 69 | } 70 | 71 | [Fact] 72 | public void Validate_ReturnFalse_WhenStateIsNull() 73 | { 74 | var command = new UpdateUserCommand 75 | { 76 | UserID = 100, 77 | City = "Falls Chruch", 78 | Country = "USA", 79 | State = null, 80 | Zip = "22044", 81 | PhoneNumber = "444-443-4444", 82 | }; 83 | 84 | var validator = new UpdateUserCommandValidator(constant); 85 | var result = validator.Validate(command); 86 | result.Errors.FirstOrDefault(e => e.ErrorMessage.Equals(constant.MSG_USER_NULLSTATE)).ShouldNotBeNull(); 87 | result.IsValid.ShouldBeFalse(); 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/User/Queries/GetAllUserQueryTest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests.User.Queries 2 | { 3 | using AutoMapper; 4 | using Shouldly; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using UserManagement.Application.Common.Interfaces; 9 | using UserManagement.Application.User.Queries; 10 | using UserManagement.Application.User.VM; 11 | using UserManagement.Domain.UnitOfWork; 12 | using Xunit; 13 | 14 | [Collection("UserCollection")] 15 | public class GetAllUserQueryTest 16 | { 17 | private readonly IConfigConstants constant; 18 | private readonly IMapper mapper; 19 | private readonly IUnitOfWork unitOfWork; 20 | 21 | public GetAllUserQueryTest(UserFixture userFixture) 22 | { 23 | constant = userFixture.Constant; 24 | mapper = userFixture.Mapper; 25 | unitOfWork = userFixture.UnitOfWork; 26 | } 27 | 28 | [Fact] 29 | public async Task Handle_ReturnsCorrectVM() 30 | { 31 | var query = new GetAllUserQuery(); 32 | var handler = new GetAllUserQuery.GetAllUserHandler(constant, mapper, unitOfWork); 33 | var result = await handler.Handle(query, CancellationToken.None); 34 | result.ShouldBeOfType(); 35 | } 36 | 37 | [Fact] 38 | public async Task Handle_ReturnTwoRecords_WhenRun() 39 | { 40 | var query = new GetAllUserQuery(); 41 | var handler = new GetAllUserQuery.GetAllUserHandler(constant, mapper, unitOfWork); 42 | var result = await handler.Handle(query, CancellationToken.None); 43 | result.UserList.Count().ShouldBe(2); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/User/Queries/GetSingleUserQueryTest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests.User.Queries 2 | { 3 | using AutoMapper; 4 | using Shouldly; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using UserManagement.Application.Common.Interfaces; 8 | using UserManagement.Application.User.Queries; 9 | using UserManagement.Application.User.VM; 10 | using UserManagement.Domain.UnitOfWork; 11 | using Xunit; 12 | 13 | [Collection("UserCollection")] 14 | public class GetSingleUserQueryTest 15 | { 16 | private readonly IConfigConstants constant; 17 | private readonly IMapper mapper; 18 | private readonly IUnitOfWork unitOfWork; 19 | 20 | public GetSingleUserQueryTest(UserFixture userFixture) 21 | { 22 | constant = userFixture.Constant; 23 | mapper = userFixture.Mapper; 24 | unitOfWork = userFixture.UnitOfWork; 25 | } 26 | 27 | [Fact] 28 | public async Task Handle_ReturnsCorrectVM() 29 | { 30 | var query = new GetSingleUserQuery 31 | { 32 | UserID = 110, 33 | }; 34 | 35 | var handler = new GetSingleUserQuery.GetSingleUserHandler(constant, mapper, unitOfWork); 36 | var result = await handler.Handle(query, CancellationToken.None); 37 | result.ShouldBeOfType(); 38 | } 39 | 40 | [Fact] 41 | public async Task Handle_ReturnCorrectAge_WhenDOBIsProvided() 42 | { 43 | var query = new GetSingleUserQuery 44 | { 45 | UserID = 110, 46 | }; 47 | 48 | var handler = new GetSingleUserQuery.GetSingleUserHandler(constant, mapper, unitOfWork); 49 | var result = await handler.Handle(query, CancellationToken.None); 50 | result.UserList[0].Age.ShouldBe(21); 51 | } 52 | 53 | [Fact] 54 | public async Task Handle_ReturnCorrectsalutation_WhenGenderIsProvided() 55 | { 56 | var query = new GetSingleUserQuery 57 | { 58 | UserID = 100, 59 | }; 60 | 61 | var handler = new GetSingleUserQuery.GetSingleUserHandler(constant, mapper, unitOfWork); 62 | var result = await handler.Handle(query, CancellationToken.None); 63 | result.UserList[0].Salutation.ShouldContain("Sir"); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/User/Queries/GetSingleUserQueryValidator.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests.User.Queries 2 | { 3 | using Shouldly; 4 | using System.Linq; 5 | using UserManagement.Application.Common.Interfaces; 6 | using UserManagement.Application.User.Queries; 7 | using Xunit; 8 | 9 | [Collection("UserCollection")] 10 | public class GetSingleUserQueryValidatorTest 11 | { 12 | private readonly IConfigConstants constant; 13 | 14 | public GetSingleUserQueryValidatorTest(UserFixture userFixture) 15 | { 16 | constant = userFixture.Constant; 17 | } 18 | 19 | [Fact] 20 | public void Validate_ReturnTrue_WhenAllDataIsValid() 21 | { 22 | var query = new GetSingleUserQuery 23 | { 24 | UserID = 110, 25 | }; 26 | 27 | var validator = new GetSingleUserQueryValidator(constant); 28 | var result = validator.Validate(query); 29 | result.IsValid.ShouldBeTrue(); 30 | } 31 | 32 | [Fact] 33 | public void Validate_ReturnFalse_WhenAllDataIsInValid() 34 | { 35 | var query = new GetSingleUserQuery 36 | { 37 | UserID = 0, 38 | }; 39 | 40 | var validator = new GetSingleUserQueryValidator(constant); 41 | var result = validator.Validate(query); 42 | result.IsValid.ShouldBeFalse(); 43 | } 44 | 45 | [Fact] 46 | public void Validate_ReturnErrorMsg_WhenUserIDIsInValid() 47 | { 48 | var query = new GetSingleUserQuery 49 | { 50 | UserID = 0, 51 | }; 52 | 53 | var validator = new GetSingleUserQueryValidator(constant); 54 | var result = validator.Validate(query); 55 | result.Errors.FirstOrDefault(x => x.ErrorMessage == constant.MSG_USER_NULLUSERID).ErrorMessage.ShouldBe(constant.MSG_USER_NULLUSERID); 56 | result.IsValid.ShouldBeFalse(); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/User/UserFixture.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.ApplicationTests.User 2 | { 3 | using Moq; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using UserManagement.Application.Common.Interfaces; 8 | using UserManagement.Domain.Repositories; 9 | using UserManagement.Domain.UnitOfWork; 10 | using Xunit; 11 | public class UserFixture : BaseFixture 12 | { 13 | public IConfigConstants Constant { get; } 14 | public IUnitOfWork UnitOfWork { get; } 15 | 16 | public UserFixture() 17 | { 18 | var mockConstant = new Mock(); 19 | mockConstant.SetupGet(p => p.MSG_USER_NULLUSERID).Returns("User Name is required!"); 20 | mockConstant.SetupGet(p => p.MSG_USER_NULLFIRSTNAME).Returns("First Name is required!"); 21 | mockConstant.SetupGet(p => p.MSG_USER_NULLLASTNAME).Returns("Last Name is required!"); 22 | mockConstant.SetupGet(p => p.MSG_USER_NULLDOB).Returns("DOB is required!"); 23 | mockConstant.SetupGet(p => p.MSG_USER_NULLGENDER).Returns("Gender is required!"); 24 | mockConstant.SetupGet(p => p.MSG_USER_NULLEMAILADDR).Returns("Email is required!"); 25 | mockConstant.SetupGet(p => p.MSG_USER_NULLPHNUM).Returns("Phone Number is required!"); 26 | mockConstant.SetupGet(p => p.MSG_USER_NULLCITY).Returns("City is required!"); 27 | mockConstant.SetupGet(p => p.MSG_USER_NULLSTATE).Returns("State is required!"); 28 | mockConstant.SetupGet(p => p.MSG_USER_NULLCOUNTRY).Returns("Country is required!"); 29 | Constant = mockConstant.Object; 30 | 31 | var mockUserRepo = new Mock(); 32 | var mockUnitOfWork = new Mock(); 33 | MockAddUser(mockUserRepo); 34 | MockUpdateUser(mockUserRepo); 35 | MockDeleteUser(mockUserRepo); 36 | MockGetAllUser(mockUserRepo); 37 | MockGetUser(mockUserRepo); 38 | mockUnitOfWork.SetupGet(repo => repo.Users).Returns(mockUserRepo.Object); 39 | UnitOfWork = mockUnitOfWork.Object; 40 | } 41 | 42 | private Mock MockAddUser(Mock mockRepo) 43 | { 44 | mockRepo.Setup(p => p.AddUser(It.IsAny())).Returns(Task.Run(() => 100)); 45 | return mockRepo; 46 | } 47 | 48 | private Mock MockUpdateUser(Mock mockRepo) 49 | { 50 | mockRepo.Setup(p => p.UpdateUser(It.IsAny())).Returns(Task.Run(() => true)); 51 | return mockRepo; 52 | } 53 | 54 | private Mock MockDeleteUser(Mock mockRepo) 55 | { 56 | mockRepo.Setup(p => p.DeleteUser(100)).Returns(Task.Run(() => true)); 57 | return mockRepo; 58 | } 59 | 60 | private Mock MockGetAllUser(Mock mockRepo) 61 | { 62 | mockRepo.Setup(p => p.GetAllUsers()).Returns(Task.Run(() => GetUserList())); 63 | return mockRepo; 64 | } 65 | 66 | private Mock MockGetUser(Mock mockRepo) 67 | { 68 | 69 | mockRepo.Setup(p => p.GetUser(110)).Returns(Task.Run(() => GetUserList().FirstOrDefault(u => u.UserID == 110))); 70 | mockRepo.Setup(p => p.GetUser(100)).Returns(Task.Run(() => GetUserList().FirstOrDefault(u => u.UserID == 100))); 71 | return mockRepo; 72 | } 73 | 74 | private IEnumerable GetUserList() 75 | { 76 | return new List 77 | { 78 | new Domain.Entities.User 79 | { 80 | FirstName = "John", 81 | LastName = "Doe", 82 | City = "Falls Chruch", 83 | Country = "USA", 84 | State = "VA", 85 | Zip = "22044", 86 | DateAdded = new System.DateTime(2019,01,01), 87 | DOB = new System.DateTime(1980,01,01), 88 | EmailAddress = "jdoe@fullstackhub.io", 89 | Gender = "M", 90 | PhoneNumber = "444-443-4444", 91 | UserID = 100 92 | }, 93 | new Domain.Entities.User 94 | { 95 | FirstName = "Lina", 96 | LastName = "Smith", 97 | City = "Fairfax", 98 | Country = "USA", 99 | State = "VA", 100 | Zip = "22019", 101 | DateAdded = new System.DateTime(2012,01,01), 102 | DOB = new System.DateTime(1999,01,01), 103 | EmailAddress = "lsmith@fullstackhub.io", 104 | Gender = "F", 105 | PhoneNumber = "333-443-7777", 106 | UserID = 110 107 | } 108 | }; 109 | } 110 | [CollectionDefinition("UserCollection")] 111 | public class QueryCollection : ICollectionFixture 112 | { 113 | } 114 | } 115 | } -------------------------------------------------------------------------------- /UserManagement.Application.UnitTests/UserManagement.Application.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /UserManagement.Application/Common/BaseClass/ApplicationBase.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.Common.BaseClass 2 | { 3 | using AutoMapper; 4 | using UserManagement.Application.Common.Interfaces; 5 | using UserManagement.Domain.UnitOfWork; 6 | public class ApplicationBase 7 | { 8 | public IUnitOfWork UnitOfWork { get; set; } 9 | public IConfigConstants ConfigConstants { get; set; } 10 | public IMapper Mapper { get; set; } 11 | 12 | public ApplicationBase(IConfigConstants configConstants, IUnitOfWork unitOfWork, IMapper mapper) 13 | { 14 | ConfigConstants = configConstants; 15 | UnitOfWork = unitOfWork; 16 | Mapper = mapper; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /UserManagement.Application/Common/Behaviors/LoggingBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.Common.Behaviors 2 | { 3 | using MediatR.Pipeline; 4 | using Microsoft.Extensions.Logging; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | public class LoggingBehaviour : IRequestPreProcessor 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public LoggingBehaviour(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | public async Task Process(TRequest request, CancellationToken cancellationToken) 18 | { 19 | var requestName = typeof(TRequest).Name; 20 | string userName = string.Empty; 21 | 22 | await Task.Run(() => _logger.LogInformation("UserManagement Request: {Name} {@UserName} {@Request}", 23 | requestName, userName, request)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /UserManagement.Application/Common/Behaviors/PerformanceBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.Common.Behaviors 2 | { 3 | using MediatR; 4 | using Microsoft.Extensions.Logging; 5 | using System.Diagnostics; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using UserManagement.Application.Common.Interfaces; 9 | public class PerformanceBehaviour : IPipelineBehavior 10 | { 11 | private readonly Stopwatch _timer; 12 | private readonly ILogger _logger; 13 | private readonly IConfigConstants _constact; 14 | 15 | public PerformanceBehaviour(ILogger logger, IConfigConstants constants) 16 | { 17 | _timer = new Stopwatch(); 18 | _logger = logger; 19 | _constact = constants; 20 | } 21 | 22 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 23 | { 24 | _timer.Start(); 25 | 26 | var response = await next(); 27 | 28 | _timer.Stop(); 29 | 30 | var elapsedMilliseconds = _timer.ElapsedMilliseconds; 31 | 32 | if (elapsedMilliseconds > _constact.LongRunningProcessMilliseconds) 33 | { 34 | var requestName = typeof(TRequest).Name; 35 | var userName = string.Empty; 36 | _logger.LogWarning("UserManagement Long Running Request: {Name} ({ElapsedMilliseconds} milliseconds) {@UserName} {@Request}", 37 | requestName, elapsedMilliseconds, userName, request); 38 | } 39 | 40 | return response; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /UserManagement.Application/Common/Behaviors/UnhandledExceptionBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.Common.Behaviours 2 | { 3 | using MediatR; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | public class UnhandledExceptionBehaviour : IPipelineBehavior 9 | { 10 | private readonly ILogger _logger; 11 | 12 | public UnhandledExceptionBehaviour(ILogger logger) 13 | { 14 | _logger = logger; 15 | } 16 | 17 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 18 | { 19 | try 20 | { 21 | return await next(); 22 | } 23 | catch (Exception ex) 24 | { 25 | var requestName = typeof(TRequest).Name; 26 | 27 | _logger.LogError(ex, "UserManagement Request: Unhandled Exception for Request {Name} {@Request}", requestName, request); 28 | 29 | throw; 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /UserManagement.Application/Common/Behaviors/ValidationBehaviour.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.Common.Behaviours 2 | { 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using FluentValidation; 8 | using MediatR; 9 | using ValidationException = UserManagement.Application.Common.Exceptions.ValidationException; 10 | public class ValidationBehaviour : IPipelineBehavior 11 | where TRequest : IRequest 12 | { 13 | private readonly IEnumerable> _validators; 14 | 15 | public ValidationBehaviour(IEnumerable> validators) 16 | { 17 | _validators = validators; 18 | } 19 | 20 | public async Task Handle(TRequest request, CancellationToken cancellationToken, RequestHandlerDelegate next) 21 | { 22 | if (_validators.Any()) 23 | { 24 | var context = new ValidationContext(request); 25 | 26 | var validationResults = await Task.WhenAll(_validators.Select(v => v.ValidateAsync(context, cancellationToken))); 27 | var failures = validationResults.SelectMany(r => r.Errors).Where(f => f != null).ToList(); 28 | 29 | if (failures.Count != 0) 30 | throw new ValidationException(failures); 31 | } 32 | return await next(); 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /UserManagement.Application/Common/Exceptions/NotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.Common.Exceptions 2 | { 3 | using System; 4 | public class NotFoundException : Exception 5 | { 6 | public NotFoundException() 7 | : base() 8 | { 9 | } 10 | 11 | public NotFoundException(string message) 12 | : base(message) 13 | { 14 | } 15 | 16 | public NotFoundException(string message, Exception innerException) 17 | : base(message, innerException) 18 | { 19 | } 20 | 21 | public NotFoundException(string name, object key) 22 | : base($"Entity \"{name}\" ({key}) was not found.") 23 | { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /UserManagement.Application/Common/Exceptions/ValidationException.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.Common.Exceptions 2 | { 3 | using FluentValidation.Results; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | public class ValidationException : Exception 8 | { 9 | public ValidationException() 10 | : base("One or more validation failures have occurred.") 11 | { 12 | Errors = new Dictionary(); 13 | } 14 | 15 | public ValidationException(IEnumerable failures) 16 | : this() 17 | { 18 | Errors = failures 19 | .GroupBy(e => e.PropertyName, e => e.ErrorMessage) 20 | .ToDictionary(failureGroup => failureGroup.Key, failureGroup => failureGroup.ToArray()); 21 | } 22 | 23 | public IDictionary Errors { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /UserManagement.Application/Common/Interfaces/IConfigConstants.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.Common.Interfaces 2 | { 3 | public interface IConfigConstants 4 | { 5 | string FullStackConnection { get; } 6 | string TestFullStackConnection { get; } 7 | int LongRunningProcessMilliseconds { get; } 8 | string MSG_USER_NULLUSERID { get; } 9 | string MSG_USER_NULLFIRSTNAME { get; } 10 | string MSG_USER_NULLLASTNAME { get; } 11 | string MSG_USER_NULLDOB { get; } 12 | string MSG_USER_NULLGENDER { get; } 13 | string MSG_USER_GENDER_LEN { get; } 14 | string MSG_USER_NULLEMAILADDR { get; } 15 | string MSG_USER_NULLPHNUM { get; } 16 | string MSG_USER_NULLCITY { get; } 17 | string MSG_USER_NULLSTATE { get; } 18 | string MSG_USER_NULLCOUNTRY { get; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /UserManagement.Application/Common/Mappings/IMapFrom.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.Common.Mappings 2 | { 3 | using AutoMapper; 4 | public interface IMapFrom 5 | { 6 | void Mapping(Profile profile) => profile.CreateMap(typeof(T), GetType()).ReverseMap(); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /UserManagement.Application/Common/Mappings/MappingProfile.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.Common.Mappings 2 | { 3 | using AutoMapper; 4 | using System; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | public class MappingProfile : Profile 9 | { 10 | public MappingProfile() 11 | { 12 | ApplyMappingsFromAssembly(Assembly.GetExecutingAssembly()); 13 | } 14 | 15 | private void ApplyMappingsFromAssembly(Assembly assembly) 16 | { 17 | var types = assembly.GetExportedTypes() 18 | .Where(t => t.GetInterfaces().Any(i => 19 | i.IsGenericType && i.GetGenericTypeDefinition() == typeof(IMapFrom<>))) 20 | .ToList(); 21 | 22 | foreach (var type in types) 23 | { 24 | var instance = Activator.CreateInstance(type); 25 | 26 | var methodInfo = type.GetMethod("Mapping") 27 | ?? type.GetInterface("IMapFrom`1").GetMethod("Mapping"); 28 | 29 | methodInfo?.Invoke(instance, new object[] { this }); 30 | 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /UserManagement.Application/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using FluentValidation; 3 | using MediatR; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using System.Reflection; 6 | using UserManagement.Application.Common.Behaviors; 7 | using UserManagement.Application.Common.Behaviours; 8 | 9 | namespace UserManagement.Application 10 | { 11 | public static class DependencyInjection 12 | { 13 | public static IServiceCollection AddApplication(this IServiceCollection services) 14 | { 15 | services.AddAutoMapper(Assembly.GetExecutingAssembly()); 16 | services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); 17 | services.AddMediatR(Assembly.GetExecutingAssembly()); 18 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(PerformanceBehaviour<,>)); 19 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>)); 20 | services.AddTransient(typeof(IPipelineBehavior<,>), typeof(UnhandledExceptionBehaviour<,>)); 21 | return services; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /UserManagement.Application/User/Commands/AddUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.Commands 2 | { 3 | using AutoMapper; 4 | using MediatR; 5 | using System; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using UserManagement.Application.Common.BaseClass; 9 | using UserManagement.Application.Common.Interfaces; 10 | using UserManagement.Domain.UnitOfWork; 11 | public class AddUserCommand : IRequest 12 | { 13 | public string FirstName { get; set; } 14 | public string LastName { get; set; } 15 | public DateTime DOB { get; set; } 16 | public string Gender { get; set; } 17 | public string EmailAddress { get; set; } 18 | public string PhoneNumber { get; set; } 19 | public string City { get; set; } 20 | public string State { get; set; } 21 | public string Zip { get; set; } 22 | public string Country { get; set; } 23 | public class AddNewUserHandler : ApplicationBase, IRequestHandler 24 | { 25 | public AddNewUserHandler(IConfigConstants constant, IMapper mapper, IUnitOfWork unitOfWork) 26 | : base(constant, unitOfWork, mapper) 27 | { 28 | } 29 | 30 | public async Task Handle(AddUserCommand request, CancellationToken cancellationToken) 31 | { 32 | var user = new UserManagement.Domain.Entities.User 33 | { 34 | FirstName = request.FirstName, 35 | LastName = request.LastName, 36 | DOB = request.DOB, 37 | Gender = request.Gender.ToUpper(), 38 | EmailAddress = request.EmailAddress, 39 | PhoneNumber = request.PhoneNumber, 40 | City = request.City, 41 | State = request.State, 42 | Zip = request.Zip, 43 | Country = request.Country 44 | }; 45 | this.UnitOfWork.StartTransaction(); 46 | var res = UnitOfWork.Users.AddUser(user).Result; 47 | this.UnitOfWork.Commit(); 48 | return await Task.Run(() => res, cancellationToken); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /UserManagement.Application/User/Commands/AddUserCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.Commands 2 | { 3 | using FluentValidation; 4 | using System.Linq; 5 | using UserManagement.Application.Common.Interfaces; 6 | public class AddUserCommandValidator : AbstractValidator 7 | { 8 | public AddUserCommandValidator(IConfigConstants constant) 9 | { 10 | this.RuleFor(v => v.FirstName).NotEmpty().WithMessage(constant.MSG_USER_NULLFIRSTNAME); 11 | this.RuleFor(v => v.LastName).NotEmpty().WithMessage(constant.MSG_USER_NULLLASTNAME); 12 | this.RuleFor(v => v.DOB).NotEmpty().WithMessage(constant.MSG_USER_NULLDOB); 13 | this.RuleFor(v => v.Gender).Must(x => (new string[] { "M", "F", "m", "f" }).Contains(x)).WithMessage(constant.MSG_USER_NULLGENDER); 14 | this.RuleFor(v => v.EmailAddress).NotEmpty().WithMessage(constant.MSG_USER_NULLEMAILADDR); 15 | this.RuleFor(v => v.PhoneNumber).NotEmpty().WithMessage(constant.MSG_USER_NULLPHNUM); 16 | this.RuleFor(v => v.City).NotEmpty().WithMessage(constant.MSG_USER_NULLCITY); 17 | this.RuleFor(v => v.State).NotEmpty().WithMessage(constant.MSG_USER_NULLSTATE); 18 | this.RuleFor(v => v.Country).NotEmpty().WithMessage(constant.MSG_USER_NULLCOUNTRY); 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /UserManagement.Application/User/Commands/DeleteUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.Commands 2 | { 3 | using AutoMapper; 4 | using MediatR; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using UserManagement.Application.Common.BaseClass; 8 | using UserManagement.Application.Common.Interfaces; 9 | using UserManagement.Domain.UnitOfWork; 10 | public class DeleteUserCommand : IRequest 11 | { 12 | public int UserID { get; set; } 13 | public class DeleteUserHandler : ApplicationBase, IRequestHandler 14 | { 15 | public DeleteUserHandler(IConfigConstants constant, IMapper mapper, IUnitOfWork unitOfWork) 16 | : base(constant, unitOfWork, mapper) 17 | { 18 | } 19 | 20 | public async Task Handle(DeleteUserCommand request, CancellationToken cancellationToken) 21 | { 22 | this.UnitOfWork.StartTransaction(); 23 | var res = UnitOfWork.Users.DeleteUser(request.UserID).Result; 24 | this.UnitOfWork.Commit(); 25 | return await Task.Run(() => res, cancellationToken); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /UserManagement.Application/User/Commands/DeleteUserCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.Commands 2 | { 3 | using FluentValidation; 4 | using UserManagement.Application.Common.Interfaces; 5 | public class DeleteUserCommandValidator : AbstractValidator 6 | { 7 | public DeleteUserCommandValidator(IConfigConstants constant) 8 | { 9 | this.RuleFor(v => v.UserID).GreaterThan(0).WithMessage(constant.MSG_USER_NULLUSERID); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /UserManagement.Application/User/Commands/UpdateUserCommand.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.Commands 2 | { 3 | using AutoMapper; 4 | using MediatR; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using UserManagement.Application.Common.BaseClass; 8 | using UserManagement.Application.Common.Exceptions; 9 | using UserManagement.Application.Common.Interfaces; 10 | using UserManagement.Domain.UnitOfWork; 11 | public class UpdateUserCommand : IRequest 12 | { 13 | public int UserID { get; set; } 14 | public string PhoneNumber { get; set; } 15 | public string City { get; set; } 16 | public string State { get; set; } 17 | public string Zip { get; set; } 18 | public string Country { get; set; } 19 | public class UpdateUserHandler : ApplicationBase, IRequestHandler 20 | { 21 | public UpdateUserHandler(IConfigConstants constant, IMapper mapper, IUnitOfWork unitOfWork) 22 | : base(constant, unitOfWork, mapper) 23 | { 24 | } 25 | 26 | public async Task Handle(UpdateUserCommand request, CancellationToken cancellationToken) 27 | { 28 | var user = await this.UnitOfWork.Users.GetUser(request.UserID); 29 | if (user == null) 30 | { 31 | throw new NotFoundException($"The User ID {request.UserID} is not found"); 32 | } 33 | 34 | user.PhoneNumber = request.PhoneNumber; 35 | user.City = request.City; 36 | user.State = request.State; 37 | user.Zip = request.Zip; 38 | user.Country = request.Country; 39 | this.UnitOfWork.StartTransaction(); 40 | var res = UnitOfWork.Users.UpdateUser(user).Result; 41 | this.UnitOfWork.Commit(); 42 | return await Task.Run(() => res, cancellationToken); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /UserManagement.Application/User/Commands/UpdateUserCommandValidator.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.Commands 2 | { 3 | using FluentValidation; 4 | using UserManagement.Application.Common.Interfaces; 5 | public class UpdateUserCommandValidator : AbstractValidator 6 | { 7 | public UpdateUserCommandValidator(IConfigConstants constant) 8 | { 9 | this.RuleFor(v => v.UserID).GreaterThan(0).WithMessage(constant.MSG_USER_NULLUSERID); 10 | this.RuleFor(v => v.PhoneNumber).NotEmpty().WithMessage(constant.MSG_USER_NULLPHNUM); 11 | this.RuleFor(v => v.City).NotEmpty().WithMessage(constant.MSG_USER_NULLCITY); 12 | this.RuleFor(v => v.State).NotEmpty().WithMessage(constant.MSG_USER_NULLSTATE); 13 | this.RuleFor(v => v.Country).NotEmpty().WithMessage(constant.MSG_USER_NULLCOUNTRY); 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /UserManagement.Application/User/DTO/UserDTO.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.DTO 2 | { 3 | using AutoMapper; 4 | using System; 5 | using UserManagement.Application.Common.Mappings; 6 | public class UserDTO : IMapFrom 7 | { 8 | public int UserID { get; set; } 9 | public string Salutation { get; set; } 10 | public string FirstName { get; set; } 11 | public string LastName { get; set; } 12 | public DateTime DOB { get; set; } 13 | public int Age { get; set; } 14 | public string Gender { get; set; } 15 | public string EmailAddress { get; set; } 16 | public string PhoneNumber { get; set; } 17 | public string City { get; set; } 18 | public string State { get; set; } 19 | public string Zip { get; set; } 20 | public string Country { get; set; } 21 | public void Mapping(Profile profile) 22 | { 23 | profile.CreateMap() 24 | .ForMember(d => d.Salutation, opt => opt.MapFrom(s => s.Gender.ToUpper() == "M" ? "Hi Sir!" : "Hi Ma'am!")) 25 | .ForMember(d => d.Age, opt => opt.MapFrom(s => DateTime.Today.Year - s.DOB.Year)) 26 | ; 27 | profile.CreateMap(); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /UserManagement.Application/User/Queries/GetAllUserQuery.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.Queries 2 | { 3 | using AutoMapper; 4 | using MediatR; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using UserManagement.Application.Common.BaseClass; 9 | using UserManagement.Application.Common.Interfaces; 10 | using UserManagement.Application.User.DTO; 11 | using UserManagement.Application.User.VM; 12 | using UserManagement.Domain.UnitOfWork; 13 | public class GetAllUserQuery : IRequest 14 | { 15 | public class GetAllUserHandler : ApplicationBase, IRequestHandler 16 | { 17 | public GetAllUserHandler(IConfigConstants constant, IMapper mapper, IUnitOfWork unitOfWork) 18 | : base(constant, unitOfWork, mapper) 19 | { 20 | } 21 | 22 | public async Task Handle(GetAllUserQuery request, CancellationToken cancellationToken) 23 | { 24 | var res = Mapper.Map(UnitOfWork.Users.GetAllUsers().Result, new List()); 25 | return await Task.FromResult(new UserVM() { UserList = res }); 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /UserManagement.Application/User/Queries/GetSingleUserQuery.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.Queries 2 | { 3 | using AutoMapper; 4 | using MediatR; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using UserManagement.Application.Common.BaseClass; 9 | using UserManagement.Application.Common.Interfaces; 10 | using UserManagement.Application.User.DTO; 11 | using UserManagement.Application.User.VM; 12 | using UserManagement.Domain.UnitOfWork; 13 | 14 | public class GetSingleUserQuery : IRequest 15 | { 16 | public int UserID { get; set; } 17 | public class GetSingleUserHandler : ApplicationBase, IRequestHandler 18 | { 19 | public GetSingleUserHandler(IConfigConstants constant, IMapper mapper, IUnitOfWork unitOfWork) 20 | : base(constant, unitOfWork, mapper) 21 | { 22 | } 23 | 24 | public async Task Handle(GetSingleUserQuery request, CancellationToken cancellationToken) 25 | { 26 | var res = this.Mapper.Map(this.UnitOfWork.Users.GetUser(request.UserID).Result, new UserDTO()); 27 | return await Task.FromResult(new UserVM() { UserList = new List { res } }); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /UserManagement.Application/User/Queries/GetSingleUserQueryValidator.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.Queries 2 | { 3 | using FluentValidation; 4 | using UserManagement.Application.Common.Interfaces; 5 | public class GetSingleUserQueryValidator : AbstractValidator 6 | { 7 | public GetSingleUserQueryValidator(IConfigConstants constant) 8 | { 9 | this.RuleFor(v => v.UserID).GreaterThan(0).WithMessage(constant.MSG_USER_NULLUSERID); 10 | } 11 | } 12 | } -------------------------------------------------------------------------------- /UserManagement.Application/User/VM/UserVM.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Application.User.VM 2 | { 3 | using System.Collections.Generic; 4 | using UserManagement.Application.User.DTO; 5 | public class UserVM 6 | { 7 | public IList UserList { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /UserManagement.Application/UserManagement.Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /UserManagement.Domain/Entities/User.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Domain.Entities 2 | { 3 | using Dapper.Contrib.Extensions; 4 | using System; 5 | 6 | [Table("[User]")] 7 | public class User 8 | { 9 | [Key] 10 | public int UserID { get; set; } 11 | public string FirstName { get; set; } 12 | public string LastName { get; set; } 13 | public DateTime DOB { get; set; } 14 | public string Gender { get; set; } 15 | public string EmailAddress { get; set; } 16 | public string PhoneNumber { get; set; } 17 | public string City { get; set; } 18 | public string State { get; set; } 19 | public string Zip { get; set; } 20 | public string Country { get; set; } 21 | public DateTime DateAdded { get; set; } 22 | public DateTime? DateUpdated { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /UserManagement.Domain/Repositories/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Domain.Repositories 2 | { 3 | using System.Collections.Generic; 4 | using System.Threading.Tasks; 5 | using UserManagement.Domain.Entities; 6 | 7 | public interface IUserRepository 8 | { 9 | Task AddUser(User user); 10 | Task UpdateUser(User user); 11 | Task DeleteUser(int userId); 12 | Task> GetAllUsers(); 13 | Task GetUser(long userId); 14 | Task DeleteAllUser(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /UserManagement.Domain/UnitOfWork/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Domain.UnitOfWork 2 | { 3 | using UserManagement.Domain.Repositories; 4 | public interface IUnitOfWork 5 | { 6 | IUserRepository Users { get; } 7 | void StartTransaction(); 8 | void Commit(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /UserManagement.Domain/UserManagement.Domain.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /UserManagement.Persistence.IntegrationTests/User/UserRepositoryTest.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Persistance.IntegrationTests.User 2 | { 3 | using Shouldly; 4 | using System; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using UserManagement.Domain.Repositories; 8 | using Xunit; 9 | 10 | [Collection("UserCollection")] 11 | public class UserRepositoryTest : IDisposable 12 | { 13 | private readonly IUserRepository userRepository; 14 | public UserRepositoryTest(UserFixture fixture) 15 | { 16 | userRepository = fixture.UserRepository; 17 | } 18 | 19 | [Fact] 20 | public async Task TestAddUser_GivenCorrectParam_ReturnUserID() 21 | { 22 | var res = await AddNewUser(); 23 | res.ShouldBeGreaterThan(0); 24 | } 25 | 26 | [Fact] 27 | public async Task TestGetAllUsers_GivenCorrectParam_ReturnUserList() 28 | { 29 | await AddNewUser(); 30 | var res = await userRepository.GetAllUsers(); 31 | res.Count().ShouldBeGreaterThan(0); 32 | } 33 | 34 | [Fact] 35 | public async Task TestGetUserByID_GivenCorrectParam_ReturnUserList() 36 | { 37 | var userId = await AddNewUser(); 38 | var res = await userRepository.GetUser(userId); 39 | res.ShouldBeOfType(); 40 | } 41 | 42 | 43 | [Fact] 44 | public async Task TestUpdateUser_GivenCorrectParam_ReturnTrue() 45 | { 46 | var userId = AddNewUser().Result; 47 | var user = new Domain.Entities.User 48 | { 49 | FirstName = "John", 50 | LastName = "Doe", 51 | City = "Falls Chruch", 52 | Country = "USA", 53 | State = "VA", 54 | Zip = "22044", 55 | DateAdded = new System.DateTime(2019, 01, 01), 56 | DOB = new System.DateTime(1980, 01, 01), 57 | EmailAddress = "jdoe@fullstackhub.io", 58 | Gender = "F", 59 | PhoneNumber = "000-000-000", 60 | UserID = userId, 61 | }; 62 | 63 | var res = await userRepository.UpdateUser(user); 64 | res.ShouldBeTrue(); 65 | } 66 | 67 | [Fact] 68 | public async Task TestDeleteUser_GivenCorrectParam_ReturnTrue() 69 | { 70 | var userId = AddNewUser().Result; 71 | var res = await userRepository.DeleteUser(userId); 72 | res.ShouldBeTrue(); 73 | } 74 | 75 | private async Task AddNewUser() 76 | { 77 | var user = new Domain.Entities.User 78 | { 79 | FirstName = "John", 80 | LastName = "Doe", 81 | City = "Falls Chruch", 82 | Country = "USA", 83 | State = "VA", 84 | Zip = "22044", 85 | DateAdded = new System.DateTime(2019, 01, 01), 86 | DOB = new System.DateTime(1980, 01, 01), 87 | EmailAddress = "jdoe@fullstackhub.io", 88 | Gender = "M", 89 | PhoneNumber = "444-443-4444" 90 | }; 91 | 92 | return await userRepository.AddUser(user); 93 | } 94 | 95 | public void Dispose() 96 | { 97 | userRepository.DeleteAllUser(); 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /UserManagement.Persistence.IntegrationTests/UserFixture.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Persistance.IntegrationTests 2 | { 3 | using System.Data; 4 | using System.Data.SqlClient; 5 | using UserManagement.Domain.Repositories; 6 | using UserManagement.Persistence.Repositories; 7 | using Xunit; 8 | public class UserFixture 9 | { 10 | public IUserRepository UserRepository { get; } 11 | 12 | public UserFixture() 13 | { 14 | IDbConnection dbConnection = new SqlConnection("Data Source=localhost\\SQLEXPRESS;Persist Security Info=True;Integrated Security=SSPI;Initial Catalog=TestFullstackHub"); 15 | UserRepository = new UserRepository(dbConnection, null); 16 | } 17 | 18 | [CollectionDefinition("UserCollection")] 19 | public class QueryCollection : ICollectionFixture 20 | { 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /UserManagement.Persistence.IntegrationTests/UserManagement.Persistence.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /UserManagement.Persistence/Constant/ConfigConstants.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Persistence.Constant 2 | { 3 | using Microsoft.Extensions.Configuration; 4 | using UserManagement.Application.Common.Interfaces; 5 | public class ConfigConstants : IConfigConstants 6 | { 7 | public IConfiguration Configuration { get; } 8 | public ConfigConstants(IConfiguration configuration) 9 | { 10 | this.Configuration = configuration; 11 | } 12 | public string FullStackConnection => this.Configuration.GetConnectionString("FullStackConnection"); 13 | 14 | public string TestFullStackConnection => this.Configuration.GetConnectionString("TestFullStackConnection"); 15 | public int LongRunningProcessMilliseconds => int.Parse(this.Configuration["AppSettings:LongRunningProcessMilliseconds"]); 16 | 17 | public string MSG_USER_NULLUSERID => this.Configuration["AppSettings:MSG_USER_NULLUSERID"]; 18 | public string MSG_USER_NULLFIRSTNAME => this.Configuration["AppSettings:MSG_USER_NULLFIRSTNAME"]; 19 | public string MSG_USER_NULLLASTNAME => this.Configuration["AppSettings:MSG_USER_NULLLASTNAME"]; 20 | public string MSG_USER_NULLDOB => this.Configuration["AppSettings:MSG_USER_NULLDOB"]; 21 | public string MSG_USER_NULLGENDER => this.Configuration["AppSettings:MSG_USER_NULLGENDER"]; 22 | public string MSG_USER_GENDER_LEN => this.Configuration["AppSettings:MSG_USER_GENDER_LEN"]; 23 | public string MSG_USER_NULLEMAILADDR => this.Configuration["AppSettings:MSG_USER_NULLEMAILADDR"]; 24 | public string MSG_USER_NULLPHNUM => this.Configuration["AppSettings:MSG_USER_NULLPHNUM"]; 25 | public string MSG_USER_NULLCITY => this.Configuration["AppSettings:MSG_USER_NULLCITY"]; 26 | public string MSG_USER_NULLSTATE => this.Configuration["AppSettings:MSG_USER_NULLSTATE"]; 27 | public string MSG_USER_NULLCOUNTRY => this.Configuration["AppSettings:MSG_USER_NULLCOUNTRY"]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /UserManagement.Persistence/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Persistence 2 | { 3 | using Microsoft.Extensions.DependencyInjection; 4 | using System.Data; 5 | using System.Data.SqlClient; 6 | using UserManagement.Application.Common.Interfaces; 7 | using UserManagement.Domain.UnitOfWork; 8 | using UserManagement.Persistence.Constant; 9 | public static class DependencyInjection 10 | { 11 | public static IServiceCollection AddPersistance(this IServiceCollection services) 12 | { 13 | services.AddSingleton(); 14 | services.AddSingleton(conn => new SqlConnection(conn.GetService().FullStackConnection)); 15 | services.AddTransient(uof => new UnitOfWork.UnitOfWork(uof.GetService())); 16 | return services; 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /UserManagement.Persistence/Repositories/Repository.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Persistence.Repositories 2 | { 3 | using System.Data; 4 | public class Repository 5 | { 6 | protected IDbConnection DbConnection { get; } 7 | protected IDbTransaction DbTransaction { get; } 8 | public Repository(IDbConnection dbConnection, IDbTransaction dbTransaction) 9 | { 10 | DbTransaction = dbTransaction; 11 | DbConnection = dbConnection; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /UserManagement.Persistence/Repositories/UserRepository.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Persistence.Repositories 2 | { 3 | using Dapper.Contrib.Extensions; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Data; 7 | using System.Threading.Tasks; 8 | using UserManagement.Domain.Entities; 9 | using UserManagement.Domain.Repositories; 10 | 11 | public class UserRepository : Repository, IUserRepository 12 | { 13 | public UserRepository(IDbConnection dbConnection, IDbTransaction dbtransaction) 14 | : base(dbConnection, dbtransaction) 15 | { 16 | } 17 | public Task AddUser(User user) 18 | { 19 | user.DateAdded = DateTime.Now; 20 | user.DateUpdated = null; 21 | return DbConnection.InsertAsync(user, DbTransaction); 22 | } 23 | 24 | public Task DeleteUser(int userId) 25 | { 26 | return DbConnection.DeleteAsync(new User { UserID = userId }, DbTransaction); 27 | } 28 | 29 | public Task> GetAllUsers() 30 | { 31 | return DbConnection.GetAllAsync(); 32 | } 33 | 34 | public Task GetUser(long userId) 35 | { 36 | return DbConnection.GetAsync(userId); 37 | } 38 | 39 | public Task UpdateUser(User user) 40 | { 41 | user.DateUpdated = DateTime.Now; 42 | return DbConnection.UpdateAsync(user, DbTransaction); 43 | } 44 | 45 | public Task DeleteAllUser() 46 | { 47 | return DbConnection.DeleteAllAsync(); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /UserManagement.Persistence/UnitOfWork/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | namespace UserManagement.Persistence.UnitOfWork 2 | { 3 | using System.Data; 4 | using UserManagement.Domain.Repositories; 5 | using UserManagement.Domain.UnitOfWork; 6 | using UserManagement.Persistence.Repositories; 7 | 8 | public class UnitOfWork : IUnitOfWork 9 | { 10 | private IDbConnection dbConnection; 11 | private IDbTransaction transaction; 12 | public UnitOfWork(IDbConnection dbConnection) 13 | { 14 | this.dbConnection = dbConnection; 15 | this.ManageConnection(); 16 | } 17 | public IUserRepository Users => new UserRepository(this.dbConnection, this.transaction); 18 | 19 | public void StartTransaction() 20 | { 21 | 22 | if (this.transaction == null) 23 | { 24 | this.transaction = this.dbConnection.BeginTransaction(); 25 | } 26 | } 27 | public void Commit() 28 | { 29 | try 30 | { 31 | this.transaction.Commit(); 32 | } 33 | catch 34 | { 35 | this.transaction.Rollback(); 36 | } 37 | } 38 | 39 | private void ManageConnection() 40 | { 41 | if (this.dbConnection.State == ConnectionState.Closed) 42 | { 43 | this.dbConnection.Open(); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /UserManagement.Persistence/UserManagement.Persistence.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /UserManagement.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.30413.136 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.API", "UserManagement.API\UserManagement.API.csproj", "{C6916167-B5D0-4422-807B-23E046A445BE}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{3CCCA724-B32F-4922-AAFF-933ABA3FBAE3}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{B9047929-76B2-4949-97FA-ABBA6A0F08FF}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Application", "UserManagement.Application\UserManagement.Application.csproj", "{71B03A26-C251-452E-A8C3-1679C737024F}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Domain", "UserManagement.Domain\UserManagement.Domain.csproj", "{6650FED4-AB07-4916-8D46-04DBE78DBF6B}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Persistence", "UserManagement.Persistence\UserManagement.Persistence.csproj", "{45C639CE-ED7B-4050-A71E-BB24DBDFE3AB}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Application.UnitTests", "UserManagement.Application.UnitTests\UserManagement.Application.UnitTests.csproj", "{4D0889FC-829F-4A29-8DE5-4051DDF0ED8B}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "UserManagement.Persistence.IntegrationTests", "UserManagement.Persistence.IntegrationTests\UserManagement.Persistence.IntegrationTests.csproj", "{6FDA919F-2367-47DF-A308-29115F84F9FF}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UserManagement.API.IntegrationTests", "UserManagement.API.IntegrationTests\UserManagement.API.IntegrationTests.csproj", "{A9CF9035-8F4B-4516-A17C-02450E2FBD98}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {C6916167-B5D0-4422-807B-23E046A445BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {C6916167-B5D0-4422-807B-23E046A445BE}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {C6916167-B5D0-4422-807B-23E046A445BE}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {C6916167-B5D0-4422-807B-23E046A445BE}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {71B03A26-C251-452E-A8C3-1679C737024F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {71B03A26-C251-452E-A8C3-1679C737024F}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {71B03A26-C251-452E-A8C3-1679C737024F}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {71B03A26-C251-452E-A8C3-1679C737024F}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {6650FED4-AB07-4916-8D46-04DBE78DBF6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {6650FED4-AB07-4916-8D46-04DBE78DBF6B}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {6650FED4-AB07-4916-8D46-04DBE78DBF6B}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {6650FED4-AB07-4916-8D46-04DBE78DBF6B}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {45C639CE-ED7B-4050-A71E-BB24DBDFE3AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {45C639CE-ED7B-4050-A71E-BB24DBDFE3AB}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {45C639CE-ED7B-4050-A71E-BB24DBDFE3AB}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {45C639CE-ED7B-4050-A71E-BB24DBDFE3AB}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {4D0889FC-829F-4A29-8DE5-4051DDF0ED8B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {4D0889FC-829F-4A29-8DE5-4051DDF0ED8B}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {4D0889FC-829F-4A29-8DE5-4051DDF0ED8B}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {4D0889FC-829F-4A29-8DE5-4051DDF0ED8B}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {6FDA919F-2367-47DF-A308-29115F84F9FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {6FDA919F-2367-47DF-A308-29115F84F9FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {6FDA919F-2367-47DF-A308-29115F84F9FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {6FDA919F-2367-47DF-A308-29115F84F9FF}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {A9CF9035-8F4B-4516-A17C-02450E2FBD98}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {A9CF9035-8F4B-4516-A17C-02450E2FBD98}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {A9CF9035-8F4B-4516-A17C-02450E2FBD98}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {A9CF9035-8F4B-4516-A17C-02450E2FBD98}.Release|Any CPU.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(NestedProjects) = preSolution 63 | {C6916167-B5D0-4422-807B-23E046A445BE} = {B9047929-76B2-4949-97FA-ABBA6A0F08FF} 64 | {71B03A26-C251-452E-A8C3-1679C737024F} = {B9047929-76B2-4949-97FA-ABBA6A0F08FF} 65 | {6650FED4-AB07-4916-8D46-04DBE78DBF6B} = {B9047929-76B2-4949-97FA-ABBA6A0F08FF} 66 | {45C639CE-ED7B-4050-A71E-BB24DBDFE3AB} = {B9047929-76B2-4949-97FA-ABBA6A0F08FF} 67 | {4D0889FC-829F-4A29-8DE5-4051DDF0ED8B} = {3CCCA724-B32F-4922-AAFF-933ABA3FBAE3} 68 | {6FDA919F-2367-47DF-A308-29115F84F9FF} = {3CCCA724-B32F-4922-AAFF-933ABA3FBAE3} 69 | {A9CF9035-8F4B-4516-A17C-02450E2FBD98} = {3CCCA724-B32F-4922-AAFF-933ABA3FBAE3} 70 | EndGlobalSection 71 | GlobalSection(ExtensibilityGlobals) = postSolution 72 | SolutionGuid = {EEF049B4-7F13-4856-8723-E74D7812EB83} 73 | EndGlobalSection 74 | EndGlobal 75 | --------------------------------------------------------------------------------