├── .gitignore
├── LICENSE
├── README.md
├── client
├── .dockerignore
├── Dockerfile
├── StudentManagement.Client.sln
├── nginx.conf
└── src
│ └── WebUI
│ ├── App.razor
│ ├── Dtos
│ └── StudentDto.cs
│ ├── Layout
│ ├── MainLayout.razor
│ └── MainLayout.razor.css
│ ├── Pages
│ ├── Home.razor
│ └── Students
│ │ ├── Add.razor
│ │ ├── Delete.razor
│ │ ├── Edit.razor
│ │ ├── Index.razor
│ │ └── View.razor
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── Services
│ ├── ApiUrls.cs
│ └── StudentService.cs
│ ├── WebUI.csproj
│ ├── _Imports.razor
│ └── wwwroot
│ ├── css
│ ├── app.css
│ ├── bootstrap
│ │ ├── bootstrap.min.css
│ │ └── bootstrap.min.css.map
│ ├── navbar.css
│ └── style.css
│ ├── images
│ ├── add.svg
│ ├── back.png
│ ├── delete.svg
│ ├── edit.svg
│ ├── favicon.png
│ ├── home.svg
│ ├── icon-192.png
│ ├── logout.png
│ ├── navbar.svg
│ ├── profile-photo.png
│ ├── students.png
│ └── view.png
│ └── index.html
├── docker-compose.yml
└── server
├── .dockerignore
├── Dockerfile
├── StudentManagement.Server.sln
├── src
├── Application
│ ├── Application.csproj
│ ├── Behaviours
│ │ └── ValidationBehaviour.cs
│ ├── Consts
│ │ └── StudentConsts.cs
│ ├── DependencyInjection.cs
│ ├── RequestModels
│ │ ├── CreateStudentRequest.cs
│ │ └── UpdateStudentRequest.cs
│ ├── ResponseModels
│ │ ├── ErrorResponse.cs
│ │ └── StudentResponse.cs
│ └── Students
│ │ ├── Commands
│ │ ├── CreateStudentCommand.cs
│ │ ├── CreateStudentCommandValidator.cs
│ │ ├── DeleteStudentCommand.cs
│ │ ├── DeleteStudentCommandValidator.cs
│ │ ├── UpdateStudentCommand.cs
│ │ └── UpdateStudentCommandValidator.cs
│ │ ├── Handlers
│ │ ├── CommandHandlers
│ │ │ ├── CreateStudentHandler.cs
│ │ │ ├── DeleteStudentHandler.cs
│ │ │ └── UpdateStudentHandler.cs
│ │ └── QueryHandlers
│ │ │ ├── GetAllStudentsHandler.cs
│ │ │ └── GetStudentByIdHandler.cs
│ │ └── Queries
│ │ ├── GetAllStudentsQuery.cs
│ │ └── GetStudentByIdQuery.cs
├── Domain
│ ├── Common
│ │ ├── AuditableEntityBase.cs
│ │ └── EntityBase.cs
│ ├── Domain.csproj
│ ├── Entities
│ │ └── Student.cs
│ ├── Interfaces
│ │ └── IStudentRepository.cs
│ └── ValueObjects
│ │ └── Email.cs
├── Infrastructure
│ ├── Configurations
│ │ └── StudentConfiguration.cs
│ ├── DbContexts
│ │ └── StudentManagementDBContext.cs
│ ├── DependencyInjection.cs
│ ├── Infrastructure.csproj
│ ├── Migrations
│ │ ├── 20240521092028_v1.Designer.cs
│ │ ├── 20240521092028_v1.cs
│ │ └── StudentManagementDBContextModelSnapshot.cs
│ └── Repositories
│ │ └── StudentRepository.cs
└── WebAPI
│ ├── Controllers
│ └── StudentsController.cs
│ ├── Middleware
│ └── GlobalExceptionHandler.cs
│ ├── Program.cs
│ ├── Properties
│ └── launchSettings.json
│ ├── RequestExamples.http
│ ├── WebAPI.csproj
│ ├── appsettings.Development.json
│ └── appsettings.json
└── tests
├── Application.Tests.Integration
├── Application.Tests.Integration.csproj
├── BaseHandlerTest.cs
├── GlobalUsings.cs
├── IntegrationTestWebApplicationFactory.cs
└── StudentHandlersTests
│ └── StudentHandlersTests.cs
├── Application.Tests.Unit
├── Application.Tests.Unit.csproj
├── GlobalUsings.cs
└── StudentHandlersTests
│ ├── CreateStudentHandlerTests.cs
│ ├── DeleteStudentHandlerTests.cs
│ ├── GetAllStudentsHandlerTests.cs
│ ├── GetStudentByIdHandlerTests.cs
│ └── UpdateStudentHandlerTests.cs
├── Domain.Tests.Unit
├── Domain.Tests.Unit.csproj
├── GlobalUsings.cs
└── ValueObjectsTests
│ └── EmailTests.cs
├── WebAPI.Tests.Acceptance
├── BaseAcceptanceTest.cs
├── Drivers
│ └── StudentManagementDriver.cs
├── Dtos
│ └── StudentDto.cs
├── Features
│ ├── StudentManagement.feature
│ └── StudentManagement.feature.cs
├── GlobalUsings.cs
├── Hooks
│ └── AcceptanceTestWebApplicationFactory.cs
├── StepDefinitions
│ └── StudentManagementSteps.cs
└── WebAPI.Tests.Acceptance.csproj
└── WebAPI.Tests.Integration
├── BaseControllerTest.cs
├── ControllersTests
└── StudentsControllerTests.cs
├── GlobalUsings.cs
└── WebAPI.Tests.Integration.csproj
/.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/main/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 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.tlog
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
298 | *.vbp
299 |
300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
301 | *.dsw
302 | *.dsp
303 |
304 | # Visual Studio 6 technical files
305 | *.ncb
306 | *.aps
307 |
308 | # Visual Studio LightSwitch build output
309 | **/*.HTMLClient/GeneratedArtifacts
310 | **/*.DesktopClient/GeneratedArtifacts
311 | **/*.DesktopClient/ModelManifest.xml
312 | **/*.Server/GeneratedArtifacts
313 | **/*.Server/ModelManifest.xml
314 | _Pvt_Extensions
315 |
316 | # Paket dependency manager
317 | .paket/paket.exe
318 | paket-files/
319 |
320 | # FAKE - F# Make
321 | .fake/
322 |
323 | # CodeRush personal settings
324 | .cr/personal
325 |
326 | # Python Tools for Visual Studio (PTVS)
327 | __pycache__/
328 | *.pyc
329 |
330 | # Cake - Uncomment if you are using it
331 | # tools/**
332 | # !tools/packages.config
333 |
334 | # Tabs Studio
335 | *.tss
336 |
337 | # Telerik's JustMock configuration file
338 | *.jmconfig
339 |
340 | # BizTalk build output
341 | *.btp.cs
342 | *.btm.cs
343 | *.odx.cs
344 | *.xsd.cs
345 |
346 | # OpenCover UI analysis results
347 | OpenCover/
348 |
349 | # Azure Stream Analytics local run output
350 | ASALocalRun/
351 |
352 | # MSBuild Binary and Structured Log
353 | *.binlog
354 |
355 | # NVidia Nsight GPU debugger configuration file
356 | *.nvuser
357 |
358 | # MFractors (Xamarin productivity tool) working folder
359 | .mfractor/
360 |
361 | # Local History for Visual Studio
362 | .localhistory/
363 |
364 | # Visual Studio History (VSHistory) files
365 | .vshistory/
366 |
367 | # BeatPulse healthcheck temp database
368 | healthchecksdb
369 |
370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
371 | MigrationBackup/
372 |
373 | # Ionide (cross platform F# VS Code tools) working folder
374 | .ionide/
375 |
376 | # Fody - auto-generated XML schema
377 | FodyWeavers.xsd
378 |
379 | # VS Code files for those working on multiple tools
380 | .vscode/*
381 | !.vscode/settings.json
382 | !.vscode/tasks.json
383 | !.vscode/launch.json
384 | !.vscode/extensions.json
385 | *.code-workspace
386 |
387 | # Local History for Visual Studio Code
388 | .history/
389 |
390 | # Windows Installer files from build outputs
391 | *.cab
392 | *.msi
393 | *.msix
394 | *.msm
395 | *.msp
396 |
397 | # JetBrains Rider
398 | *.sln.iml
399 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Sara Rasoulian
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 | ## Student Management Application
2 | A sample full-stack application with a backend __ASP.NET Core Web API__ project and a frontend __Blazor Web Assembly__ project.
3 |
4 | 
5 |
6 |
7 | ## Technology Stack
8 | - ASP.NET Core Web API -v8
9 | - Entity Framework Core -v8
10 | - DDD (Domain-Driven Design)
11 | - TDD (Test-Driven Development)
12 | - BDD (Behavior-driven Development)
13 | - Clean Architecture
14 | - Clean Code
15 | - Repository Design Pattern
16 | - CQRS Design Pattern
17 | - Mediator Design pattern
18 | - PostgreSQL Database
19 | - Blazor WebAssembly -v8
20 | - Bootstrap -v5
21 | - Docker
22 |
23 | #### Nuget Packages
24 | - __xUnit__ for unit, integration and acceptance testing
25 | - __Testcontainers__ for integration and acceptance testing
26 | - __SpecFlow__ for acceptance testing
27 | - __NSubstitute__ for mocking
28 | - __Bogus__ for fake data generating
29 | - __MediatR__ for implementing mediator pattern
30 | - __FluentValidation__ for server-side validation
31 | - __Mapster__ for object mapping
32 | - __Blazor.Bootstrap__ for implementing user-interface
33 |
34 |
35 | This repository is intended for demonstrating best practices in software development. In real-world applications, these practices should be selected based on the specific requirements of each project.
36 |
37 |
38 |
39 | ## Run with Docker
40 |
41 | #### 1. Start with Docker compose
42 |
43 | Run the following command in project directory:
44 |
45 | ```
46 | docker-compose up -d
47 | ```
48 |
49 | Docker compose in this application includes 4 services:
50 |
51 | - __Web API application__ will be listening at `http://localhost:5000`
52 |
53 | - __Blazor webAssembly application__ will be listening at `http://localhost:8080`
54 |
55 | - __Postgres database__ will be listening at `http://localhost:5433`
56 |
57 | - __PgAdmin4 web interface__ will be listening at `http://localhost:8000`
58 |
59 |
60 | #### 2. Run the migrations
61 |
62 | Open `StudentManagement.Server.sln` file in visual studio, then in package manager console tab, run:
63 |
64 | ```
65 | update-database
66 | ```
67 |
68 | This command will generate the database schema in postgres container.
69 |
70 |
71 | ---
72 |
73 | Make sure Docker engine is running, before running integration and acceptance tests.
74 |
75 |
--------------------------------------------------------------------------------
/client/.dockerignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/csharp
3 |
4 | ### Csharp ###
5 | ## Ignore Visual Studio temporary files, build results, and
6 | ## files generated by popular Visual Studio add-ons.
7 |
8 | # User-specific files
9 | *.suo
10 | *.user
11 | *.userosscache
12 | *.sln.docstates
13 |
14 | # User-specific files (MonoDevelop/Xamarin Studio)
15 | *.userprefs
16 |
17 | # Build results
18 | [Dd]ebug/
19 | [Dd]ebugPublic/
20 | [Rr]elease/
21 | [Rr]eleases/
22 | x64/
23 | x86/
24 | bld/
25 | [Bb]in/
26 | [Oo]bj/
27 | [Ll]og/
28 |
29 | # Visual Studio cache/options directory
30 | .vs/
31 | # Uncomment if you have tasks that create the project's static files in wwwroot
32 | #wwwroot/
33 |
34 | # MSTest test Results
35 | [Tt]est[Rr]esult*/
36 | [Bb]uild[Ll]og.*
37 |
38 | # NUNIT
39 | *.VisualState.xml
40 | TestResult.xml
41 |
42 | # Build Results of an ATL Project
43 | [Dd]ebugPS/
44 | [Rr]eleasePS/
45 | dlldata.c
46 |
47 | # DNX
48 | project.lock.json
49 | project.fragment.lock.json
50 | artifacts/
51 | Properties/launchSettings.json
52 |
53 | *_i.c
54 | *_p.c
55 | *_i.h
56 | *.ilk
57 | *.meta
58 | *.obj
59 | *.pch
60 | *.pdb
61 | *.pgc
62 | *.pgd
63 | *.rsp
64 | *.sbr
65 | *.tlb
66 | *.tli
67 | *.tlh
68 | *.tmp
69 | *.tmp_proj
70 | *.log
71 | *.vspscc
72 | *.vssscc
73 | .builds
74 | *.pidb
75 | *.svclog
76 | *.scc
77 |
78 | # Chutzpah Test files
79 | _Chutzpah*
80 |
81 | # Visual C++ cache files
82 | ipch/
83 | *.aps
84 | *.ncb
85 | *.opendb
86 | *.opensdf
87 | *.sdf
88 | *.cachefile
89 | *.VC.db
90 | *.VC.VC.opendb
91 |
92 | # Visual Studio profiler
93 | *.psess
94 | *.vsp
95 | *.vspx
96 | *.sap
97 |
98 | # TFS 2012 Local Workspace
99 | $tf/
100 |
101 | # Guidance Automation Toolkit
102 | *.gpState
103 |
104 | # ReSharper is a .NET coding add-in
105 | _ReSharper*/
106 | *.[Rr]e[Ss]harper
107 | *.DotSettings.user
108 |
109 | # JustCode is a .NET coding add-in
110 | .JustCode
111 |
112 | # TeamCity is a build add-in
113 | _TeamCity*
114 |
115 | # DotCover is a Code Coverage Tool
116 | *.dotCover
117 |
118 | # Visual Studio code coverage results
119 | *.coverage
120 | *.coveragexml
121 |
122 | # NCrunch
123 | _NCrunch_*
124 | .*crunch*.local.xml
125 | nCrunchTemp_*
126 |
127 | # MightyMoose
128 | *.mm.*
129 | AutoTest.Net/
130 |
131 | # Web workbench (sass)
132 | .sass-cache/
133 |
134 | # Installshield output folder
135 | [Ee]xpress/
136 |
137 | # DocProject is a documentation generator add-in
138 | DocProject/buildhelp/
139 | DocProject/Help/*.HxT
140 | DocProject/Help/*.HxC
141 | DocProject/Help/*.hhc
142 | DocProject/Help/*.hhk
143 | DocProject/Help/*.hhp
144 | DocProject/Help/Html2
145 | DocProject/Help/html
146 |
147 | # Click-Once directory
148 | publish/
149 |
150 | # Publish Web Output
151 | *.[Pp]ublish.xml
152 | *.azurePubxml
153 | # TODO: Comment the next line if you want to checkin your web deploy settings
154 | # but database connection strings (with potential passwords) will be unencrypted
155 | *.pubxml
156 | *.publishproj
157 |
158 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
159 | # checkin your Azure Web App publish settings, but sensitive information contained
160 | # in these scripts will be unencrypted
161 | PublishScripts/
162 |
163 | # NuGet Packages
164 | *.nupkg
165 | # The packages folder can be ignored because of Package Restore
166 | **/packages/*
167 | # except build/, which is used as an MSBuild target.
168 | !**/packages/build/
169 | # Uncomment if necessary however generally it will be regenerated when needed
170 | #!**/packages/repositories.config
171 | # NuGet v3's project.json files produces more ignoreable files
172 | *.nuget.props
173 | *.nuget.targets
174 |
175 | # Microsoft Azure Build Output
176 | csx/
177 | *.build.csdef
178 |
179 | # Microsoft Azure Emulator
180 | ecf/
181 | rcf/
182 |
183 | # Windows Store app package directories and files
184 | AppPackages/
185 | BundleArtifacts/
186 | Package.StoreAssociation.xml
187 | _pkginfo.txt
188 |
189 | # Visual Studio cache files
190 | # files ending in .cache can be ignored
191 | *.[Cc]ache
192 | # but keep track of directories ending in .cache
193 | !*.[Cc]ache/
194 |
195 | # Others
196 | ClientBin/
197 | ~$*
198 | *~
199 | *.dbmdl
200 | *.dbproj.schemaview
201 | *.jfm
202 | *.pfx
203 | *.publishsettings
204 | node_modules/
205 | orleans.codegen.cs
206 |
207 | # Since there are multiple workflows, uncomment next line to ignore bower_components
208 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
209 | #bower_components/
210 |
211 | # RIA/Silverlight projects
212 | Generated_Code/
213 |
214 | # Backup & report files from converting an old project file
215 | # to a newer Visual Studio version. Backup files are not needed,
216 | # because we have git ;-)
217 | _UpgradeReport_Files/
218 | Backup*/
219 | UpgradeLog*.XML
220 | UpgradeLog*.htm
221 |
222 | # SQL Server files
223 | *.mdf
224 | *.ldf
225 |
226 | # Business Intelligence projects
227 | *.rdl.data
228 | *.bim.layout
229 | *.bim_*.settings
230 |
231 | # Microsoft Fakes
232 | FakesAssemblies/
233 |
234 | # GhostDoc plugin setting file
235 | *.GhostDoc.xml
236 |
237 | # Node.js Tools for Visual Studio
238 | .ntvs_analysis.dat
239 |
240 | # Visual Studio 6 build log
241 | *.plg
242 |
243 | # Visual Studio 6 workspace options file
244 | *.opt
245 |
246 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
247 | *.vbw
248 |
249 | # Visual Studio LightSwitch build output
250 | **/*.HTMLClient/GeneratedArtifacts
251 | **/*.DesktopClient/GeneratedArtifacts
252 | **/*.DesktopClient/ModelManifest.xml
253 | **/*.Server/GeneratedArtifacts
254 | **/*.Server/ModelManifest.xml
255 | _Pvt_Extensions
256 |
257 | # Paket dependency manager
258 | .paket/paket.exe
259 | paket-files/
260 |
261 | # FAKE - F# Make
262 | .fake/
263 |
264 | # JetBrains Rider
265 | .idea/
266 | *.sln.iml
267 |
268 | # CodeRush
269 | .cr/
270 |
271 | # Python Tools for Visual Studio (PTVS)
272 | __pycache__/
273 | *.pyc
274 |
275 | # Cake - Uncomment if you are using it
276 | # tools/
277 | tools/Cake.CoreCLR
278 | .vscode
279 | tools
280 | .dotnet
281 | Dockerfile
282 |
283 | # .env file contains default environment variables for docker
284 | .env
285 | .git/
--------------------------------------------------------------------------------
/client/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
2 | WORKDIR /app
3 | COPY . ./
4 | RUN dotnet publish -c Release -o output
5 | FROM nginx:latest
6 | WORKDIR /var/www/web
7 | COPY --from=build /app/output/wwwroot .
8 | COPY nginx.conf /etc/nginx/nginx.conf
9 | EXPOSE 8080
--------------------------------------------------------------------------------
/client/StudentManagement.Client.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.8.34601.278
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebUI", "src\WebUI\WebUI.csproj", "{3C17E96B-0D83-482B-9984-51A59476A4EE}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{79759013-F62E-4185-A4E0-341B8AD89F3A}"
9 | ProjectSection(SolutionItems) = preProject
10 | .dockerignore = .dockerignore
11 | Dockerfile = Dockerfile
12 | nginx.conf = nginx.conf
13 | EndProjectSection
14 | EndProject
15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{EAE8E7F3-6FB3-4E58-8F35-E5689497CE42}"
16 | EndProject
17 | Global
18 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
19 | Debug|Any CPU = Debug|Any CPU
20 | Release|Any CPU = Release|Any CPU
21 | EndGlobalSection
22 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
23 | {3C17E96B-0D83-482B-9984-51A59476A4EE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
24 | {3C17E96B-0D83-482B-9984-51A59476A4EE}.Debug|Any CPU.Build.0 = Debug|Any CPU
25 | {3C17E96B-0D83-482B-9984-51A59476A4EE}.Release|Any CPU.ActiveCfg = Release|Any CPU
26 | {3C17E96B-0D83-482B-9984-51A59476A4EE}.Release|Any CPU.Build.0 = Release|Any CPU
27 | EndGlobalSection
28 | GlobalSection(SolutionProperties) = preSolution
29 | HideSolutionNode = FALSE
30 | EndGlobalSection
31 | GlobalSection(NestedProjects) = preSolution
32 | {3C17E96B-0D83-482B-9984-51A59476A4EE} = {EAE8E7F3-6FB3-4E58-8F35-E5689497CE42}
33 | EndGlobalSection
34 | GlobalSection(ExtensibilityGlobals) = postSolution
35 | SolutionGuid = {184A2DD8-AB3F-4E1A-A85E-B2AB99253868}
36 | EndGlobalSection
37 | EndGlobal
38 |
--------------------------------------------------------------------------------
/client/nginx.conf:
--------------------------------------------------------------------------------
1 | events { }
2 | http {
3 | include mime.types;
4 | types {
5 | application/wasm wasm;
6 | }
7 | server {
8 | listen 8080;
9 | index index.html;
10 | location / {
11 | root /var/www/web;
12 | try_files $uri $uri/ /index.html =404;
13 | }
14 | }
15 | }
--------------------------------------------------------------------------------
/client/src/WebUI/App.razor:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Not found
8 |
9 | Sorry, there's nothing at this address.
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/client/src/WebUI/Dtos/StudentDto.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 |
3 | namespace WebUI.Dtos;
4 |
5 | public record StudentDto
6 | {
7 | public long Id { get; set; }
8 |
9 | [Required(ErrorMessage = "First name is required")]
10 | [RegularExpression(@"^[a-zA-Z0-9 ]+$", ErrorMessage = "Special characters are not accepted")]
11 | [MinLength(2, ErrorMessage = "First name must be at least 2 characters")]
12 | [MaxLength(50, ErrorMessage = "First name must be at most 50 characters")]
13 | public string FirstName { get; set; } = null!;
14 |
15 | [Required(ErrorMessage = "Last name is required")]
16 | [RegularExpression(@"^[a-zA-Z0-9 ]+$", ErrorMessage = "Special characters are not accepted")]
17 | [MinLength(2, ErrorMessage = "Last name must be at least 2 characters")]
18 | [MaxLength(50, ErrorMessage = "Last name must be at most 50 characters")]
19 | public string LastName { get; set; } = null!;
20 |
21 | [Required(ErrorMessage = "Email is required")]
22 | public Email Email { get; set; } = null!;
23 |
24 | [Required(ErrorMessage = "Birth date is required")]
25 | public DateOnly BirthDate { get; set; }
26 |
27 | [Required(ErrorMessage = "GitHub username is required")]
28 | [RegularExpression(@"^[a-zA-Z0-9\-]+$", ErrorMessage = "Special characters are not accepted")]
29 | [MinLength(3, ErrorMessage = "GitHub username must be at least 3 characters")]
30 | [MaxLength(40, ErrorMessage = "GitHub username must be at most 40 characters")]
31 | public string GitHubUsername { get; set; } = null!;
32 | }
33 |
34 | public record Email
35 | {
36 | public string Value { get; set; } = null!;
37 | }
--------------------------------------------------------------------------------
/client/src/WebUI/Layout/MainLayout.razor:
--------------------------------------------------------------------------------
1 | @inherits LayoutComponentBase
2 |
118 |
119 | @code {
120 | #region Toggle Mini Sidebar
121 | private bool isSidebarMinimized = false;
122 | private string? MinSidebarCssClass => isSidebarMinimized ? "min-sidebar" : null;
123 | private void ToggleSidebar()
124 | {
125 | isSidebarMinimized = !isSidebarMinimized;
126 | }
127 | #endregion
128 |
129 | #region Toggle Mobile Navbar
130 | private bool isMobileNavbarVisible = false;
131 | private string? MobileNavbarCssClass => isMobileNavbarVisible ? "display" : null;
132 | private void ToggleMobileNavbar()
133 | {
134 | isMobileNavbarVisible = !isMobileNavbarVisible;
135 | }
136 | #endregion
137 | }
--------------------------------------------------------------------------------
/client/src/WebUI/Layout/MainLayout.razor.css:
--------------------------------------------------------------------------------
1 | .page {
2 | position: relative;
3 | display: flex;
4 | flex-direction: column;
5 | }
6 |
7 | main {
8 | flex: 1;
9 | }
10 |
11 | .sidebar {
12 | background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%);
13 | }
14 |
15 | .top-row {
16 | background-color: #f7f7f7;
17 | border-bottom: 1px solid #d6d5d5;
18 | justify-content: flex-end;
19 | height: 3.5rem;
20 | display: flex;
21 | align-items: center;
22 | }
23 |
24 | .top-row ::deep a, .top-row ::deep .btn-link {
25 | white-space: nowrap;
26 | margin-left: 1.5rem;
27 | text-decoration: none;
28 | }
29 |
30 | .top-row ::deep a:hover, .top-row ::deep .btn-link:hover {
31 | text-decoration: underline;
32 | }
33 |
34 | .top-row ::deep a:first-child {
35 | overflow: hidden;
36 | text-overflow: ellipsis;
37 | }
38 |
39 | @media (max-width: 640.98px) {
40 | .top-row {
41 | justify-content: space-between;
42 | }
43 |
44 | .top-row ::deep a, .top-row ::deep .btn-link {
45 | margin-left: 0;
46 | }
47 | }
48 |
49 | @media (min-width: 641px) {
50 | .page {
51 | flex-direction: row;
52 | }
53 |
54 | .sidebar {
55 | width: 250px;
56 | height: 100vh;
57 | position: sticky;
58 | top: 0;
59 | }
60 |
61 | .top-row {
62 | position: sticky;
63 | top: 0;
64 | z-index: 1;
65 | }
66 |
67 | .top-row.auth ::deep a:first-child {
68 | flex: 1;
69 | text-align: right;
70 | width: 0;
71 | }
72 |
73 | .top-row, article {
74 | padding-left: 2rem !important;
75 | padding-right: 1.5rem !important;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/client/src/WebUI/Pages/Home.razor:
--------------------------------------------------------------------------------
1 | @page "/"
2 |
3 | Home
4 |
--------------------------------------------------------------------------------
/client/src/WebUI/Pages/Students/Add.razor:
--------------------------------------------------------------------------------
1 | @page "/students/add"
2 | @inject NavigationManager NavigationManager
3 | @inject StudentService studentService
4 | @inject IJSRuntime JsRuntime
5 |
6 | Add Student
7 |
8 |
9 |
11 |
12 |

13 |
14 |
15 |
Add Student
16 |
17 |
18 |
19 |
76 |
77 |
78 | @code {
79 | private DateOnly minBirthDate = DateOnly.FromDateTime(DateTime.Now).AddYears(-140);
80 | private DateOnly maxBirthDate = DateOnly.FromDateTime(DateTime.Now).AddYears(-18);
81 | private StudentDto newStudent = new StudentDto();
82 | private List ErrorList = new List();
83 |
84 | protected override async Task OnInitializedAsync()
85 | {
86 | newStudent.BirthDate = maxBirthDate;
87 | newStudent.Email = new Email();
88 | }
89 |
90 | private async Task AddStudent()
91 | {
92 | var response = await studentService.Add(newStudent);
93 | if (response.IsSuccessStatusCode)
94 | {
95 | await JsRuntime.InvokeVoidAsync("alert", "Student added!");
96 | NavigationManager.NavigateTo("/students");
97 | }
98 | else
99 | {
100 | await JsRuntime.InvokeVoidAsync("alert", "Something went wrong!");
101 | }
102 | }
103 | }
--------------------------------------------------------------------------------
/client/src/WebUI/Pages/Students/Delete.razor:
--------------------------------------------------------------------------------
1 | @page "/students/delete/{studentId}"
2 | @inject NavigationManager NavigationManager
3 | @inject StudentService studentService
4 | @inject IJSRuntime JsRuntime
5 |
6 | Delete Student
7 | @if (student is not null)
8 | {
9 |
10 |
11 |
12 |
14 |
15 |

16 |
17 |
18 |
Delete Student
19 |
20 |
21 |
22 |
83 |
84 |
85 |
86 | }
87 | else
88 | {
89 |
90 | }
91 |
92 | @code {
93 | private StudentDto student;
94 |
95 | [Parameter]
96 | public string studentId { get; set; }
97 |
98 | protected override async Task OnInitializedAsync()
99 | {
100 | student = await studentService.Get(studentId);
101 | }
102 |
103 | private async void DeleteStudent()
104 | {
105 | var response = await studentService.Delete(studentId);
106 | if (response.IsSuccessStatusCode)
107 | {
108 | await JsRuntime.InvokeVoidAsync("alert", "Student deleted!");
109 | NavigationManager.NavigateTo("/students");
110 | }
111 | else
112 | {
113 | await JsRuntime.InvokeVoidAsync("alert", "Something went wrong!");
114 | }
115 | }
116 | }
--------------------------------------------------------------------------------
/client/src/WebUI/Pages/Students/Edit.razor:
--------------------------------------------------------------------------------
1 | @page "/students/edit/{studentId}"
2 | @inject NavigationManager NavigationManager
3 | @inject StudentService studentService
4 | @inject IJSRuntime JsRuntime
5 |
6 | Edit Student
7 | @if (student is not null)
8 | {
9 |
10 |
11 |
13 |
14 |

15 |
16 |
17 |
Edit Student
18 |
19 |
20 |
21 |
79 |
80 | }
81 | else
82 | {
83 |
84 | }
85 |
86 | @code {
87 | private DateOnly minBirthDate = DateOnly.FromDateTime(DateTime.Now).AddYears(-140);
88 | private DateOnly maxBirthDate = DateOnly.FromDateTime(DateTime.Now).AddYears(-18);
89 |
90 | private StudentDto student;
91 |
92 | [Parameter]
93 | public string studentId { get; set; }
94 |
95 | protected override async Task OnInitializedAsync()
96 | {
97 | student = await studentService.Get(studentId);
98 | }
99 |
100 | private async void EditStudent()
101 | {
102 | var response = await studentService.Edit(studentId, student);
103 | if (response.IsSuccessStatusCode)
104 | {
105 | await JsRuntime.InvokeVoidAsync("alert", "Student edited!");
106 | NavigationManager.NavigateTo("/students");
107 | }
108 | else
109 | {
110 | await JsRuntime.InvokeVoidAsync("alert", "Something went wrong!");
111 | }
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/client/src/WebUI/Pages/Students/Index.razor:
--------------------------------------------------------------------------------
1 | @page "/students"
2 | @inject StudentService studentService
3 |
4 | Students
5 | @if (studentList is not null)
6 | {
7 |
8 |
9 |
10 |
Students
11 |
12 | @studentList.Count()
13 | total
14 |
15 |
16 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
47 |
48 |
49 | @foreach (var item in studentList)
50 | {
51 |
52 |
53 |
54 | @item.FirstName
55 |
56 |
57 | @item.LastName
58 |
59 |
60 |
61 | @item.Email.Value
62 |
63 |
64 |
65 | @item.BirthDate
66 |
67 |
68 |
82 |
83 |
84 | }
85 |
86 |
87 | }
88 | else
89 | {
90 |
91 | }
92 |
93 | @code {
94 | private StudentDto[] studentList;
95 |
96 | protected override async Task OnInitializedAsync()
97 | {
98 | studentList = await studentService.GetAll();
99 | }
100 | }
--------------------------------------------------------------------------------
/client/src/WebUI/Pages/Students/View.razor:
--------------------------------------------------------------------------------
1 | @page "/students/view/{studentId}"
2 | @inject NavigationManager NavigationManager
3 | @inject StudentService studentService
4 |
5 | View Student
6 | @if (student is not null)
7 | {
8 |
9 |
10 |
11 |
13 |
14 |

15 |
16 |
17 |
View Student
18 |
19 |
28 |
29 |
30 |
31 |
82 |
83 |
84 |
85 | }
86 | else
87 | {
88 |
89 | }
90 |
91 | @code {
92 | private StudentDto student;
93 |
94 | [Parameter]
95 | public string studentId { get; set; }
96 |
97 | protected override async Task OnInitializedAsync()
98 | {
99 | student = await studentService.Get(studentId);
100 | }
101 | }
--------------------------------------------------------------------------------
/client/src/WebUI/Program.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
2 | using Microsoft.AspNetCore.Components.Web;
3 | using WebUI.Services;
4 | using WebUI;
5 |
6 | var builder = WebAssemblyHostBuilder.CreateDefault(args);
7 | builder.RootComponents.Add("#app");
8 | builder.RootComponents.Add("head::after");
9 |
10 | builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
11 |
12 | builder.Services.AddScoped();
13 | builder.Services.AddBlazorBootstrap();
14 |
15 | await builder.Build().RunAsync();
16 |
--------------------------------------------------------------------------------
/client/src/WebUI/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:7565",
8 | "sslPort": 44347
9 | }
10 | },
11 | "profiles": {
12 | "http": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
17 | "applicationUrl": "http://localhost:5089",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "https": {
23 | "commandName": "Project",
24 | "dotnetRunMessages": true,
25 | "launchBrowser": true,
26 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
27 | "applicationUrl": "https://localhost:7147;http://localhost:5089",
28 | "environmentVariables": {
29 | "ASPNETCORE_ENVIRONMENT": "Development"
30 | }
31 | },
32 | "IIS Express": {
33 | "commandName": "IISExpress",
34 | "launchBrowser": true,
35 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
36 | "environmentVariables": {
37 | "ASPNETCORE_ENVIRONMENT": "Development"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/client/src/WebUI/Services/ApiUrls.cs:
--------------------------------------------------------------------------------
1 | namespace WebUI.Services;
2 |
3 | public static class ApiUrls
4 | {
5 | // URL for API app running inside the Docker container
6 | public const string BaseURL = "http://localhost:5000";
7 | }
8 |
--------------------------------------------------------------------------------
/client/src/WebUI/Services/StudentService.cs:
--------------------------------------------------------------------------------
1 | using System.Net.Http.Json;
2 | using WebUI.Dtos;
3 |
4 | namespace WebUI.Services;
5 |
6 | public class StudentService
7 | {
8 | private const string URL = ApiUrls.BaseURL + "/api/students";
9 | private readonly HttpClient _httpClient;
10 |
11 | public StudentService(HttpClient httpClient)
12 | {
13 | _httpClient = httpClient;
14 | }
15 |
16 | public async Task GetAll()
17 | {
18 | return await _httpClient.GetFromJsonAsync(URL);
19 | }
20 |
21 | public async Task Get(string studentId)
22 | {
23 | return await _httpClient.GetFromJsonAsync($"{URL}/{studentId}");
24 | }
25 |
26 | public async Task Add(StudentDto student)
27 | {
28 | return await _httpClient.PostAsJsonAsync(URL, student);
29 | }
30 |
31 | public async Task Edit(string studentId, StudentDto StudentDto)
32 | {
33 | return await _httpClient.PutAsJsonAsync($"{URL}/{studentId}", StudentDto);
34 | }
35 |
36 | public async Task Delete(string studentId)
37 | {
38 | return await _httpClient.DeleteAsync($"{URL}/{studentId}");
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/client/src/WebUI/WebUI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/client/src/WebUI/_Imports.razor:
--------------------------------------------------------------------------------
1 | @using Microsoft.AspNetCore.Components.Web.Virtualization
2 | @using Microsoft.AspNetCore.Components.WebAssembly.Http
3 | @using Microsoft.AspNetCore.Components.Routing
4 | @using Microsoft.AspNetCore.Components.Forms
5 | @using Microsoft.AspNetCore.Components.Web
6 | @using System.Net.Http.Json
7 | @using Microsoft.JSInterop
8 | @using BlazorBootstrap;
9 | @using System.Net.Http
10 | @using WebUI.Services;
11 | @using WebUI.Layout;
12 | @using WebUI.Dtos;
13 | @using WebUI
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/css/app.css:
--------------------------------------------------------------------------------
1 | .valid.modified:not([type=checkbox]) {
2 | outline: 1px solid #26b050;
3 | }
4 |
5 | .invalid {
6 | outline: 1px solid red;
7 | }
8 |
9 | .validation-message {
10 | color: red;
11 | }
12 |
13 | #blazor-error-ui {
14 | background: lightyellow;
15 | bottom: 0;
16 | box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2);
17 | display: none;
18 | left: 0;
19 | padding: 0.6rem 1.25rem 0.7rem 1.25rem;
20 | position: fixed;
21 | width: 100%;
22 | z-index: 1000;
23 | }
24 |
25 | #blazor-error-ui .dismiss {
26 | cursor: pointer;
27 | position: absolute;
28 | right: 0.75rem;
29 | top: 0.5rem;
30 | }
31 |
32 | .blazor-error-boundary {
33 | background: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTYiIGhlaWdodD0iNDkiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIG92ZXJmbG93PSJoaWRkZW4iPjxkZWZzPjxjbGlwUGF0aCBpZD0iY2xpcDAiPjxyZWN0IHg9IjIzNSIgeT0iNTEiIHdpZHRoPSI1NiIgaGVpZ2h0PSI0OSIvPjwvY2xpcFBhdGg+PC9kZWZzPjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMCkiIHRyYW5zZm9ybT0idHJhbnNsYXRlKC0yMzUgLTUxKSI+PHBhdGggZD0iTTI2My41MDYgNTFDMjY0LjcxNyA1MSAyNjUuODEzIDUxLjQ4MzcgMjY2LjYwNiA1Mi4yNjU4TDI2Ny4wNTIgNTIuNzk4NyAyNjcuNTM5IDUzLjYyODMgMjkwLjE4NSA5Mi4xODMxIDI5MC41NDUgOTIuNzk1IDI5MC42NTYgOTIuOTk2QzI5MC44NzcgOTMuNTEzIDI5MSA5NC4wODE1IDI5MSA5NC42NzgyIDI5MSA5Ny4wNjUxIDI4OS4wMzggOTkgMjg2LjYxNyA5OUwyNDAuMzgzIDk5QzIzNy45NjMgOTkgMjM2IDk3LjA2NTEgMjM2IDk0LjY3ODIgMjM2IDk0LjM3OTkgMjM2LjAzMSA5NC4wODg2IDIzNi4wODkgOTMuODA3MkwyMzYuMzM4IDkzLjAxNjIgMjM2Ljg1OCA5Mi4xMzE0IDI1OS40NzMgNTMuNjI5NCAyNTkuOTYxIDUyLjc5ODUgMjYwLjQwNyA1Mi4yNjU4QzI2MS4yIDUxLjQ4MzcgMjYyLjI5NiA1MSAyNjMuNTA2IDUxWk0yNjMuNTg2IDY2LjAxODNDMjYwLjczNyA2Ni4wMTgzIDI1OS4zMTMgNjcuMTI0NSAyNTkuMzEzIDY5LjMzNyAyNTkuMzEzIDY5LjYxMDIgMjU5LjMzMiA2OS44NjA4IDI1OS4zNzEgNzAuMDg4N0wyNjEuNzk1IDg0LjAxNjEgMjY1LjM4IDg0LjAxNjEgMjY3LjgyMSA2OS43NDc1QzI2Ny44NiA2OS43MzA5IDI2Ny44NzkgNjkuNTg3NyAyNjcuODc5IDY5LjMxNzkgMjY3Ljg3OSA2Ny4xMTgyIDI2Ni40NDggNjYuMDE4MyAyNjMuNTg2IDY2LjAxODNaTTI2My41NzYgODYuMDU0N0MyNjEuMDQ5IDg2LjA1NDcgMjU5Ljc4NiA4Ny4zMDA1IDI1OS43ODYgODkuNzkyMSAyNTkuNzg2IDkyLjI4MzcgMjYxLjA0OSA5My41Mjk1IDI2My41NzYgOTMuNTI5NSAyNjYuMTE2IDkzLjUyOTUgMjY3LjM4NyA5Mi4yODM3IDI2Ny4zODcgODkuNzkyMSAyNjcuMzg3IDg3LjMwMDUgMjY2LjExNiA4Ni4wNTQ3IDI2My41NzYgODYuMDU0N1oiIGZpbGw9IiNGRkU1MDAiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvZz48L3N2Zz4=) no-repeat 1rem/1.8rem, #b32121;
34 | padding: 1rem 1rem 1rem 3.7rem;
35 | color: white;
36 | }
37 |
38 | .blazor-error-boundary::after {
39 | content: "An error has occurred."
40 | }
41 |
42 | .loading-progress {
43 | position: relative;
44 | display: block;
45 | width: 8rem;
46 | height: 8rem;
47 | margin: 20vh auto 1rem auto;
48 | }
49 |
50 | .loading-progress circle {
51 | fill: none;
52 | stroke: #e0e0e0;
53 | stroke-width: 0.6rem;
54 | transform-origin: 50% 50%;
55 | transform: rotate(-90deg);
56 | }
57 |
58 | .loading-progress circle:last-child {
59 | stroke: #1b6ec2;
60 | stroke-dasharray: calc(3.141 * var(--blazor-load-percentage, 0%) * 0.8), 500%;
61 | transition: stroke-dasharray 0.05s ease-in-out;
62 | }
63 |
64 | .loading-progress-text {
65 | position: absolute;
66 | text-align: center;
67 | font-weight: bold;
68 | inset: calc(20vh + 3.25rem) 0 auto 0.2rem;
69 | }
70 |
71 | .loading-progress-text:after {
72 | content: var(--blazor-load-percentage-text, "Loading");
73 | }
74 |
75 | code {
76 | color: #c02d76;
77 | }
78 |
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/css/navbar.css:
--------------------------------------------------------------------------------
1 | /******************** Header ********************/
2 | .header {
3 | padding-right: 25px;
4 | background-color: var(--theme-color);
5 | height: 60px;
6 | }
7 |
8 | .header .nav-item {
9 | height: fit-content;
10 | }
11 |
12 | .top-navbar {
13 | float: right;
14 | }
15 |
16 | .profile-photo {
17 | width: 2rem;
18 | border-radius: 50%;
19 | margin-right: 5px;
20 | }
21 |
22 | .wraper {
23 | display: flex;
24 | align-items: center;
25 | justify-content: end;
26 | height: 60px;
27 | }
28 |
29 | .wraper-div {
30 | text-align: center;
31 | }
32 |
33 | .header .dropdown-menu {
34 | left: auto;
35 | right: 0;
36 | position: absolute;
37 | }
38 |
39 | .dropdown-menu li {
40 | padding: 3px 0;
41 | }
42 |
43 | .header .dropdown-menu .dropdown-icon {
44 | margin-right: 10px;
45 | }
46 |
47 | .header .dropdown-item:active {
48 | background-color: var(--active-gray);
49 | color: var(--text-color);
50 | }
51 |
52 | /******************** Sidebar ********************/
53 | .sidebar {
54 | height: 100%;
55 | width: 250px;
56 | position: fixed;
57 | top: 0;
58 | left: 0;
59 | background-color: var(--theme-color);
60 | padding: 10px 15px;
61 | margin: 0;
62 | z-index: 1030;
63 | }
64 |
65 | .sidebar ul {
66 | list-style: none;
67 | padding: 0;
68 | }
69 |
70 | .sidebar li {
71 | margin-bottom: .5rem;
72 | }
73 |
74 | /* toggle-sidebar */
75 | .toggle-sidebar-wrapper {
76 | display: flex;
77 | align-items: center;
78 | }
79 |
80 | .toggle-sidebar {
81 | text-align: center;
82 | justify-content: center;
83 | align-items: center;
84 | padding: 10px;
85 | height: 40px;
86 | width: 40px;
87 | border-radius: 50%;
88 | display: flex;
89 | cursor: pointer;
90 | }
91 |
92 | .toggle-sidebar:hover {
93 | background-color: var(--dark-gray);
94 | }
95 |
96 | .toggle-sidebar:active {
97 | background-color: var(--active-color);
98 | }
99 |
100 | .sidebar li.sidebar-title {
101 | margin-bottom: 30px;
102 | }
103 |
104 | .sidebar-title span {
105 | margin: 0 5px;
106 | }
107 |
108 | .sidebar-icon {
109 | width: 1rem;
110 | height: 1rem;
111 | margin: auto 0;
112 | }
113 |
114 | .nav-link-inner {
115 | display: inline-flex;
116 | padding: 8px 12px;
117 | }
118 |
119 | .nav-link-inner span {
120 | padding: 0 1rem;
121 | }
122 |
123 | .sidebar .nav-item:hover,
124 | .mobile-navbar .nav-item:hover {
125 | background-color: var(--dark-gray);
126 | border-top-right-radius: 50px;
127 | border-bottom-right-radius: 50px;
128 | width: -webkit-fill-available;
129 | width: -moz-available;
130 | margin-left: -2rem;
131 | padding-left: 2rem;
132 | }
133 |
134 |
135 | .sidebar .nav-item:active,
136 | .mobile-navbar .nav-item:active {
137 | background-color: var(--active-color);
138 | border-top-right-radius: 50px;
139 | border-bottom-right-radius: 50px;
140 | width: -webkit-fill-available;
141 | width: -moz-available;
142 | margin-left: -2rem;
143 | padding-left: 2rem;
144 | }
145 |
146 | /*** active item ***/
147 | .sidebar .active,
148 | .sidebar .active:hover,
149 | .mobile-navbar .nav-item .active {
150 | background-color: var(--active-color);
151 | border-top-right-radius: 50px;
152 | border-bottom-right-radius: 50px;
153 | width: -webkit-fill-available;
154 | width: -moz-available;
155 | font-weight: 600;
156 | margin-left: -2rem;
157 | padding-left: 2rem;
158 | }
159 | .sidebar .nav-link .active {
160 | font-weight: 600;
161 | color: var(--text-color);
162 | }
163 |
164 | .nav-link {
165 | color: var(--text-color);
166 | padding: 0;
167 | display: flex;
168 | align-items: center;
169 | height: 40px;
170 | }
171 |
172 | .nav-link:hover {
173 | color: black;
174 | }
175 |
176 | .nav-link:focus {
177 | color: var(--text-color);
178 | }
179 |
180 | .desktop-navbar {
181 | display: block;
182 | }
183 |
184 | .mobile-navbar {
185 | display: none;
186 | }
187 |
188 | @media (max-width: 768px) {
189 | .desktop-navbar {
190 | display: none;
191 | }
192 |
193 | .mobile-navbar {
194 | display: block;
195 | padding-bottom: 65px;
196 | }
197 |
198 | .mobile-navbar .navbar-top {
199 | background-color: var(--theme-color);
200 | box-shadow: 0 0px 5px var(--bg-color);
201 | height: 55px;
202 | display: flex;
203 | align-items: center;
204 | width: 100%;
205 | position: fixed;
206 | padding: 0;
207 | margin: 0;
208 | z-index: 9;
209 | }
210 |
211 | .mobile-navbar .navbar-icon {
212 | margin: auto 29px;
213 | cursor: pointer;
214 | width: 1.3rem;
215 | }
216 |
217 | .mobile-navbar .navbar-content {
218 | height: 100vh;
219 | width: 70vw;
220 | position: fixed;
221 | z-index: 999;
222 | display: none;
223 | background-color: var(--theme-color);
224 | box-shadow: 0 0px 5px var(--gray);
225 | padding: 2.5rem 1.8rem;
226 | animation-duration: 0.2s;
227 | animation-timing-function: ease-in-out;
228 | animation-fill-mode: forwards;
229 | overflow-y: scroll;
230 | }
231 |
232 | .mobile-navbar .navbar-background {
233 | height: 100vh;
234 | width: 100vw;
235 | position: fixed;
236 | display: none;
237 | z-index: 99;
238 | opacity: 0.2;
239 | background-color: var(--gray);
240 | }
241 |
242 | .mobile-navbar .navbar-content.display {
243 | display: block;
244 | animation-name: slideIn;
245 | }
246 |
247 | @keyframes slideIn {
248 | 0% {
249 | transform: translateX(-100%);
250 | }
251 |
252 | 100% {
253 | transform: translateX(0);
254 | }
255 | }
256 |
257 | .mobile-navbar .navbar-background.display {
258 | display: block;
259 | }
260 |
261 | .mobile-navbar .nav-item {
262 | margin-bottom: 10px;
263 | }
264 |
265 | .mobile-navbar .line {
266 | margin: 0 0 8px 0;
267 | }
268 |
269 | .mobile-navbar .profile-container {
270 | display: block;
271 | padding: 0 1rem;
272 | }
273 |
274 | .mobile-navbar .profile-container span {
275 | padding: 5px;
276 | }
277 | }
278 |
279 | .noselect {
280 | -webkit-touch-callout: none;
281 | /* iOS Safari */
282 | -webkit-user-select: none;
283 | /* Safari */
284 | -khtml-user-select: none;
285 | /* Konqueror HTML */
286 | -moz-user-select: none;
287 | /* Old versions of Firefox */
288 | -ms-user-select: none;
289 | /* Internet Explorer/Edge */
290 | user-select: none;
291 | /* Non-prefixed version, currently
292 | supported by Chrome, Edge, Opera and Firefox */
293 | }
294 |
295 |
296 | /******************** min-sidebar ********************/
297 | .min-sidebar .content-container {
298 | margin-left: 90px;
299 | }
300 |
301 | .min-sidebar .sidebar {
302 | width: 70px;
303 | display: flex;
304 | justify-content: center;
305 | align-content: center;
306 | }
307 |
308 | .min-sidebar .sidebar span {
309 | display: none;
310 | }
311 |
312 | .min-sidebar .sidebar li {
313 | justify-content: center;
314 | width: 40px;
315 | }
316 |
317 | .min-sidebar .sidebar .nav-item:hover {
318 | border-radius: 50%;
319 | margin-left: 0;
320 | padding-left: 0;
321 | }
322 |
323 |
324 | .min-sidebar .sidebar .nav-item:active {
325 | border-radius: 50%;
326 | margin-left: 0;
327 | padding-left: 0;
328 | }
329 |
330 | /*** active item in min sidebar ***/
331 | .min-sidebar .sidebar .active,
332 | .min-sidebar .sidebar .active:hover {
333 | border-radius: 50%;
334 | margin-left: 0;
335 | padding-left: 0;
336 | }
337 |
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/css/style.css:
--------------------------------------------------------------------------------
1 | :root {
2 | /******************** Colors ********************/
3 | --bg-color: #1C2128;
4 | --theme-color: #22272E;
5 | --text-color: #f2f5fc;
6 | --active-color: #444C56;
7 | --gray: #68717c;
8 | --btn-cancel-color: #393E46;
9 | --light-gray: #DEDCDC;
10 | --dark-gray: #333a42;
11 | --active-gray: #e2e2e6;
12 | --green: #29BC9C;
13 | --dark-green: #26A88C;
14 | --red: #DB575A;
15 | --dark-red: #CB5153;
16 | /* Bootstrap colors */
17 | --bs-body-color: #f2f5fc;
18 | --bs-body-bg: #22272E;
19 | --bs-tertiary-bg: #333a42;
20 | }
21 |
22 | /******************** Base styles ********************/
23 | html {
24 | scroll-behavior: smooth;
25 | }
26 |
27 | span,
28 | p,
29 | label,
30 | h1,
31 | h2,
32 | h3,
33 | h4,
34 | h5,
35 | h6 {
36 | color: var(--text-color);
37 | padding: 0;
38 | margin: 0;
39 | }
40 |
41 | body {
42 | background-color: var(--bg-color);
43 | margin: 0;
44 | padding: 0;
45 | box-sizing: border-box;
46 | position: relative;
47 | overflow-x: hidden;
48 | overflow-y: scroll;
49 | line-height: 1.5;
50 | font-weight: 400;
51 | }
52 |
53 | .bold {
54 | font-weight: 600;
55 | }
56 |
57 | .secondary-text {
58 | color: var(--gray);
59 | font-size: 15px;
60 | }
61 |
62 | /******************** Content ********************/
63 | .content-container {
64 | background-color: var(--theme-color);
65 | padding: 20px;
66 | margin: 80px 20px 0 270px;
67 | height: fit-content;
68 | border-radius: 12px;
69 | }
70 |
71 | .content {
72 | margin: 15px 0 20px 0;
73 | }
74 |
75 | .error-list {
76 | list-style: disc;
77 | margin: 0;
78 | }
79 |
80 | @media (max-width: 768px) {
81 | .content-container {
82 | background-color: var(--theme-color);
83 | padding: 10px;
84 | margin: 0 10px !important;
85 | }
86 |
87 | .content {
88 | margin: 0;
89 | }
90 | }
91 |
92 | .title-wrapper {
93 | display: flex;
94 | align-items: center;
95 | margin-right: 15px;
96 | }
97 |
98 | .back-to-list-wrapper {
99 | margin: 5px;
100 | }
101 |
102 | .back-to-list {
103 | display: flex;
104 | align-items: center;
105 | justify-content: center;
106 | cursor: pointer;
107 | height: 40px;
108 | width: 40px;
109 | border-radius: 50%;
110 | }
111 |
112 | .back-to-list:hover {
113 | background-color: var(--dark-gray);
114 | }
115 |
116 | .back-to-list:active {
117 | background-color: var(--active-color);
118 | }
119 |
120 | .back-icon {
121 | width: 1rem;
122 | height: 1rem;
123 | margin: auto 0;
124 | }
125 |
126 | .row {
127 | margin-bottom: 1rem;
128 | }
129 |
130 | .line {
131 | margin: 0;
132 | padding: 0;
133 | color: var(--active-color);
134 | }
135 |
136 | textarea {
137 | height: 86px;
138 | }
139 |
140 | ::placeholder {
141 | color: var(--light-gray) !important;
142 | opacity: 1; /* Firefox */
143 | }
144 |
145 | ::-ms-input-placeholder { /* Edge 12 -18 */
146 | color: var(--light-gray) !important;
147 | }
148 |
149 | textarea,
150 | input {
151 | color: var(--text-color);
152 | background-color: var(--bg-color);
153 | }
154 |
155 | .form-control {
156 | color: var(--text-color);
157 | background-color: var(--bg-color);
158 | }
159 |
160 | .form-control:active, .form-control:focus {
161 | color: var(--text-color);
162 | background-color: var(--active-color);
163 | }
164 |
165 | /*********** Buttons ***********/
166 | .content-container .button-container {
167 | display: flex;
168 | }
169 |
170 | .content-container .btn {
171 | border-radius: 40px;
172 | border: none;
173 | font-weight: 600;
174 | width: 100%;
175 | height: 2.4rem;
176 | width: 10rem;
177 | display: flex;
178 | text-align: center;
179 | align-items: center;
180 | justify-content: center;
181 | }
182 |
183 | .content-container .btn-save {
184 | background-color: var(--green);
185 | color: white;
186 | }
187 |
188 | .content-container .btn-save:hover,
189 | .content-container .btn-save:focus {
190 | background-color: var(--dark-green);
191 | color: white;
192 | }
193 |
194 | .content-container .btn-delete {
195 | background-color: var(--red);
196 | color: white;
197 | }
198 |
199 | .content-container .btn-delete:hover,
200 | .content-container .btn-delete:focus {
201 | background-color: var(--dark-red);
202 | color: white;
203 | }
204 |
205 |
206 | .content-container .btn-cancel {
207 | background-color: var(--btn-cancel-color);
208 | color: var(--text-color);
209 | margin-left: 5px;
210 | }
211 |
212 | .content-container .btn-cancel:hover,
213 | .content-container .btn-cancel:focus {
214 | background-color: var(--active-color);
215 | color: var(--text-color);
216 | }
217 |
218 | .content-container .btn-add {
219 | height: 2rem;
220 | width: 7rem;
221 | }
222 |
223 | /*********** list ***********/
224 | .list-header {
225 | padding: 0 15px 16px 15px;
226 | display: flex;
227 | }
228 |
229 | .list-header span {
230 | color: var(--gray);
231 | }
232 |
233 | .list-header .control-header {
234 | min-width: 120px;
235 | }
236 |
237 | .list-header .message-control-header {
238 | min-width: 75px;
239 | }
240 |
241 | .list-title {
242 | display: flex;
243 | text-align: center;
244 | align-items: flex-end;
245 | justify-content: end;
246 | }
247 |
248 | .right-button-wrapper,
249 | .view-wrapper .buttons-wrapper {
250 | margin-left: auto;
251 | margin-right: 0;
252 | }
253 |
254 | .parent-page-title {
255 | margin: 10px 15px;
256 | }
257 |
258 | .list-total {
259 | margin-left: 10px;
260 | }
261 |
262 | .list-total span {
263 | color: darkgray;
264 | }
265 |
266 | .list-total span:first-child {
267 | margin-right: 5px;
268 | }
269 |
270 | .add-icon {
271 | margin-right: 5px;
272 | }
273 |
274 | .add-text {
275 | color: white;
276 | }
277 |
278 | .list-item {
279 | padding: 25px 15px;
280 | display: flex;
281 | align-items: center;
282 | }
283 |
284 | .list-item .row,
285 | .list-header .row {
286 | width: 100%;
287 | display: flex;
288 | margin-bottom: 0;
289 | margin-right: 0;
290 | align-items: center;
291 | }
292 |
293 | .list-item .buttons-wrapper {
294 | display: flex;
295 | }
296 |
297 | .form-container {
298 | padding: 0 15px;
299 | }
300 |
301 | @media (max-width: 768px) {
302 | .form-container {
303 | padding: 10px;
304 | }
305 |
306 | .list-header {
307 | display: none;
308 | }
309 |
310 | .right-button-wrapper .add-text {
311 | display: none;
312 | }
313 |
314 | .content-container .btn-add {
315 | width: 2rem;
316 | }
317 |
318 | .add-icon {
319 | margin: 0;
320 | }
321 |
322 | .parent-page-title {
323 | margin: 10px;
324 | }
325 |
326 | .title-wrapper {
327 | margin-right: 10px;
328 | }
329 |
330 | .list-item {
331 | padding: 15px 10px;
332 | display: block;
333 | }
334 |
335 | .list-item .col-sm-12 {
336 | margin: 3px 0;
337 | }
338 |
339 | .buttons-wrapper {
340 | padding: 10px 0 0 0;
341 | width: fit-content;
342 | margin: 0 auto;
343 | }
344 |
345 | .back-to-list-wrapper {
346 | margin-left: 0px;
347 | }
348 |
349 | .view-wrapper .buttons-wrapper {
350 | padding: 0;
351 | }
352 | }
353 |
354 | .list-item:hover {
355 | box-shadow: 0 0px 5px var(--active-color);
356 | cursor: pointer;
357 | }
358 |
359 | .truncated {
360 | overflow: hidden;
361 | white-space: nowrap;
362 | text-overflow: ellipsis;
363 | max-width: 25ch;
364 | }
365 |
366 | /*********** list button ***********/
367 | .list-button {
368 | padding: 9px;
369 | border-radius: 50%;
370 | background-color: var(--theme-color);
371 | box-shadow: 0 0 4px var(--active-color);
372 | text-decoration: none;
373 | text-align: center;
374 | display: inline-flex;
375 | margin-left: 15px;
376 | }
377 |
378 | .buttons-wrapper .list-button:first-child {
379 | margin-left: 0px;
380 | }
381 |
382 | .list-button:hover {
383 | background-color: var(--active-color);
384 | }
385 |
386 | .list-button-icon {
387 | width: 12px;
388 | height: 12px;
389 | }
390 |
391 | .view-item-wrapper {
392 | display: grid;
393 | margin-bottom: 1rem;
394 | }
395 |
396 | .view-item-wrapper span {
397 | font-weight: 600;
398 | margin-bottom: 5px;
399 | }
400 |
401 | .view-profile-photo {
402 | width: 10rem;
403 | }
404 |
405 | .edit-photo-wrapper {
406 | display: flex;
407 | }
408 |
409 | .photo-buttons-wrapper {
410 | margin: 0 0 0 1rem;
411 | }
412 |
413 | .photo-buttons-wrapper .photo-btn {
414 | border: none;
415 | background-color: white;
416 | font-weight: 600;
417 | padding: 0;
418 | }
419 |
420 | .photo-buttons-wrapper .photo-btn.update-btn {
421 | color: var(--green);
422 | margin-right: 20px;
423 | }
424 |
425 | .photo-buttons-wrapper .photo-btn.update-btn:hover {
426 | color: var(--dark-green);
427 | }
428 |
429 | .photo-buttons-wrapper .photo-btn.remove-btn {
430 | color: var(--red);
431 | }
432 |
433 | .photo-buttons-wrapper .photo-btn.remove-btn:hover {
434 | color: var(--dark-red);
435 | }
436 |
437 | @media (max-width: 426px) {
438 |
439 | .list-button,
440 | .buttons-wrapper .list-button:first-child {
441 | margin: 0 20px;
442 | }
443 |
444 | .view-wrapper .buttons-wrapper .list-button {
445 | margin: 0 5px;
446 | }
447 |
448 | .edit-photo-wrapper {
449 | display: block;
450 | }
451 |
452 | .view-profile-photo {
453 | width: 100%;
454 | }
455 |
456 | .photo-buttons-wrapper {
457 | margin: 1rem 0 0 0;
458 | }
459 | }
460 |
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/add.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/back.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Blazor-Sample/bd88afb855ec77cd296ffcc79623b9e24779cdc1/client/src/WebUI/wwwroot/images/back.png
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/edit.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Blazor-Sample/bd88afb855ec77cd296ffcc79623b9e24779cdc1/client/src/WebUI/wwwroot/images/favicon.png
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/home.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/icon-192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Blazor-Sample/bd88afb855ec77cd296ffcc79623b9e24779cdc1/client/src/WebUI/wwwroot/images/icon-192.png
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/logout.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Blazor-Sample/bd88afb855ec77cd296ffcc79623b9e24779cdc1/client/src/WebUI/wwwroot/images/logout.png
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/navbar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/profile-photo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Blazor-Sample/bd88afb855ec77cd296ffcc79623b9e24779cdc1/client/src/WebUI/wwwroot/images/profile-photo.png
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/students.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Blazor-Sample/bd88afb855ec77cd296ffcc79623b9e24779cdc1/client/src/WebUI/wwwroot/images/students.png
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/images/view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Blazor-Sample/bd88afb855ec77cd296ffcc79623b9e24779cdc1/client/src/WebUI/wwwroot/images/view.png
--------------------------------------------------------------------------------
/client/src/WebUI/wwwroot/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Student Management
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
24 |
25 |
26 |
27 |
28 | An unhandled error has occurred.
29 |
Reload
30 |
🗙
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 |
3 | # ASP.NET Core Web API Application
4 | student_management_api:
5 | container_name: api_container
6 | ports:
7 | - "5000:5000"
8 | depends_on:
9 | - student_management_postgres_db
10 | build:
11 | context: ./server
12 | dockerfile: Dockerfile
13 | environment:
14 | - ConnectionStrings__DefaultConnection=Server=postgres_db_container;Port=5432;Database=StudentManagementDB;Username=postgres;Password=mysecretpassword;
15 | - ASPNETCORE_URLS=http://+:5000
16 | networks:
17 | - student_management_network
18 |
19 | # Blazor WebAssembly Application
20 | student_management_client:
21 | container_name: client_container
22 | ports:
23 | - "8080:8080"
24 | build:
25 | context: ./client
26 | dockerfile: Dockerfile
27 | networks:
28 | - student_management_network
29 |
30 | # PostgreSQL Database
31 | student_management_postgres_db:
32 | container_name: postgres_db_container
33 | image: postgres:latest
34 | environment:
35 | POSTGRES_USER: postgres
36 | POSTGRES_PASSWORD: mysecretpassword
37 | PGDATA: /data/postgres
38 | volumes:
39 | - student_management_postgres_data:/data/postgres
40 | ports:
41 | - "5433:5432"
42 | networks:
43 | - student_management_network
44 | restart: unless-stopped
45 |
46 | # PGAdmin User Interface
47 | student_management_pgadmin:
48 | container_name: pgadmin_container
49 | image: dpage/pgadmin4
50 | environment:
51 | PGADMIN_DEFAULT_EMAIL: admin@gmail.com
52 | PGADMIN_DEFAULT_PASSWORD: mysecretpassword
53 | PGADMIN_CONFIG_SERVER_MODE: 'False'
54 | volumes:
55 | - student_management_pgadmin_data:/var/lib/pgadmin
56 | ports:
57 | - "8000:80"
58 | networks:
59 | - student_management_network
60 | restart: unless-stopped
61 |
62 | networks:
63 | student_management_network:
64 | driver: bridge
65 |
66 | volumes:
67 | student_management_postgres_data:
68 | student_management_pgadmin_data:
--------------------------------------------------------------------------------
/server/.dockerignore:
--------------------------------------------------------------------------------
1 |
2 | # Created by https://www.gitignore.io/api/csharp
3 |
4 | ### Csharp ###
5 | ## Ignore Visual Studio temporary files, build results, and
6 | ## files generated by popular Visual Studio add-ons.
7 |
8 | # User-specific files
9 | *.suo
10 | *.user
11 | *.userosscache
12 | *.sln.docstates
13 |
14 | # User-specific files (MonoDevelop/Xamarin Studio)
15 | *.userprefs
16 |
17 | # Build results
18 | [Dd]ebug/
19 | [Dd]ebugPublic/
20 | [Rr]elease/
21 | [Rr]eleases/
22 | x64/
23 | x86/
24 | bld/
25 | [Bb]in/
26 | [Oo]bj/
27 | [Ll]og/
28 |
29 | # Visual Studio cache/options directory
30 | .vs/
31 | # Uncomment if you have tasks that create the project's static files in wwwroot
32 | #wwwroot/
33 |
34 | # MSTest test Results
35 | [Tt]est[Rr]esult*/
36 | [Bb]uild[Ll]og.*
37 |
38 | # NUNIT
39 | *.VisualState.xml
40 | TestResult.xml
41 |
42 | # Build Results of an ATL Project
43 | [Dd]ebugPS/
44 | [Rr]eleasePS/
45 | dlldata.c
46 |
47 | # DNX
48 | project.lock.json
49 | project.fragment.lock.json
50 | artifacts/
51 | Properties/launchSettings.json
52 |
53 | *_i.c
54 | *_p.c
55 | *_i.h
56 | *.ilk
57 | *.meta
58 | *.obj
59 | *.pch
60 | *.pdb
61 | *.pgc
62 | *.pgd
63 | *.rsp
64 | *.sbr
65 | *.tlb
66 | *.tli
67 | *.tlh
68 | *.tmp
69 | *.tmp_proj
70 | *.log
71 | *.vspscc
72 | *.vssscc
73 | .builds
74 | *.pidb
75 | *.svclog
76 | *.scc
77 |
78 | # Chutzpah Test files
79 | _Chutzpah*
80 |
81 | # Visual C++ cache files
82 | ipch/
83 | *.aps
84 | *.ncb
85 | *.opendb
86 | *.opensdf
87 | *.sdf
88 | *.cachefile
89 | *.VC.db
90 | *.VC.VC.opendb
91 |
92 | # Visual Studio profiler
93 | *.psess
94 | *.vsp
95 | *.vspx
96 | *.sap
97 |
98 | # TFS 2012 Local Workspace
99 | $tf/
100 |
101 | # Guidance Automation Toolkit
102 | *.gpState
103 |
104 | # ReSharper is a .NET coding add-in
105 | _ReSharper*/
106 | *.[Rr]e[Ss]harper
107 | *.DotSettings.user
108 |
109 | # JustCode is a .NET coding add-in
110 | .JustCode
111 |
112 | # TeamCity is a build add-in
113 | _TeamCity*
114 |
115 | # DotCover is a Code Coverage Tool
116 | *.dotCover
117 |
118 | # Visual Studio code coverage results
119 | *.coverage
120 | *.coveragexml
121 |
122 | # NCrunch
123 | _NCrunch_*
124 | .*crunch*.local.xml
125 | nCrunchTemp_*
126 |
127 | # MightyMoose
128 | *.mm.*
129 | AutoTest.Net/
130 |
131 | # Web workbench (sass)
132 | .sass-cache/
133 |
134 | # Installshield output folder
135 | [Ee]xpress/
136 |
137 | # DocProject is a documentation generator add-in
138 | DocProject/buildhelp/
139 | DocProject/Help/*.HxT
140 | DocProject/Help/*.HxC
141 | DocProject/Help/*.hhc
142 | DocProject/Help/*.hhk
143 | DocProject/Help/*.hhp
144 | DocProject/Help/Html2
145 | DocProject/Help/html
146 |
147 | # Click-Once directory
148 | publish/
149 |
150 | # Publish Web Output
151 | *.[Pp]ublish.xml
152 | *.azurePubxml
153 | # TODO: Comment the next line if you want to checkin your web deploy settings
154 | # but database connection strings (with potential passwords) will be unencrypted
155 | *.pubxml
156 | *.publishproj
157 |
158 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
159 | # checkin your Azure Web App publish settings, but sensitive information contained
160 | # in these scripts will be unencrypted
161 | PublishScripts/
162 |
163 | # NuGet Packages
164 | *.nupkg
165 | # The packages folder can be ignored because of Package Restore
166 | **/packages/*
167 | # except build/, which is used as an MSBuild target.
168 | !**/packages/build/
169 | # Uncomment if necessary however generally it will be regenerated when needed
170 | #!**/packages/repositories.config
171 | # NuGet v3's project.json files produces more ignoreable files
172 | *.nuget.props
173 | *.nuget.targets
174 |
175 | # Microsoft Azure Build Output
176 | csx/
177 | *.build.csdef
178 |
179 | # Microsoft Azure Emulator
180 | ecf/
181 | rcf/
182 |
183 | # Windows Store app package directories and files
184 | AppPackages/
185 | BundleArtifacts/
186 | Package.StoreAssociation.xml
187 | _pkginfo.txt
188 |
189 | # Visual Studio cache files
190 | # files ending in .cache can be ignored
191 | *.[Cc]ache
192 | # but keep track of directories ending in .cache
193 | !*.[Cc]ache/
194 |
195 | # Others
196 | ClientBin/
197 | ~$*
198 | *~
199 | *.dbmdl
200 | *.dbproj.schemaview
201 | *.jfm
202 | *.pfx
203 | *.publishsettings
204 | node_modules/
205 | orleans.codegen.cs
206 |
207 | # Since there are multiple workflows, uncomment next line to ignore bower_components
208 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
209 | #bower_components/
210 |
211 | # RIA/Silverlight projects
212 | Generated_Code/
213 |
214 | # Backup & report files from converting an old project file
215 | # to a newer Visual Studio version. Backup files are not needed,
216 | # because we have git ;-)
217 | _UpgradeReport_Files/
218 | Backup*/
219 | UpgradeLog*.XML
220 | UpgradeLog*.htm
221 |
222 | # SQL Server files
223 | *.mdf
224 | *.ldf
225 |
226 | # Business Intelligence projects
227 | *.rdl.data
228 | *.bim.layout
229 | *.bim_*.settings
230 |
231 | # Microsoft Fakes
232 | FakesAssemblies/
233 |
234 | # GhostDoc plugin setting file
235 | *.GhostDoc.xml
236 |
237 | # Node.js Tools for Visual Studio
238 | .ntvs_analysis.dat
239 |
240 | # Visual Studio 6 build log
241 | *.plg
242 |
243 | # Visual Studio 6 workspace options file
244 | *.opt
245 |
246 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
247 | *.vbw
248 |
249 | # Visual Studio LightSwitch build output
250 | **/*.HTMLClient/GeneratedArtifacts
251 | **/*.DesktopClient/GeneratedArtifacts
252 | **/*.DesktopClient/ModelManifest.xml
253 | **/*.Server/GeneratedArtifacts
254 | **/*.Server/ModelManifest.xml
255 | _Pvt_Extensions
256 |
257 | # Paket dependency manager
258 | .paket/paket.exe
259 | paket-files/
260 |
261 | # FAKE - F# Make
262 | .fake/
263 |
264 | # JetBrains Rider
265 | .idea/
266 | *.sln.iml
267 |
268 | # CodeRush
269 | .cr/
270 |
271 | # Python Tools for Visual Studio (PTVS)
272 | __pycache__/
273 | *.pyc
274 |
275 | # Cake - Uncomment if you are using it
276 | # tools/
277 | tools/Cake.CoreCLR
278 | .vscode
279 | tools
280 | .dotnet
281 | Dockerfile
282 |
283 | # .env file contains default environment variables for docker
284 | .env
285 | .git/
--------------------------------------------------------------------------------
/server/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS base
2 | WORKDIR /app
3 | EXPOSE 5000
4 |
5 | FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
6 | WORKDIR /src
7 | COPY ["src/WebAPI/WebAPI.csproj", "src/WebAPI/"]
8 | COPY ["src/Application/Application.csproj", "src/Application/"]
9 | COPY ["src/Domain/Domain.csproj", "src/Domain/"]
10 | COPY ["src/Infrastructure/Infrastructure.csproj", "src/Infrastructure/"]
11 | RUN dotnet restore "./src/WebAPI/WebAPI.csproj"
12 | COPY . .
13 | WORKDIR "/src/src/WebAPI"
14 | RUN dotnet build "WebAPI.csproj" -c Release -o /app/build
15 |
16 | FROM build AS publish
17 | RUN dotnet publish "WebAPI.csproj" -c Release -o /app/publish
18 |
19 | FROM base AS final
20 | WORKDIR /app
21 |
22 | COPY --from=publish /app/publish .
23 | ENTRYPOINT ["dotnet", "WebAPI.dll"]
--------------------------------------------------------------------------------
/server/StudentManagement.Server.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.0.31903.59
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6ED356A7-8B47-4613-AD01-C85CF28491BD}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{664D406C-2F83-48F0-BFC3-408D5CB53C65}"
9 | EndProject
10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{E2DA20AA-28D1-455C-BF50-C49A8F831633}"
11 | ProjectSection(SolutionItems) = preProject
12 | .dockerignore = .dockerignore
13 | Dockerfile = Dockerfile
14 | EndProjectSection
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebAPI", "src\WebApi\WebAPI.csproj", "{C28C5493-ED77-40AA-8E7F-78AA28633EDC}"
17 | EndProject
18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain.Tests.Unit", "tests\Domain.Tests.Unit\Domain.Tests.Unit.csproj", "{05FB5C23-10FA-41A7-A520-C15CD14A5375}"
19 | EndProject
20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application.Tests.Unit", "tests\Application.Tests.Unit\Application.Tests.Unit.csproj", "{74A83D1D-306A-46AE-8F49-7D80D6C480A7}"
21 | EndProject
22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application.Tests.Integration", "tests\Application.Tests.Integration\Application.Tests.Integration.csproj", "{DCBC54A2-5D16-465B-95B8-FCC7B33CC3CD}"
23 | EndProject
24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "src\Domain\Domain.csproj", "{9C28732F-A548-4EF0-A796-1063FDB3E479}"
25 | EndProject
26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "src\Application\Application.csproj", "{22A603E9-7A63-4235-9EA2-53525948045D}"
27 | EndProject
28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{B8F88D98-9933-47AA-806B-3606CCA177C3}"
29 | EndProject
30 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebAPI.Tests.Integration", "tests\WebAPI.Tests.Integration\WebAPI.Tests.Integration.csproj", "{F3C10C1F-A947-4A05-ADFE-517E3ADBF3D1}"
31 | EndProject
32 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebAPI.Tests.Acceptance", "tests\WebAPI.Tests.Acceptance\WebAPI.Tests.Acceptance.csproj", "{26D5CFC5-2368-4C94-822C-5A51AE178A91}"
33 | EndProject
34 | Global
35 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
36 | Debug|Any CPU = Debug|Any CPU
37 | Release|Any CPU = Release|Any CPU
38 | EndGlobalSection
39 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
40 | {C28C5493-ED77-40AA-8E7F-78AA28633EDC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
41 | {C28C5493-ED77-40AA-8E7F-78AA28633EDC}.Debug|Any CPU.Build.0 = Debug|Any CPU
42 | {C28C5493-ED77-40AA-8E7F-78AA28633EDC}.Release|Any CPU.ActiveCfg = Release|Any CPU
43 | {C28C5493-ED77-40AA-8E7F-78AA28633EDC}.Release|Any CPU.Build.0 = Release|Any CPU
44 | {05FB5C23-10FA-41A7-A520-C15CD14A5375}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
45 | {05FB5C23-10FA-41A7-A520-C15CD14A5375}.Debug|Any CPU.Build.0 = Debug|Any CPU
46 | {05FB5C23-10FA-41A7-A520-C15CD14A5375}.Release|Any CPU.ActiveCfg = Release|Any CPU
47 | {05FB5C23-10FA-41A7-A520-C15CD14A5375}.Release|Any CPU.Build.0 = Release|Any CPU
48 | {74A83D1D-306A-46AE-8F49-7D80D6C480A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
49 | {74A83D1D-306A-46AE-8F49-7D80D6C480A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
50 | {74A83D1D-306A-46AE-8F49-7D80D6C480A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
51 | {74A83D1D-306A-46AE-8F49-7D80D6C480A7}.Release|Any CPU.Build.0 = Release|Any CPU
52 | {DCBC54A2-5D16-465B-95B8-FCC7B33CC3CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
53 | {DCBC54A2-5D16-465B-95B8-FCC7B33CC3CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
54 | {DCBC54A2-5D16-465B-95B8-FCC7B33CC3CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
55 | {DCBC54A2-5D16-465B-95B8-FCC7B33CC3CD}.Release|Any CPU.Build.0 = Release|Any CPU
56 | {9C28732F-A548-4EF0-A796-1063FDB3E479}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
57 | {9C28732F-A548-4EF0-A796-1063FDB3E479}.Debug|Any CPU.Build.0 = Debug|Any CPU
58 | {9C28732F-A548-4EF0-A796-1063FDB3E479}.Release|Any CPU.ActiveCfg = Release|Any CPU
59 | {9C28732F-A548-4EF0-A796-1063FDB3E479}.Release|Any CPU.Build.0 = Release|Any CPU
60 | {22A603E9-7A63-4235-9EA2-53525948045D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
61 | {22A603E9-7A63-4235-9EA2-53525948045D}.Debug|Any CPU.Build.0 = Debug|Any CPU
62 | {22A603E9-7A63-4235-9EA2-53525948045D}.Release|Any CPU.ActiveCfg = Release|Any CPU
63 | {22A603E9-7A63-4235-9EA2-53525948045D}.Release|Any CPU.Build.0 = Release|Any CPU
64 | {B8F88D98-9933-47AA-806B-3606CCA177C3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
65 | {B8F88D98-9933-47AA-806B-3606CCA177C3}.Debug|Any CPU.Build.0 = Debug|Any CPU
66 | {B8F88D98-9933-47AA-806B-3606CCA177C3}.Release|Any CPU.ActiveCfg = Release|Any CPU
67 | {B8F88D98-9933-47AA-806B-3606CCA177C3}.Release|Any CPU.Build.0 = Release|Any CPU
68 | {F3C10C1F-A947-4A05-ADFE-517E3ADBF3D1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
69 | {F3C10C1F-A947-4A05-ADFE-517E3ADBF3D1}.Debug|Any CPU.Build.0 = Debug|Any CPU
70 | {F3C10C1F-A947-4A05-ADFE-517E3ADBF3D1}.Release|Any CPU.ActiveCfg = Release|Any CPU
71 | {F3C10C1F-A947-4A05-ADFE-517E3ADBF3D1}.Release|Any CPU.Build.0 = Release|Any CPU
72 | {26D5CFC5-2368-4C94-822C-5A51AE178A91}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
73 | {26D5CFC5-2368-4C94-822C-5A51AE178A91}.Debug|Any CPU.Build.0 = Debug|Any CPU
74 | {26D5CFC5-2368-4C94-822C-5A51AE178A91}.Release|Any CPU.ActiveCfg = Release|Any CPU
75 | {26D5CFC5-2368-4C94-822C-5A51AE178A91}.Release|Any CPU.Build.0 = Release|Any CPU
76 | EndGlobalSection
77 | GlobalSection(SolutionProperties) = preSolution
78 | HideSolutionNode = FALSE
79 | EndGlobalSection
80 | GlobalSection(NestedProjects) = preSolution
81 | {C28C5493-ED77-40AA-8E7F-78AA28633EDC} = {6ED356A7-8B47-4613-AD01-C85CF28491BD}
82 | {05FB5C23-10FA-41A7-A520-C15CD14A5375} = {664D406C-2F83-48F0-BFC3-408D5CB53C65}
83 | {74A83D1D-306A-46AE-8F49-7D80D6C480A7} = {664D406C-2F83-48F0-BFC3-408D5CB53C65}
84 | {DCBC54A2-5D16-465B-95B8-FCC7B33CC3CD} = {664D406C-2F83-48F0-BFC3-408D5CB53C65}
85 | {9C28732F-A548-4EF0-A796-1063FDB3E479} = {6ED356A7-8B47-4613-AD01-C85CF28491BD}
86 | {22A603E9-7A63-4235-9EA2-53525948045D} = {6ED356A7-8B47-4613-AD01-C85CF28491BD}
87 | {B8F88D98-9933-47AA-806B-3606CCA177C3} = {6ED356A7-8B47-4613-AD01-C85CF28491BD}
88 | {F3C10C1F-A947-4A05-ADFE-517E3ADBF3D1} = {664D406C-2F83-48F0-BFC3-408D5CB53C65}
89 | {26D5CFC5-2368-4C94-822C-5A51AE178A91} = {664D406C-2F83-48F0-BFC3-408D5CB53C65}
90 | EndGlobalSection
91 | GlobalSection(ExtensibilityGlobals) = postSolution
92 | SolutionGuid = {3CB609D9-5D54-4C11-A371-DAAC8B74E430}
93 | EndGlobalSection
94 | EndGlobal
95 |
--------------------------------------------------------------------------------
/server/src/Application/Application.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/server/src/Application/Behaviours/ValidationBehaviour.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation;
2 | using MediatR;
3 |
4 | namespace Application.Behaviours;
5 |
6 | public class ValidationBehaviour : IPipelineBehavior
7 | where TRequest : notnull
8 | {
9 | private readonly IEnumerable> _validators;
10 |
11 | public ValidationBehaviour(IEnumerable> validators)
12 | {
13 | _validators = validators;
14 | }
15 |
16 | public async Task Handle(TRequest request, RequestHandlerDelegate next, CancellationToken cancellationToken)
17 | {
18 | if (_validators.Any())
19 | {
20 | var context = new ValidationContext(request);
21 |
22 | var validationResults = await Task.WhenAll(
23 | _validators.Select(v =>
24 | v.ValidateAsync(context, cancellationToken)));
25 |
26 | var failures = validationResults
27 | .Where(r => r.Errors.Any())
28 | .SelectMany(r => r.Errors)
29 | .ToList();
30 |
31 | if (failures.Any())
32 | throw new ValidationException(failures);
33 | }
34 | return await next();
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/server/src/Application/Consts/StudentConsts.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Consts;
2 |
3 | public static class StudentConsts
4 | {
5 | public const int NameMaxLength = 50;
6 | public const int NameMinLength = 2;
7 |
8 | public const int EmailMaxLength = 320;
9 |
10 | public const int UsernameMaxLength = 40;
11 | public const int UsernameMinLength = 3;
12 | }
13 |
--------------------------------------------------------------------------------
/server/src/Application/DependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Application.Behaviours;
3 | using System.Reflection;
4 | using FluentValidation;
5 | using MediatR;
6 |
7 | namespace Application;
8 | public static class DependencyInjection
9 | {
10 | public static IServiceCollection AddApplicationServices(this IServiceCollection services)
11 | {
12 | services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly());
13 |
14 | services.AddMediatR(cfg =>
15 | {
16 | cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly());
17 | cfg.AddBehavior(typeof(IPipelineBehavior<,>), typeof(ValidationBehaviour<,>));
18 | });
19 |
20 | return services;
21 | }
22 | }
--------------------------------------------------------------------------------
/server/src/Application/RequestModels/CreateStudentRequest.cs:
--------------------------------------------------------------------------------
1 | using Domain.ValueObjects;
2 |
3 | namespace Application.RequestModels;
4 |
5 | public record CreateStudentRequest(string FirstName, string LastName, Email Email, DateOnly BirthDate, string GitHubUsername);
--------------------------------------------------------------------------------
/server/src/Application/RequestModels/UpdateStudentRequest.cs:
--------------------------------------------------------------------------------
1 | using Domain.ValueObjects;
2 |
3 | namespace Application.RequestModels;
4 |
5 | public record UpdateStudentRequest(long Id, string FirstName, string LastName, Email Email,
6 | DateOnly BirthDate, string GitHubUsername);
--------------------------------------------------------------------------------
/server/src/Application/ResponseModels/ErrorResponse.cs:
--------------------------------------------------------------------------------
1 | namespace Application.ResponseModels;
2 |
3 | public record ErrorResponse
4 | {
5 | public string Title { get; set; }
6 | public int StatusCode { get; set; }
7 | public string Message { get; set; }
8 | }
9 |
--------------------------------------------------------------------------------
/server/src/Application/ResponseModels/StudentResponse.cs:
--------------------------------------------------------------------------------
1 | using Domain.ValueObjects;
2 |
3 | namespace Application.ResponseModels;
4 |
5 | public record StudentResponse(long Id, string FirstName, string LastName, Email Email, DateOnly BirthDate, string GitHubUsername);
--------------------------------------------------------------------------------
/server/src/Application/Students/Commands/CreateStudentCommand.cs:
--------------------------------------------------------------------------------
1 | using Application.ResponseModels;
2 | using Domain.ValueObjects;
3 | using MediatR;
4 |
5 | namespace Application.Students.Commands;
6 |
7 | public record CreateStudentCommand(string FirstName, string LastName, Email Email, DateOnly BirthDate,
8 | string GitHubUsername) : IRequest;
9 |
--------------------------------------------------------------------------------
/server/src/Application/Students/Commands/CreateStudentCommandValidator.cs:
--------------------------------------------------------------------------------
1 | using Domain.ValueObjects;
2 | using Domain.Interfaces;
3 | using FluentValidation;
4 | using Application.Consts;
5 |
6 | namespace Application.Students.Commands;
7 |
8 | public sealed class CreateStudentCommandValidator : AbstractValidator
9 | {
10 | private readonly IStudentRepository _studentRepository;
11 | public CreateStudentCommandValidator(IStudentRepository studentRepository)
12 | {
13 | _studentRepository = studentRepository;
14 |
15 | RuleFor(v => v.FirstName)
16 | .NotEmpty()
17 | .NotNull()
18 | .MaximumLength(StudentConsts.NameMaxLength)
19 | .MinimumLength(StudentConsts.NameMinLength);
20 |
21 | RuleFor(v => v.LastName)
22 | .NotEmpty()
23 | .NotNull()
24 | .MaximumLength(StudentConsts.NameMaxLength)
25 | .MinimumLength(StudentConsts.NameMinLength);
26 |
27 | RuleFor(v => v.Email)
28 | .MustAsync(IsEmailUnique)
29 | .WithMessage("'{PropertyName}' must be unique.")
30 | .WithErrorCode("Unique");
31 |
32 | RuleFor(v => v.BirthDate)
33 | .LessThan(DateOnly.FromDateTime(DateTime.Now))
34 | .GreaterThan(DateOnly.FromDateTime(DateTime.Now.AddYears(-150)));
35 |
36 | RuleFor(v => v.GitHubUsername)
37 | .NotEmpty()
38 | .NotNull()
39 | .MaximumLength(StudentConsts.UsernameMaxLength)
40 | .MinimumLength(StudentConsts.UsernameMinLength)
41 | .MustAsync(IsGitHubUsernameUnique)
42 | .WithMessage("'{PropertyName}' must be unique.")
43 | .WithErrorCode("Unique");
44 | }
45 |
46 | private async Task IsEmailUnique(Email email, CancellationToken token)
47 | {
48 | return await _studentRepository.IsEmailUnique(email);
49 | }
50 |
51 | private async Task IsGitHubUsernameUnique(string gitHubUsername, CancellationToken token)
52 | {
53 | return await _studentRepository.IsGitHubUsernameUnique(gitHubUsername);
54 | }
55 | }
--------------------------------------------------------------------------------
/server/src/Application/Students/Commands/DeleteStudentCommand.cs:
--------------------------------------------------------------------------------
1 | using Application.ResponseModels;
2 | using MediatR;
3 |
4 | namespace Application.Students.Commands;
5 |
6 | public record DeleteStudentCommand(long StudentId) : IRequest;
--------------------------------------------------------------------------------
/server/src/Application/Students/Commands/DeleteStudentCommandValidator.cs:
--------------------------------------------------------------------------------
1 | using FluentValidation;
2 |
3 | namespace Application.Students.Commands;
4 |
5 | public sealed class DeleteStudentCommandValidator : AbstractValidator
6 | {
7 | public DeleteStudentCommandValidator()
8 | {
9 | RuleFor(v => v.StudentId)
10 | .NotEmpty()
11 | .NotNull()
12 | .GreaterThan(0);
13 | }
14 | }
--------------------------------------------------------------------------------
/server/src/Application/Students/Commands/UpdateStudentCommand.cs:
--------------------------------------------------------------------------------
1 | using Application.ResponseModels;
2 | using Domain.ValueObjects;
3 | using MediatR;
4 |
5 | namespace Application.Students.Commands;
6 |
7 | public record UpdateStudentCommand(long Id, string FirstName, string LastName, Email Email, DateOnly BirthDate,
8 | string GitHubUsername) : IRequest;
9 |
--------------------------------------------------------------------------------
/server/src/Application/Students/Commands/UpdateStudentCommandValidator.cs:
--------------------------------------------------------------------------------
1 | using Domain.ValueObjects;
2 | using Domain.Interfaces;
3 | using FluentValidation;
4 | using Application.Consts;
5 |
6 | namespace Application.Students.Commands;
7 |
8 | public sealed class UpdateStudentCommandValidator : AbstractValidator
9 | {
10 | private readonly IStudentRepository _studentRepository;
11 | public UpdateStudentCommandValidator(IStudentRepository studentRepository)
12 | {
13 | _studentRepository = studentRepository;
14 |
15 | RuleFor(v => v.Id)
16 | .NotEmpty()
17 | .NotNull()
18 | .GreaterThan(0);
19 |
20 | RuleFor(v => v.FirstName)
21 | .NotEmpty()
22 | .NotNull()
23 | .MaximumLength(StudentConsts.NameMaxLength)
24 | .MinimumLength(StudentConsts.NameMinLength);
25 |
26 | RuleFor(v => v.LastName)
27 | .NotEmpty()
28 | .NotNull()
29 | .MaximumLength(StudentConsts.NameMaxLength)
30 | .MinimumLength(StudentConsts.NameMinLength);
31 |
32 | RuleFor(v => v.Email)
33 | .MustAsync(IsEmailUnique)
34 | .WithMessage("'{PropertyName}' must be unique.")
35 | .WithErrorCode("Unique");
36 |
37 | RuleFor(v => v.BirthDate)
38 | .LessThan(DateOnly.FromDateTime(DateTime.Now))
39 | .GreaterThan(DateOnly.FromDateTime(DateTime.Now.AddYears(-150)));
40 |
41 | RuleFor(v => v.GitHubUsername)
42 | .NotEmpty()
43 | .NotNull()
44 | .MaximumLength(StudentConsts.UsernameMaxLength)
45 | .MinimumLength(StudentConsts.UsernameMinLength)
46 | .MustAsync(IsGitHubUsernameUnique)
47 | .WithMessage("'{PropertyName}' must be unique.")
48 | .WithErrorCode("Unique");
49 | }
50 |
51 | private async Task IsEmailUnique(UpdateStudentCommand model, Email email, CancellationToken token)
52 | {
53 | return await _studentRepository.IsEmailUnique(email, model.Id);
54 | }
55 |
56 | private async Task IsGitHubUsernameUnique(UpdateStudentCommand model, string gitHubUsername, CancellationToken token)
57 | {
58 | return await _studentRepository.IsGitHubUsernameUnique(gitHubUsername, model.Id);
59 | }
60 | }
--------------------------------------------------------------------------------
/server/src/Application/Students/Handlers/CommandHandlers/CreateStudentHandler.cs:
--------------------------------------------------------------------------------
1 | using Application.Students.Commands;
2 | using Application.ResponseModels;
3 | using Domain.Interfaces;
4 | using Domain.Entities;
5 | using Mapster;
6 | using MediatR;
7 |
8 | namespace Application.Students.Handlers.CommandHandlers;
9 |
10 | public class CreateStudentHandler : IRequestHandler
11 | {
12 | private readonly IStudentRepository _studentRepository;
13 | public CreateStudentHandler(IStudentRepository studentRepository)
14 | {
15 | _studentRepository = studentRepository;
16 | }
17 |
18 | public async Task Handle(CreateStudentCommand request, CancellationToken cancellationToken)
19 | {
20 | Student toAdd = request.Adapt();
21 | var result = await _studentRepository.Add(toAdd);
22 | await _studentRepository.SaveChanges();
23 |
24 | return result.Adapt();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/server/src/Application/Students/Handlers/CommandHandlers/DeleteStudentHandler.cs:
--------------------------------------------------------------------------------
1 | using Application.Students.Commands;
2 | using Domain.Interfaces;
3 | using MediatR;
4 |
5 | namespace Application.Students.Handlers.CommandHandlers;
6 |
7 | public class DeleteStudentHandler : IRequestHandler
8 | {
9 | private readonly IStudentRepository _studentRepository;
10 | public DeleteStudentHandler(IStudentRepository studentRepository)
11 | {
12 | _studentRepository = studentRepository;
13 | }
14 |
15 | public async Task Handle(DeleteStudentCommand request, CancellationToken cancellationToken)
16 | {
17 | var toDelete = await _studentRepository.GetById(request.StudentId);
18 | if (toDelete is null) throw new ArgumentException("Student not found");
19 |
20 | _studentRepository.Delete(toDelete);
21 | await _studentRepository.SaveChanges();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/server/src/Application/Students/Handlers/CommandHandlers/UpdateStudentHandler.cs:
--------------------------------------------------------------------------------
1 | using Application.Students.Commands;
2 | using Application.ResponseModels;
3 | using Domain.Interfaces;
4 | using Domain.Entities;
5 | using Mapster;
6 | using MediatR;
7 |
8 | namespace Application.Students.Handlers.CommandHandlers;
9 |
10 | public class UpdateStudentHandler : IRequestHandler
11 | {
12 | private readonly IStudentRepository _studentRepository;
13 | public UpdateStudentHandler(IStudentRepository studentRepository)
14 | {
15 | _studentRepository = studentRepository;
16 | }
17 |
18 | public async Task Handle(UpdateStudentCommand request, CancellationToken cancellationToken)
19 | {
20 | var student = await _studentRepository.GetById(request.Id);
21 | if (student is null) throw new Exception("Student not found");
22 |
23 | Student toUpdate = request.Adapt();
24 |
25 | var result = _studentRepository.Update(toUpdate);
26 | await _studentRepository.SaveChanges();
27 | return result.Adapt();
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/server/src/Application/Students/Handlers/QueryHandlers/GetAllStudentsHandler.cs:
--------------------------------------------------------------------------------
1 | using Application.Students.Queries;
2 | using Application.ResponseModels;
3 | using Domain.Interfaces;
4 | using Mapster;
5 | using MediatR;
6 |
7 | namespace Application.Students.Handlers.QueryHandlers;
8 |
9 | public class GetAllStudentsHandler : IRequestHandler>
10 | {
11 | private readonly IStudentRepository _studentRepository;
12 | public GetAllStudentsHandler(IStudentRepository studentRepository)
13 | {
14 | _studentRepository = studentRepository;
15 | }
16 | public async Task> Handle(GetAllStudentsQuery request, CancellationToken cancellationToken)
17 | {
18 | var result = await _studentRepository.GetAll();
19 | return result.Adapt>();
20 | }
21 | }
--------------------------------------------------------------------------------
/server/src/Application/Students/Handlers/QueryHandlers/GetStudentByIdHandler.cs:
--------------------------------------------------------------------------------
1 | using Application.Students.Queries;
2 | using Application.ResponseModels;
3 | using Domain.Interfaces;
4 | using Mapster;
5 | using MediatR;
6 |
7 | namespace Application.Students.Handlers.QueryHandlers;
8 |
9 | public class GetStudentByIdHandler : IRequestHandler
10 | {
11 | private readonly IStudentRepository _studentRepository;
12 | public GetStudentByIdHandler(IStudentRepository studentRepository)
13 | {
14 | _studentRepository = studentRepository;
15 | }
16 | public async Task Handle(GetStudentByIdQuery request, CancellationToken cancellationToken)
17 | {
18 | var result = await _studentRepository.GetById(request.StudentId);
19 | return result.Adapt();
20 | }
21 | }
--------------------------------------------------------------------------------
/server/src/Application/Students/Queries/GetAllStudentsQuery.cs:
--------------------------------------------------------------------------------
1 | using Application.ResponseModels;
2 | using MediatR;
3 |
4 | namespace Application.Students.Queries;
5 |
6 | public record GetAllStudentsQuery : IRequest>;
--------------------------------------------------------------------------------
/server/src/Application/Students/Queries/GetStudentByIdQuery.cs:
--------------------------------------------------------------------------------
1 | using Application.ResponseModels;
2 | using MediatR;
3 |
4 | namespace Application.Students.Queries;
5 |
6 | public record GetStudentByIdQuery(long StudentId) : IRequest;
--------------------------------------------------------------------------------
/server/src/Domain/Common/AuditableEntityBase.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Common;
2 |
3 | public abstract record AuditableEntityBase : EntityBase
4 | {
5 | public DateTimeOffset CreatedAt { get; set; }
6 |
7 | public long CreatedBy { get; set; }
8 |
9 | public DateTimeOffset LastUpdatedAt { get; set; }
10 |
11 | public long LastUpdatedBy { get; set; }
12 | }
13 |
--------------------------------------------------------------------------------
/server/src/Domain/Common/EntityBase.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.Common;
2 |
3 | public abstract record EntityBase
4 | {
5 | public long Id { get; set; }
6 | }
7 |
--------------------------------------------------------------------------------
/server/src/Domain/Domain.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/server/src/Domain/Entities/Student.cs:
--------------------------------------------------------------------------------
1 | using Domain.ValueObjects;
2 | using Domain.Common;
3 |
4 | namespace Domain.Entities;
5 |
6 | public record Student : EntityBase
7 | {
8 | public string FirstName { get; set; } = null!;
9 |
10 | public string LastName { get; set; } = null!;
11 |
12 | public Email Email { get; set; } = null!;
13 |
14 | public DateOnly BirthDate { get; set; }
15 |
16 | public string GitHubUsername { get; set; } = null!;
17 | }
--------------------------------------------------------------------------------
/server/src/Domain/Interfaces/IStudentRepository.cs:
--------------------------------------------------------------------------------
1 | using Domain.ValueObjects;
2 | using Domain.Entities;
3 |
4 | namespace Domain.Interfaces;
5 |
6 | public interface IStudentRepository
7 | {
8 | Task> GetAll();
9 |
10 | Task GetById(long id);
11 |
12 | Task Add(Student model);
13 |
14 | Student Update(Student model);
15 |
16 | void Delete(Student model);
17 |
18 | Task SaveChanges();
19 |
20 | Task IsEmailUnique(Email email);
21 |
22 | Task IsEmailUnique(Email email, long studentId);
23 |
24 | Task IsGitHubUsernameUnique(string gitHubUsername);
25 |
26 | Task IsGitHubUsernameUnique(string gitHubUsername, long studentId);
27 | }
--------------------------------------------------------------------------------
/server/src/Domain/ValueObjects/Email.cs:
--------------------------------------------------------------------------------
1 | namespace Domain.ValueObjects;
2 |
3 | public record Email
4 | {
5 | public string Value { get; private set; }
6 |
7 | public Email(string value)
8 | {
9 | if (!IsEmailValid(value))
10 | throw new InvalidDataException("Email is invalid");
11 |
12 | Value = value.Trim().ToLower();
13 | }
14 |
15 | bool IsEmailValid(string value)
16 | {
17 | if (string.IsNullOrWhiteSpace(value))
18 | return false;
19 | if (!value.Contains('@'))
20 | return false;
21 | if (value.EndsWith("."))
22 | return false;
23 |
24 | try
25 | {
26 | var addr = new System.Net.Mail.MailAddress(value);
27 | return addr.Address == value.Trim();
28 | }
29 | catch
30 | {
31 | return false;
32 | }
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/server/src/Infrastructure/Configurations/StudentConfiguration.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore.Metadata.Builders;
2 | using Microsoft.EntityFrameworkCore;
3 | using Domain.ValueObjects;
4 | using Domain.Entities;
5 | using Application.Consts;
6 |
7 | namespace Infrastructure.Configurations;
8 |
9 | public class StudentConfiguration : IEntityTypeConfiguration
10 | {
11 | public void Configure(EntityTypeBuilder builder)
12 | {
13 | builder.HasKey(x => x.Id);
14 |
15 | builder.Property(x => x.FirstName)
16 | .HasMaxLength(StudentConsts.NameMaxLength)
17 | .IsRequired();
18 |
19 | builder.Property(x => x.LastName)
20 | .HasMaxLength(StudentConsts.NameMaxLength)
21 | .IsRequired();
22 |
23 | builder.Property(x => x.Email)
24 | .HasConversion(
25 | e => e.Value,
26 | value => new Email(value))
27 | .HasMaxLength(StudentConsts.EmailMaxLength)
28 | .IsRequired();
29 |
30 | builder.HasIndex(x => x.Email)
31 | .IsUnique();
32 |
33 | builder.Property(x => x.BirthDate)
34 | .IsRequired();
35 |
36 | builder.Property(x => x.GitHubUsername)
37 | .HasMaxLength(StudentConsts.UsernameMaxLength)
38 | .IsRequired();
39 |
40 | builder.HasIndex(x => x.GitHubUsername)
41 | .IsUnique();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/server/src/Infrastructure/DbContexts/StudentManagementDBContext.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using System.Reflection;
3 | using Domain.Entities;
4 |
5 | namespace Infrastructure.DbContexts;
6 |
7 | public class StudentManagementDBContext : DbContext
8 | {
9 | public StudentManagementDBContext(DbContextOptions options) : base(options) { }
10 |
11 | public DbSet Students => Set();
12 |
13 | protected override void OnModelCreating(ModelBuilder modelBuilder)
14 | {
15 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
16 | }
17 | }
--------------------------------------------------------------------------------
/server/src/Infrastructure/DependencyInjection.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Configuration;
2 | using Microsoft.EntityFrameworkCore;
3 | using Infrastructure.Repositories;
4 | using Infrastructure.DbContexts;
5 | using Domain.Interfaces;
6 |
7 | namespace Microsoft.Extensions.DependencyInjection;
8 |
9 | public static class DependencyInjection
10 | {
11 | public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration)
12 | {
13 | // Setting DBContexts
14 | var connectionString = configuration.GetConnectionString("DefaultConnection");
15 | services.AddDbContext(options => options.UseNpgsql(connectionString, o => o.UseNodaTime()));
16 | services.AddHealthChecks().AddNpgSql(connectionString, "StudentManagementDB");
17 |
18 | AppContext.SetSwitch("Npgsql.EnableLegacyTimestampBehavior", true);
19 |
20 | services.AddScoped();
21 |
22 | return services;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/server/src/Infrastructure/Infrastructure.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 |
9 |
10 |
11 |
12 |
13 | all
14 | runtime; build; native; contentfiles; analyzers; buildtransitive
15 |
16 |
17 | all
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/server/src/Infrastructure/Migrations/20240521092028_v1.Designer.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Infrastructure.DbContexts;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Infrastructure;
6 | using Microsoft.EntityFrameworkCore.Migrations;
7 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
8 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
9 |
10 | #nullable disable
11 |
12 | namespace Infrastructure.Migrations
13 | {
14 | [DbContext(typeof(StudentManagementDBContext))]
15 | [Migration("20240521092028_v1")]
16 | partial class v1
17 | {
18 | ///
19 | protected override void BuildTargetModel(ModelBuilder modelBuilder)
20 | {
21 | #pragma warning disable 612, 618
22 | modelBuilder
23 | .HasAnnotation("ProductVersion", "8.0.4")
24 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
25 |
26 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
27 |
28 | modelBuilder.Entity("Domain.Entities.Student", b =>
29 | {
30 | b.Property("Id")
31 | .ValueGeneratedOnAdd()
32 | .HasColumnType("bigint");
33 |
34 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
35 |
36 | b.Property("BirthDate")
37 | .HasColumnType("date");
38 |
39 | b.Property("Email")
40 | .IsRequired()
41 | .HasMaxLength(320)
42 | .HasColumnType("character varying(320)");
43 |
44 | b.Property("FirstName")
45 | .IsRequired()
46 | .HasMaxLength(50)
47 | .HasColumnType("character varying(50)");
48 |
49 | b.Property("GitHubUsername")
50 | .IsRequired()
51 | .HasMaxLength(40)
52 | .HasColumnType("character varying(40)");
53 |
54 | b.Property("LastName")
55 | .IsRequired()
56 | .HasMaxLength(50)
57 | .HasColumnType("character varying(50)");
58 |
59 | b.HasKey("Id");
60 |
61 | b.HasIndex("Email")
62 | .IsUnique();
63 |
64 | b.HasIndex("GitHubUsername")
65 | .IsUnique();
66 |
67 | b.ToTable("Students");
68 | });
69 | #pragma warning restore 612, 618
70 | }
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/server/src/Infrastructure/Migrations/20240521092028_v1.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Microsoft.EntityFrameworkCore.Migrations;
3 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
4 |
5 | #nullable disable
6 |
7 | namespace Infrastructure.Migrations
8 | {
9 | ///
10 | public partial class v1 : Migration
11 | {
12 | ///
13 | protected override void Up(MigrationBuilder migrationBuilder)
14 | {
15 | migrationBuilder.CreateTable(
16 | name: "Students",
17 | columns: table => new
18 | {
19 | Id = table.Column(type: "bigint", nullable: false)
20 | .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
21 | FirstName = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
22 | LastName = table.Column(type: "character varying(50)", maxLength: 50, nullable: false),
23 | Email = table.Column(type: "character varying(320)", maxLength: 320, nullable: false),
24 | BirthDate = table.Column(type: "date", nullable: false),
25 | GitHubUsername = table.Column(type: "character varying(40)", maxLength: 40, nullable: false)
26 | },
27 | constraints: table =>
28 | {
29 | table.PrimaryKey("PK_Students", x => x.Id);
30 | });
31 |
32 | migrationBuilder.CreateIndex(
33 | name: "IX_Students_Email",
34 | table: "Students",
35 | column: "Email",
36 | unique: true);
37 |
38 | migrationBuilder.CreateIndex(
39 | name: "IX_Students_GitHubUsername",
40 | table: "Students",
41 | column: "GitHubUsername",
42 | unique: true);
43 | }
44 |
45 | ///
46 | protected override void Down(MigrationBuilder migrationBuilder)
47 | {
48 | migrationBuilder.DropTable(
49 | name: "Students");
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/server/src/Infrastructure/Migrations/StudentManagementDBContextModelSnapshot.cs:
--------------------------------------------------------------------------------
1 | //
2 | using System;
3 | using Infrastructure.DbContexts;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.EntityFrameworkCore.Infrastructure;
6 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
7 | using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
8 |
9 | #nullable disable
10 |
11 | namespace Infrastructure.Migrations
12 | {
13 | [DbContext(typeof(StudentManagementDBContext))]
14 | partial class StudentManagementDBContextModelSnapshot : ModelSnapshot
15 | {
16 | protected override void BuildModel(ModelBuilder modelBuilder)
17 | {
18 | #pragma warning disable 612, 618
19 | modelBuilder
20 | .HasAnnotation("ProductVersion", "8.0.4")
21 | .HasAnnotation("Relational:MaxIdentifierLength", 63);
22 |
23 | NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
24 |
25 | modelBuilder.Entity("Domain.Entities.Student", b =>
26 | {
27 | b.Property("Id")
28 | .ValueGeneratedOnAdd()
29 | .HasColumnType("bigint");
30 |
31 | NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id"));
32 |
33 | b.Property("BirthDate")
34 | .HasColumnType("date");
35 |
36 | b.Property("Email")
37 | .IsRequired()
38 | .HasMaxLength(320)
39 | .HasColumnType("character varying(320)");
40 |
41 | b.Property("FirstName")
42 | .IsRequired()
43 | .HasMaxLength(50)
44 | .HasColumnType("character varying(50)");
45 |
46 | b.Property("GitHubUsername")
47 | .IsRequired()
48 | .HasMaxLength(40)
49 | .HasColumnType("character varying(40)");
50 |
51 | b.Property("LastName")
52 | .IsRequired()
53 | .HasMaxLength(50)
54 | .HasColumnType("character varying(50)");
55 |
56 | b.HasKey("Id");
57 |
58 | b.HasIndex("Email")
59 | .IsUnique();
60 |
61 | b.HasIndex("GitHubUsername")
62 | .IsUnique();
63 |
64 | b.ToTable("Students");
65 | });
66 | #pragma warning restore 612, 618
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/server/src/Infrastructure/Repositories/StudentRepository.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.EntityFrameworkCore;
2 | using Infrastructure.DbContexts;
3 | using Domain.ValueObjects;
4 | using Domain.Interfaces;
5 | using Domain.Entities;
6 |
7 | namespace Infrastructure.Repositories
8 | {
9 | public class StudentRepository : IStudentRepository
10 | {
11 | private readonly StudentManagementDBContext _dbContext;
12 | public StudentRepository(StudentManagementDBContext dbContext)
13 | {
14 | _dbContext = dbContext;
15 | }
16 |
17 | public async Task> GetAll()
18 | {
19 | return await _dbContext.Students.ToListAsync();
20 | }
21 |
22 | public async Task GetById(long id)
23 | {
24 | return await _dbContext.Students.AsNoTracking().FirstOrDefaultAsync(c => c.Id == id);
25 | }
26 |
27 | public async Task Add(Student model)
28 | {
29 | var result = await _dbContext.Students.AddAsync(model);
30 | return result.Entity;
31 | }
32 |
33 | public Student Update(Student model)
34 | {
35 | var result = _dbContext.Students.Update(model);
36 | return result.Entity;
37 | }
38 |
39 | public void Delete(Student model)
40 | {
41 | _dbContext.Students.Remove(model);
42 | }
43 |
44 | public async Task SaveChanges()
45 | {
46 | await _dbContext.SaveChangesAsync();
47 | }
48 |
49 | public async Task IsEmailUnique(Email email)
50 | {
51 | return !await _dbContext.Students.AnyAsync(c => c.Email == email);
52 | }
53 |
54 | public async Task IsEmailUnique(Email email, long studentId)
55 | {
56 | return !await _dbContext.Students.AnyAsync(c => c.Email == email && c.Id != studentId);
57 | }
58 |
59 | public async Task IsGitHubUsernameUnique(string gitHubUsername)
60 | {
61 | return !await _dbContext.Students.AnyAsync(c => c.GitHubUsername.ToLower() == gitHubUsername.ToLower());
62 | }
63 |
64 | public async Task IsGitHubUsernameUnique(string gitHubUsername, long studentId)
65 | {
66 | return !await _dbContext.Students.AnyAsync(c => c.GitHubUsername.ToLower() == gitHubUsername.ToLower() && c.Id != studentId);
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/server/src/WebAPI/Controllers/StudentsController.cs:
--------------------------------------------------------------------------------
1 | using Application.Students.Commands;
2 | using Application.Students.Queries;
3 | using Application.RequestModels;
4 | using Microsoft.AspNetCore.Mvc;
5 | using Mapster;
6 | using MediatR;
7 |
8 | namespace WebAPI.Controllers;
9 |
10 | [Route("api/students")]
11 | [ApiController]
12 | public class StudentsController : ControllerBase
13 | {
14 | private readonly IMediator _mediator;
15 | public StudentsController(IMediator mediator)
16 | {
17 | _mediator = mediator;
18 | }
19 |
20 | [HttpGet]
21 | public async Task Get()
22 | {
23 | var query = new GetAllStudentsQuery();
24 | var response = await _mediator.Send(query);
25 | return Ok(response);
26 | }
27 |
28 | [HttpGet("{studentId:long}")]
29 | public async Task Get(long studentId)
30 | {
31 | var query = new GetStudentByIdQuery(studentId);
32 | var response = await _mediator.Send(query);
33 | return Ok(response);
34 | }
35 |
36 | [HttpPost]
37 | public async Task Post([FromBody] CreateStudentRequest request)
38 | {
39 | var command = request.Adapt();
40 | var response = await _mediator.Send(command);
41 | return Ok(response);
42 | }
43 |
44 | [HttpPut("{studentId:long}")]
45 | public async Task Put(long studentId, [FromBody] UpdateStudentRequest request)
46 | {
47 | var command = request.Adapt();
48 | if (studentId != command.Id) return BadRequest();
49 | var response = await _mediator.Send(command);
50 | return Ok(response);
51 | }
52 |
53 | [HttpDelete("{studentId:long}")]
54 | public async Task Delete(long studentId)
55 | {
56 | var command = new DeleteStudentCommand(studentId);
57 | await _mediator.Send(command);
58 | return Ok();
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/server/src/WebAPI/Middleware/GlobalExceptionHandler.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Diagnostics;
2 | using Application.ResponseModels;
3 | using System.Net;
4 |
5 | namespace WebAPI.Middleware;
6 |
7 | public class GlobalExceptionHandler : IExceptionHandler
8 | {
9 | private readonly ILogger _logger;
10 |
11 | public GlobalExceptionHandler(ILogger logger)
12 | {
13 | _logger = logger;
14 | }
15 |
16 | public async ValueTask TryHandleAsync(
17 | HttpContext httpContext,
18 | Exception exception,
19 | CancellationToken cancellationToken)
20 | {
21 | _logger.LogError(
22 | $"An error occurred while processing your request: {exception.Message}");
23 |
24 | var errorResponse = new ErrorResponse
25 | {
26 | Message = exception.Message
27 | };
28 |
29 | switch (exception)
30 | {
31 | case BadHttpRequestException:
32 | errorResponse.StatusCode = (int)HttpStatusCode.BadRequest;
33 | errorResponse.Title = exception.GetType().Name + "://";
34 | break;
35 |
36 | default:
37 | errorResponse.StatusCode = (int)HttpStatusCode.InternalServerError;
38 | errorResponse.Title = "Internal Server Error ://";
39 | break;
40 | }
41 |
42 | httpContext.Response.StatusCode = errorResponse.StatusCode;
43 |
44 | await httpContext
45 | .Response
46 | .WriteAsJsonAsync(errorResponse, cancellationToken);
47 |
48 | return true;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/server/src/WebAPI/Program.cs:
--------------------------------------------------------------------------------
1 | using Application;
2 | using WebAPI.Middleware;
3 |
4 | var builder = WebApplication.CreateBuilder(args);
5 |
6 | builder.Services.AddApplicationServices();
7 | builder.Services.AddInfrastructureServices(builder.Configuration);
8 | builder.Services.AddExceptionHandler();
9 | builder.Services.AddProblemDetails();
10 |
11 | builder.Services.AddControllers();
12 | builder.Services.AddEndpointsApiExplorer();
13 | builder.Services.AddSwaggerGen();
14 |
15 | var app = builder.Build();
16 |
17 | // Enable CORS for the client app running inside a Docker container
18 | app.UseCors(options => options.WithOrigins("http://localhost:8080").AllowAnyHeader().AllowAnyMethod());
19 |
20 | // Enable CORS for the client app running on localhost
21 | app.UseCors(options => options.WithOrigins("https://localhost:7147").AllowAnyHeader().AllowAnyMethod());
22 |
23 | if (app.Environment.IsDevelopment())
24 | {
25 | app.UseSwagger();
26 | app.UseSwaggerUI();
27 | }
28 |
29 | app.UseHttpsRedirection();
30 |
31 | app.UseAuthorization();
32 |
33 | app.MapControllers();
34 |
35 | app.Run();
36 |
37 | public partial class Program { }
--------------------------------------------------------------------------------
/server/src/WebAPI/Properties/launchSettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json.schemastore.org/launchsettings.json",
3 | "iisSettings": {
4 | "windowsAuthentication": false,
5 | "anonymousAuthentication": true,
6 | "iisExpress": {
7 | "applicationUrl": "http://localhost:23657",
8 | "sslPort": 44306
9 | }
10 | },
11 | "profiles": {
12 | "http": {
13 | "commandName": "Project",
14 | "dotnetRunMessages": true,
15 | "launchBrowser": true,
16 | "launchUrl": "swagger",
17 | "applicationUrl": "http://localhost:5127",
18 | "environmentVariables": {
19 | "ASPNETCORE_ENVIRONMENT": "Development"
20 | }
21 | },
22 | "https": {
23 | "commandName": "Project",
24 | "dotnetRunMessages": true,
25 | "launchBrowser": true,
26 | "launchUrl": "swagger",
27 | "applicationUrl": "https://localhost:7120;http://localhost:5127",
28 | "environmentVariables": {
29 | "ASPNETCORE_ENVIRONMENT": "Development"
30 | }
31 | },
32 | "IIS Express": {
33 | "commandName": "IISExpress",
34 | "launchBrowser": true,
35 | "launchUrl": "swagger",
36 | "environmentVariables": {
37 | "ASPNETCORE_ENVIRONMENT": "Development"
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/server/src/WebAPI/RequestExamples.http:
--------------------------------------------------------------------------------
1 | @WebAPI_HostAddress = https://localhost:7120
2 |
3 | ### Get All Students
4 | GET {{WebAPI_HostAddress}}/api/students/
5 | Accept: application/json
6 |
7 | ### Get Student By Id
8 | GET {{WebAPI_HostAddress}}/api/students/1
9 | Accept: application/json
10 |
11 | ### Create a new student
12 | POST {{WebAPI_HostAddress}}/api/students/
13 | Content-Type: application/json
14 |
15 | {
16 | "FirstName": "Emma",
17 | "LastName": "Bovary",
18 | "Email": {
19 | "Value": "emmabovary@gmail.com"
20 | },
21 | "BirthDate": "2004-10-01",
22 | "GitHubUsername": "emma-bovary"
23 | }
24 |
--------------------------------------------------------------------------------
/server/src/WebAPI/WebAPI.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | true
8 |
9 |
10 |
11 |
12 | all
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/server/src/WebAPI/appsettings.Development.json:
--------------------------------------------------------------------------------
1 | {
2 | "Logging": {
3 | "LogLevel": {
4 | "Default": "Information",
5 | "Microsoft.AspNetCore": "Warning"
6 | }
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/server/src/WebAPI/appsettings.json:
--------------------------------------------------------------------------------
1 | {
2 | "ConnectionStrings": {
3 | // Connect to the database in the Docker container
4 | "DefaultConnection": "Host=localhost;Port=5433;Database=StudentManagementDB;Username=postgres;Password=mysecretpassword;"
5 | },
6 | "Logging": {
7 | "LogLevel": {
8 | "Default": "Information",
9 | "Microsoft.AspNetCore": "Warning"
10 | }
11 | },
12 | "AllowedHosts": "*"
13 | }
14 |
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Integration/Application.Tests.Integration.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 | all
21 |
22 |
23 | runtime; build; native; contentfiles; analyzers; buildtransitive
24 | all
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Integration/BaseHandlerTest.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using MediatR;
3 |
4 | namespace Application.Tests.Integration;
5 |
6 | public class BaseHandlerTest : IClassFixture
7 | {
8 | private readonly IServiceScope _serviceScope;
9 | protected readonly ISender _sender;
10 | protected BaseHandlerTest(IntegrationTestWebApplicationFactory factory)
11 | {
12 | _serviceScope = factory.Services.CreateScope();
13 | _sender = _serviceScope.ServiceProvider.GetRequiredService();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Integration/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Integration/IntegrationTestWebApplicationFactory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.AspNetCore.Mvc.Testing;
3 | using Microsoft.AspNetCore.TestHost;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Infrastructure.DbContexts;
7 | using Testcontainers.PostgreSql;
8 |
9 | namespace Application.Tests.Integration;
10 |
11 | public class IntegrationTestWebApplicationFactory : WebApplicationFactory, IAsyncLifetime
12 | {
13 | private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
14 | .WithImage("postgres:latest")
15 | .WithDatabase("StudentManagementDB_Test")
16 | .WithUsername("postgres")
17 | .WithPassword("mysecretpassword")
18 | .Build();
19 |
20 | public async Task InitializeAsync()
21 | {
22 | await _dbContainer.StartAsync();
23 |
24 | using (var scope = Services.CreateScope())
25 | {
26 | var scopedServices = scope.ServiceProvider;
27 | var cntx = scopedServices.GetRequiredService();
28 |
29 | await cntx.Database.EnsureCreatedAsync();
30 | }
31 | }
32 |
33 | public new async Task DisposeAsync()
34 | {
35 | await _dbContainer.StopAsync();
36 | }
37 |
38 | protected override void ConfigureWebHost(IWebHostBuilder builder)
39 | {
40 | builder.ConfigureTestServices(services =>
41 | {
42 | var descriptor = services.SingleOrDefault(s => s.ServiceType == typeof(DbContextOptions));
43 |
44 | if (descriptor is not null)
45 | {
46 | services.Remove(descriptor);
47 | }
48 |
49 | services.AddDbContext(options =>
50 | {
51 | options.UseNpgsql(_dbContainer.GetConnectionString());
52 | });
53 | });
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Integration/StudentHandlersTests/StudentHandlersTests.cs:
--------------------------------------------------------------------------------
1 | using Application.Students.Commands;
2 | using Application.Students.Queries;
3 | using Domain.ValueObjects;
4 |
5 | namespace Application.Tests.Integration.StudentHandlersTests;
6 |
7 | public class StudentHandlersTests : BaseHandlerTest
8 | {
9 | public StudentHandlersTests(IntegrationTestWebApplicationFactory factory) : base(factory) { }
10 |
11 | [Fact]
12 | public async void GetAllStudentsHandler_Returns_StudentResponse()
13 | {
14 | // Arrange
15 | var query = new GetAllStudentsQuery();
16 |
17 | // Act
18 | var result = await _sender.Send(query);
19 |
20 | // Assert
21 | Assert.NotNull(result);
22 | }
23 |
24 | [Fact]
25 | public async void CreateStudentCommand_Returns_StudentResponse()
26 | {
27 | // Arrange
28 | var command = new CreateStudentCommand(
29 | FirstName: "Danial",
30 | LastName: "Moradi",
31 | Email: new Email("danial@gmail.com"),
32 | BirthDate: new DateOnly(2000, 3, 2),
33 | GitHubUsername: "dani");
34 |
35 | // Act
36 | var result = await _sender.Send(command);
37 |
38 | // Assert
39 | Assert.NotNull(result);
40 | Assert.Equal(command.FirstName, result.FirstName);
41 | Assert.Equal(command.Email, result.Email);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Unit/Application.Tests.Unit.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 | all
20 |
21 |
22 | runtime; build; native; contentfiles; analyzers; buildtransitive
23 | all
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Unit/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Application.Students.Handlers.CommandHandlers;
2 | global using Application.Students.Handlers.QueryHandlers;
3 | global using Application.Students.Commands;
4 | global using Application.Students.Queries;
5 | global using Application.ResponseModels;
6 | global using Domain.ValueObjects;
7 | global using Domain.Interfaces;
8 | global using Domain.Entities;
9 | global using NSubstitute;
10 | global using Xunit;
11 | global using Bogus;
12 |
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Unit/StudentHandlersTests/CreateStudentHandlerTests.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Tests.Unit.StudentHandlersTests;
2 |
3 | public class CreateStudentHandlerTests
4 | {
5 | [Fact]
6 | public async void CreateStudentHandler_Returns_StudentResponse()
7 | {
8 | // Arrange
9 | var fakeRepository = Substitute.For();
10 |
11 | var responseStudent = new Student
12 | {
13 | Id = 1,
14 | FirstName = "Sara",
15 | LastName = "Rasoulian",
16 | Email = new Email("sara@gmail.com"),
17 | BirthDate = new DateOnly(1900, 1, 1),
18 | GitHubUsername = "sara90",
19 | };
20 |
21 | fakeRepository.Add(Arg.Any()).Returns(responseStudent);
22 |
23 | var command = new CreateStudentCommand(
24 | FirstName: "Sara",
25 | LastName: "Rasoulian",
26 | BirthDate: new DateOnly(1900, 1, 1),
27 | Email: new Email("sara@gmail.com"),
28 | GitHubUsername: "sara90");
29 |
30 | var handler = new CreateStudentHandler(fakeRepository);
31 |
32 | // Act
33 | var result = await handler.Handle(command, default);
34 |
35 | // Assert
36 | Assert.NotNull(result);
37 | Assert.Equal(command.FirstName, result.FirstName);
38 | Assert.Equal(command.Email.Value, result.Email.Value);
39 | await fakeRepository.Received(1).SaveChanges();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Unit/StudentHandlersTests/DeleteStudentHandlerTests.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Tests.Unit.StudentHandlersTests;
2 |
3 | public class DeleteStudentHandlerTests
4 | {
5 | [Fact]
6 | public async Task DeleteStudentHandler_With_Valid_StudentId_Deletes_Student()
7 | {
8 | // Arrange
9 | var studentId = 1;
10 | var mockRepository = Substitute.For();
11 | var student = new Student { Id = studentId };
12 |
13 | mockRepository.GetById(studentId).Returns(student);
14 |
15 | var handler = new DeleteStudentHandler(mockRepository);
16 | var command = new DeleteStudentCommand(studentId);
17 |
18 | // Act
19 | await handler.Handle(command, default);
20 |
21 | // Assert
22 | mockRepository.Received(1).Delete(Arg.Any());
23 | await mockRepository.Received(1).SaveChanges();
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Unit/StudentHandlersTests/GetAllStudentsHandlerTests.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Tests.Unit.StudentHandlersTests;
2 |
3 | public class GetAllStudentsHandlerTests
4 | {
5 | [Fact]
6 | public async Task GetAllStudentsHandler_Returns_Students()
7 | {
8 | // Arrange
9 | var fakeRepository = Substitute.For();
10 |
11 | byte fakeStudentCount = 4;
12 | var fakeStudents = new Faker().Generate(fakeStudentCount);
13 |
14 | fakeRepository.GetAll().Returns(fakeStudents);
15 |
16 | var handler = new GetAllStudentsHandler(fakeRepository);
17 | var query = new GetAllStudentsQuery();
18 |
19 | // Act
20 | var result = await handler.Handle(query, default);
21 |
22 | // Assert
23 | Assert.NotNull(result);
24 | Assert.IsAssignableFrom>(result);
25 | Assert.Equal(fakeStudentCount, (result as List).Count);
26 | }
27 | }
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Unit/StudentHandlersTests/GetStudentByIdHandlerTests.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Tests.Unit.StudentHandlersTests;
2 |
3 | public class GetStudentByIdHandlerTests
4 | {
5 | [Fact]
6 | public async Task GetStudentByIdHandler_With_ValidId_Returns_StudentResponse()
7 | {
8 | // Arrange
9 | var studentId = 1;
10 | var fakeStudent = new Faker()
11 | .RuleFor(c => c.Id, studentId)
12 | .RuleFor(c => c.FirstName, f => f.Person.FirstName).Generate();
13 |
14 | var mockRepository = Substitute.For();
15 | mockRepository.GetById(studentId).Returns(fakeStudent);
16 |
17 | var handler = new GetStudentByIdHandler(mockRepository);
18 | var query = new GetStudentByIdQuery(studentId);
19 |
20 | // Act
21 | var result = await handler.Handle(query, default);
22 |
23 | // Assert
24 | Assert.NotNull(result);
25 | Assert.Equal(studentId, result.Id);
26 | Assert.Equal(result.FirstName, fakeStudent.FirstName);
27 | }
28 |
29 | [Fact]
30 | public async Task GetStudentByIdHandler_With_InvalidId_Returns_Null()
31 | {
32 | // Arrange
33 | long studentId = 999;
34 |
35 | var mockRepository = Substitute.For();
36 | mockRepository.GetById(studentId)
37 | .Returns(Task.FromResult(null));
38 |
39 | var handler = new GetStudentByIdHandler(mockRepository);
40 | var query = new GetStudentByIdQuery(studentId);
41 |
42 | // Act
43 | var result = await handler.Handle(query, default);
44 |
45 | // Assert
46 | Assert.Null(result);
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/server/tests/Application.Tests.Unit/StudentHandlersTests/UpdateStudentHandlerTests.cs:
--------------------------------------------------------------------------------
1 | namespace Application.Tests.Unit.StudentHandlersTests;
2 |
3 | public class UpdateStudentHandlerTests
4 | {
5 | [Fact]
6 | public async Task UpdateStudentHandler_Returns_StudentResponse()
7 | {
8 | // Arrange
9 | var studentId = 1;
10 | var updateCommand = new UpdateStudentCommand(
11 | Id: studentId,
12 | FirstName: "Sara",
13 | LastName: "Rasoulian",
14 | BirthDate: new DateOnly(1900, 1, 1),
15 | Email: new Email("sara@gmail.com"),
16 | GitHubUsername: "sara90");
17 |
18 | var existingStudent = new Student
19 | {
20 | Id = studentId,
21 | FirstName = "Sara",
22 | LastName = "Rasoulian",
23 | Email = new Email("sara@gmail.com"),
24 | BirthDate = new DateOnly(1900, 1, 1),
25 | GitHubUsername = "sara90",
26 | };
27 |
28 | var mockRepository = Substitute.For();
29 | mockRepository.GetById(studentId).Returns(existingStudent);
30 |
31 | var handler = new UpdateStudentHandler(mockRepository);
32 |
33 | // Act
34 | await handler.Handle(updateCommand, default);
35 |
36 | // Assert
37 | mockRepository.Received(1).Update(Arg.Any());
38 | await mockRepository.Received(1).SaveChanges();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/server/tests/Domain.Tests.Unit/Domain.Tests.Unit.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/server/tests/Domain.Tests.Unit/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/server/tests/Domain.Tests.Unit/ValueObjectsTests/EmailTests.cs:
--------------------------------------------------------------------------------
1 | using Domain.ValueObjects;
2 |
3 | namespace Domain.UnitTests.ValueObjectsTests;
4 |
5 | public class EmailTests
6 | {
7 | [Fact]
8 | public void Valid_Email_Returns_Email()
9 | {
10 | // Arrange
11 | string validEmail = "example@gmail.com";
12 |
13 | // Act
14 | Email email = new Email(validEmail);
15 |
16 | // Assert
17 | Assert.Equal(validEmail, email.Value);
18 | }
19 |
20 | [Theory]
21 | [InlineData("invalidexample.com")]
22 | [InlineData(".invalid@example.com")]
23 | [InlineData("@invalid")]
24 | [InlineData("invalid@.com")]
25 | public void Invalid_Email_Throws_InvalidDataException(string invalidEmail)
26 | {
27 | // Act
28 | Action act = () => new Email(invalidEmail);
29 |
30 | // Assert
31 | Assert.Throws(act);
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Acceptance/BaseAcceptanceTest.cs:
--------------------------------------------------------------------------------
1 | using WebAPI.Tests.Acceptance.Hooks;
2 |
3 | namespace WebAPI.Tests.Acceptance;
4 |
5 | public abstract class BaseAcceptanceTest : IClassFixture
6 | {
7 | protected readonly HttpClient _httpClient;
8 |
9 | protected BaseAcceptanceTest(AcceptanceTestWebApplicationFactory factory)
10 | {
11 | _httpClient = factory.CreateDefaultClient();
12 | }
13 | }
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Acceptance/Drivers/StudentManagementDriver.cs:
--------------------------------------------------------------------------------
1 | using WebAPI.Tests.Acceptance.Hooks;
2 | using WebAPI.Tests.Acceptance.Dtos;
3 | using TechTalk.SpecFlow.Assist;
4 | using System.Globalization;
5 | using System.Net.Http.Json;
6 | using Domain.ValueObjects;
7 | using TechTalk.SpecFlow;
8 |
9 | namespace WebAPI.Tests.Acceptance.Drivers;
10 |
11 | public class StudentManagementDriver : BaseAcceptanceTest
12 | {
13 | public StudentManagementDriver(AcceptanceTestWebApplicationFactory factory) : base(factory)
14 | {
15 | }
16 |
17 | public List GetAllStudents()
18 | {
19 | var response = _httpClient.GetAsync("/api/students").Result;
20 | return response.Content.ReadFromJsonAsync>().Result;
21 | }
22 |
23 | public HttpResponseMessage CreateStudent(Table student)
24 | {
25 | var request = ParseCreateStudentData(student);
26 | return _httpClient.PostAsJsonAsync("/api/students", request).Result;
27 | }
28 |
29 | public HttpResponseMessage UpdateStudent(long studentId, Table student)
30 | {
31 | var request = ParseCreateStudentData(student);
32 | request.Id = studentId;
33 |
34 | return _httpClient.PutAsJsonAsync("/api/students/" + studentId, request).Result;
35 | }
36 |
37 | public HttpResponseMessage DeleteStudent(long studentId)
38 | {
39 | return _httpClient.DeleteAsync("/api/students/" + studentId).Result;
40 | }
41 |
42 | public StudentDto ParseCreateStudentData(Table student)
43 | {
44 | var request = student.CreateInstance();
45 | request.BirthDate = DateOnly.ParseExact(student.Rows[0]["BirthDate"], "yyyy-MM-dd", CultureInfo.InvariantCulture);
46 | request.Email = new Email(student.Rows[0]["Email"]);
47 | return request;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Acceptance/Dtos/StudentDto.cs:
--------------------------------------------------------------------------------
1 | using Domain.ValueObjects;
2 |
3 | namespace WebAPI.Tests.Acceptance.Dtos;
4 |
5 | public record StudentDto
6 | {
7 | public long Id { get; set; }
8 |
9 | public string FirstName { get; set; } = null!;
10 |
11 | public string LastName { get; set; } = null!;
12 |
13 | public Email Email { get; set; } = null!;
14 |
15 | public DateOnly BirthDate { get; set; }
16 |
17 | public string GitHubUsername { get; set; } = null!;
18 | }
19 |
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Acceptance/Features/StudentManagement.feature:
--------------------------------------------------------------------------------
1 | Feature: StudentManagement
2 |
3 | Create, Read, Update, Delete Student
4 |
5 | Background: system error codes are following
6 | | Code | Description |
7 | | 500 | Internal Server Error |
8 |
9 | Scenario: User creates, reads, updates and deletes a student
10 | Given platform has "0" students
11 | When user creates a student with following data by sending 'Create Student Command' through API
12 | | FirstName | LastName | Email | BirthDate | GitHubUsername |
13 | | John | Doe | john@doe.com | 2004-01-01 | john-doe |
14 | Then user requests to get all the students and it must return "1" student with following data
15 | | FirstName | LastName | Email | BirthDate | GitHubUsername |
16 | | John | Doe | john@doe.com | 2004-01-01 | john-doe |
17 | When user creates another student with following data by sending 'Create Student Command' through API
18 | | FirstName | LastName | Email | BirthDate | GitHubUsername |
19 | | John | Doe | john@gmail.com | 2004-01-01 | john-doe |
20 | Then user must get status code "500"
21 | When user updates student by email "john@doe.com" with following data
22 | | FirstName | LastName | Email | BirthDate | GitHubUsername |
23 | | Jane | William | jane@william.com | 2000-01-01 | jane-will |
24 | Then user requests to get all the students and it must return "1" student
25 | | FirstName | LastName | Email | BirthDate | GitHubUsername |
26 | | Jane | William | jane@william.com | 2000-01-01 | jane-will |
27 | When user deletes student by Email of "jane@william.com"
28 | Then user requests to get all students and it must return "0" students
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Acceptance/Features/StudentManagement.feature.cs:
--------------------------------------------------------------------------------
1 | // ------------------------------------------------------------------------------
2 | //
3 | // This code was generated by SpecFlow (https://www.specflow.org/).
4 | // SpecFlow Version:3.9.0.0
5 | // SpecFlow Generator Version:3.9.0.0
6 | //
7 | // Changes to this file may cause incorrect behavior and will be lost if
8 | // the code is regenerated.
9 | //
10 | // ------------------------------------------------------------------------------
11 | #region Designer generated code
12 | #pragma warning disable
13 | namespace WebAPI.Tests.Acceptance.Features
14 | {
15 | using TechTalk.SpecFlow;
16 | using System;
17 | using System.Linq;
18 |
19 |
20 | [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")]
21 | [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
22 | public partial class StudentManagementFeature : object, Xunit.IClassFixture, System.IDisposable
23 | {
24 |
25 | private static TechTalk.SpecFlow.ITestRunner testRunner;
26 |
27 | private static string[] featureTags = ((string[])(null));
28 |
29 | private Xunit.Abstractions.ITestOutputHelper _testOutputHelper;
30 |
31 | #line 1 "StudentManagement.feature"
32 | #line hidden
33 |
34 | public StudentManagementFeature(StudentManagementFeature.FixtureData fixtureData, WebAPI_Tests_Acceptance_XUnitAssemblyFixture assemblyFixture, Xunit.Abstractions.ITestOutputHelper testOutputHelper)
35 | {
36 | this._testOutputHelper = testOutputHelper;
37 | this.TestInitialize();
38 | }
39 |
40 | public static void FeatureSetup()
41 | {
42 | testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner();
43 | TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "Features", "StudentManagement", "Create, Read, Update, Delete Student", ProgrammingLanguage.CSharp, featureTags);
44 | testRunner.OnFeatureStart(featureInfo);
45 | }
46 |
47 | public static void FeatureTearDown()
48 | {
49 | testRunner.OnFeatureEnd();
50 | testRunner = null;
51 | }
52 |
53 | public void TestInitialize()
54 | {
55 | }
56 |
57 | public void TestTearDown()
58 | {
59 | testRunner.OnScenarioEnd();
60 | }
61 |
62 | public void ScenarioInitialize(TechTalk.SpecFlow.ScenarioInfo scenarioInfo)
63 | {
64 | testRunner.OnScenarioInitialize(scenarioInfo);
65 | testRunner.ScenarioContext.ScenarioContainer.RegisterInstanceAs(_testOutputHelper);
66 | }
67 |
68 | public void ScenarioStart()
69 | {
70 | testRunner.OnScenarioStart();
71 | }
72 |
73 | public void ScenarioCleanup()
74 | {
75 | testRunner.CollectScenarioErrors();
76 | }
77 |
78 | public virtual void FeatureBackground()
79 | {
80 | #line 5
81 | #line hidden
82 | }
83 |
84 | void System.IDisposable.Dispose()
85 | {
86 | this.TestTearDown();
87 | }
88 |
89 | [Xunit.SkippableFactAttribute(DisplayName="User creates, reads, updates and deletes a student")]
90 | [Xunit.TraitAttribute("FeatureTitle", "StudentManagement")]
91 | [Xunit.TraitAttribute("Description", "User creates, reads, updates and deletes a student")]
92 | public void UserCreatesReadsUpdatesAndDeletesAStudent()
93 | {
94 | string[] tagsOfScenario = ((string[])(null));
95 | System.Collections.Specialized.OrderedDictionary argumentsOfScenario = new System.Collections.Specialized.OrderedDictionary();
96 | TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("User creates, reads, updates and deletes a student", null, tagsOfScenario, argumentsOfScenario, featureTags);
97 | #line 9
98 | this.ScenarioInitialize(scenarioInfo);
99 | #line hidden
100 | if ((TagHelper.ContainsIgnoreTag(tagsOfScenario) || TagHelper.ContainsIgnoreTag(featureTags)))
101 | {
102 | testRunner.SkipScenario();
103 | }
104 | else
105 | {
106 | this.ScenarioStart();
107 | #line 5
108 | this.FeatureBackground();
109 | #line hidden
110 | #line 10
111 | testRunner.Given("platform has \"0\" students", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given ");
112 | #line hidden
113 | TechTalk.SpecFlow.Table table1 = new TechTalk.SpecFlow.Table(new string[] {
114 | "FirstName",
115 | "LastName",
116 | "Email",
117 | "BirthDate",
118 | "GitHubUsername"});
119 | table1.AddRow(new string[] {
120 | "John",
121 | "Doe",
122 | "john@doe.com",
123 | "2004-01-01",
124 | "john-doe"});
125 | #line 11
126 | testRunner.When("user creates a student with following data by sending \'Create Student Command\' th" +
127 | "rough API", ((string)(null)), table1, "When ");
128 | #line hidden
129 | TechTalk.SpecFlow.Table table2 = new TechTalk.SpecFlow.Table(new string[] {
130 | "FirstName",
131 | "LastName",
132 | "Email",
133 | "BirthDate",
134 | "GitHubUsername"});
135 | table2.AddRow(new string[] {
136 | "John",
137 | "Doe",
138 | "john@doe.com",
139 | "2004-01-01",
140 | "john-doe"});
141 | #line 14
142 | testRunner.Then("user requests to get all the students and it must return \"1\" student with followi" +
143 | "ng data", ((string)(null)), table2, "Then ");
144 | #line hidden
145 | TechTalk.SpecFlow.Table table3 = new TechTalk.SpecFlow.Table(new string[] {
146 | "FirstName",
147 | "LastName",
148 | "Email",
149 | "BirthDate",
150 | "GitHubUsername"});
151 | table3.AddRow(new string[] {
152 | "John",
153 | "Doe",
154 | "john@gmail.com",
155 | "2004-01-01",
156 | "john-doe"});
157 | #line 17
158 | testRunner.When("user creates another student with following data by sending \'Create Student Comma" +
159 | "nd\' through API", ((string)(null)), table3, "When ");
160 | #line hidden
161 | #line 20
162 | testRunner.Then("user must get status code \"500\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
163 | #line hidden
164 | TechTalk.SpecFlow.Table table4 = new TechTalk.SpecFlow.Table(new string[] {
165 | "FirstName",
166 | "LastName",
167 | "Email",
168 | "BirthDate",
169 | "GitHubUsername"});
170 | table4.AddRow(new string[] {
171 | "Jane",
172 | "William",
173 | "jane@william.com",
174 | "2000-01-01",
175 | "jane-will"});
176 | #line 21
177 | testRunner.When("user updates student by email \"john@doe.com\" with following data", ((string)(null)), table4, "When ");
178 | #line hidden
179 | TechTalk.SpecFlow.Table table5 = new TechTalk.SpecFlow.Table(new string[] {
180 | "FirstName",
181 | "LastName",
182 | "Email",
183 | "BirthDate",
184 | "GitHubUsername"});
185 | table5.AddRow(new string[] {
186 | "Jane",
187 | "William",
188 | "jane@william.com",
189 | "2000-01-01",
190 | "jane-will"});
191 | #line 24
192 | testRunner.Then("user requests to get all the students and it must return \"1\" student", ((string)(null)), table5, "Then ");
193 | #line hidden
194 | #line 27
195 | testRunner.When("user deletes student by Email of \"jane@william.com\"", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
196 | #line hidden
197 | #line 28
198 | testRunner.Then("user requests to get all students and it must return \"0\" students", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
199 | #line hidden
200 | }
201 | this.ScenarioCleanup();
202 | }
203 |
204 | [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "3.9.0.0")]
205 | [System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
206 | public class FixtureData : System.IDisposable
207 | {
208 |
209 | public FixtureData()
210 | {
211 | StudentManagementFeature.FeatureSetup();
212 | }
213 |
214 | void System.IDisposable.Dispose()
215 | {
216 | StudentManagementFeature.FeatureTearDown();
217 | }
218 | }
219 | }
220 | }
221 | #pragma warning restore
222 | #endregion
223 |
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Acceptance/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Acceptance/Hooks/AcceptanceTestWebApplicationFactory.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.DependencyInjection;
2 | using Microsoft.AspNetCore.Mvc.Testing;
3 | using Microsoft.AspNetCore.TestHost;
4 | using Microsoft.EntityFrameworkCore;
5 | using Microsoft.AspNetCore.Hosting;
6 | using Infrastructure.DbContexts;
7 | using Testcontainers.PostgreSql;
8 | using TechTalk.SpecFlow;
9 |
10 | namespace WebAPI.Tests.Acceptance.Hooks;
11 |
12 | [Binding]
13 | public class AcceptanceTestWebApplicationFactory : WebApplicationFactory, IAsyncLifetime
14 | {
15 | private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder()
16 | .WithImage("postgres:latest")
17 | .WithDatabase("StudentManagementDB_Test")
18 | .WithUsername("postgres")
19 | .WithPassword("mysecretpassword")
20 | .Build();
21 |
22 | [BeforeScenario]
23 | public async Task InitializeAsync()
24 | {
25 | await _dbContainer.StartAsync();
26 |
27 | using (var scope = Services.CreateScope())
28 | {
29 | var scopedServices = scope.ServiceProvider;
30 | var cntx = scopedServices.GetRequiredService();
31 |
32 | await cntx.Database.EnsureCreatedAsync();
33 | }
34 | }
35 |
36 | [AfterScenario]
37 | public new async Task DisposeAsync()
38 | {
39 | await _dbContainer.StopAsync();
40 | }
41 |
42 | protected override void ConfigureWebHost(IWebHostBuilder builder)
43 | {
44 | builder.ConfigureTestServices(services =>
45 | {
46 | var descriptor = services.SingleOrDefault(s => s.ServiceType == typeof(DbContextOptions));
47 |
48 | if (descriptor is not null)
49 | {
50 | services.Remove(descriptor);
51 | }
52 |
53 | services.AddDbContext(options =>
54 | {
55 | options.UseNpgsql(_dbContainer.GetConnectionString());
56 | });
57 | });
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Acceptance/StepDefinitions/StudentManagementSteps.cs:
--------------------------------------------------------------------------------
1 | using WebAPI.Tests.Acceptance.Drivers;
2 | using WebAPI.Tests.Acceptance.Dtos;
3 | using TechTalk.SpecFlow;
4 |
5 | namespace WebAPI.Tests.Acceptance.StepDefinitions;
6 |
7 | [Binding]
8 | public class StudentManagementSteps
9 | {
10 | private readonly ScenarioContext _scenarioContext;
11 | private readonly StudentManagementDriver _driver;
12 | public StudentManagementSteps(ScenarioContext scenarioContext, StudentManagementDriver driver)
13 | {
14 | _scenarioContext = scenarioContext;
15 | _driver = driver;
16 | }
17 |
18 | [Given(@"platform has ""(.*)"" students")]
19 | public void GivenplatformHas0Students(string numberOfStudents)
20 | {
21 | List studentResponse = _driver.GetAllStudents();
22 | Assert.Equal(int.Parse(numberOfStudents), studentResponse?.Count);
23 | }
24 |
25 | [When(@"user creates a student with following data by sending 'Create Student Command' through API")]
26 | public void WhenUserCreatesAStudent(Table student)
27 | {
28 | HttpResponseMessage createResponse = _driver.CreateStudent(student);
29 | createResponse.EnsureSuccessStatusCode();
30 | }
31 |
32 | [Then(@"user requests to get all the students and it must return ""(.*)"" student with following data")]
33 | public void ThenUserGet1Student(string numberOfStudents, Table expectedStudent)
34 | {
35 | List studentResponse = _driver.GetAllStudents();
36 |
37 | Assert.Equal(int.Parse(numberOfStudents), studentResponse?.Count);
38 |
39 | var expectedStudentResponse = _driver.ParseCreateStudentData(expectedStudent);
40 | Assert.Equal(expectedStudentResponse.FirstName, studentResponse[0].FirstName);
41 | Assert.Equal(expectedStudentResponse.LastName, studentResponse[0].LastName);
42 | Assert.Equal(expectedStudentResponse.Email.Value, studentResponse[0].Email.Value);
43 | Assert.Equal(expectedStudentResponse.BirthDate, studentResponse[0].BirthDate);
44 | Assert.Equal(expectedStudentResponse.GitHubUsername, studentResponse[0].GitHubUsername);
45 | }
46 |
47 | [When(@"user creates another student with following data by sending 'Create Student Command' through API")]
48 | public void WhenUserCreatesAStudentWithSameInfo(Table student)
49 | {
50 | HttpResponseMessage createSameStudentResponse = _driver.CreateStudent(student);
51 | _scenarioContext.Add("createSameStudentResponse", createSameStudentResponse);
52 | }
53 |
54 | [Then(@"user must get status code ""(.*)""")]
55 | public void ThenUserMustGetInternalServerError(string statusCode)
56 | {
57 | var createSameStudentResponse = _scenarioContext.Get("createSameStudentResponse");
58 | Assert.Equal(int.Parse(statusCode), (int)createSameStudentResponse.StatusCode);
59 | }
60 |
61 | [When(@"user updates student by email ""(.*)"" with following data")]
62 | public void ThenUserUpdateStudent(string email, Table student)
63 | {
64 | List studentResponse = _driver.GetAllStudents();
65 | Assert.Equal(email, studentResponse[0].Email.Value);
66 |
67 | HttpResponseMessage updateResponse = _driver.UpdateStudent(studentResponse[0].Id, student);
68 | updateResponse.EnsureSuccessStatusCode();
69 | }
70 |
71 | [Then(@"user requests to get all the students and it must return ""(.*)"" student")]
72 | public void ThenUserCanRequestToGetStudents(string numberOfStudents, Table expectedStudent)
73 | {
74 | List studentResponse = _driver.GetAllStudents();
75 |
76 | Assert.Equal(int.Parse(numberOfStudents), studentResponse?.Count);
77 |
78 | var expectedStudentResponse = _driver.ParseCreateStudentData(expectedStudent);
79 | Assert.Equal(expectedStudentResponse.FirstName, studentResponse[0].FirstName);
80 | Assert.Equal(expectedStudentResponse.LastName, studentResponse[0].LastName);
81 | Assert.Equal(expectedStudentResponse.Email.Value, studentResponse[0].Email.Value);
82 | Assert.Equal(expectedStudentResponse.BirthDate, studentResponse[0].BirthDate);
83 | Assert.Equal(expectedStudentResponse.GitHubUsername, studentResponse[0].GitHubUsername);
84 | }
85 |
86 | [When(@"user deletes student by Email of ""(.*)""")]
87 | public void WhenUserDeleteTheStudent(string email)
88 | {
89 | List studentResponse = _driver.GetAllStudents();
90 | Assert.Equal(email, studentResponse[0].Email.Value);
91 |
92 | HttpResponseMessage deleteResponse = _driver.DeleteStudent(studentResponse[0].Id);
93 | deleteResponse.EnsureSuccessStatusCode();
94 | }
95 |
96 | [Then(@"user requests to get all students and it must return ""(.*)"" students")]
97 | public void ThenUserQueryToGetAllStudents(string numberOfStudents)
98 | {
99 | List studentResponse = _driver.GetAllStudents();
100 | Assert.Equal(int.Parse(numberOfStudents), studentResponse?.Count);
101 | }
102 | }
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Acceptance/WebAPI.Tests.Acceptance.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 | all
21 |
22 |
23 | runtime; build; native; contentfiles; analyzers; buildtransitive
24 | all
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Integration/BaseControllerTest.cs:
--------------------------------------------------------------------------------
1 | using Application.Tests.Integration;
2 |
3 | namespace WebAPI.Tests.Integration;
4 |
5 | public class BaseControllerTest : IClassFixture
6 | {
7 | protected readonly HttpClient _httpClient;
8 |
9 | protected BaseControllerTest(IntegrationTestWebApplicationFactory factory)
10 | {
11 | _httpClient = factory.CreateDefaultClient();
12 | }
13 | }
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Integration/ControllersTests/StudentsControllerTests.cs:
--------------------------------------------------------------------------------
1 | using Application.Tests.Integration;
2 | using Application.ResponseModels;
3 | using Application.RequestModels;
4 | using System.Net.Http.Json;
5 | using Domain.ValueObjects;
6 |
7 | namespace WebAPI.Tests.Integration.ControllersTests;
8 |
9 | public class StudentsControllerTests : BaseControllerTest
10 | {
11 | public StudentsControllerTests(IntegrationTestWebApplicationFactory factory) : base(factory)
12 | {
13 | }
14 |
15 | [Fact]
16 | public async Task GetAll_Returns_OK()
17 | {
18 | // Arrange
19 |
20 | // Act
21 | var response = await _httpClient.GetAsync("/api/students");
22 |
23 | // Assert
24 | response.EnsureSuccessStatusCode();
25 | }
26 |
27 | [Fact]
28 | public async Task Post_Returns_OK_With_StudentResponse()
29 | {
30 | // Arrange
31 | CreateStudentRequest request = new CreateStudentRequest(
32 | FirstName: "Max",
33 | LastName: "Mirzaee",
34 | Email: new Email("max@gmail.com"),
35 | BirthDate: new DateOnly(2006, 06, 01),
36 | GitHubUsername: "max-1");
37 |
38 | // Act
39 | var response = await _httpClient.PostAsJsonAsync("/api/students", request);
40 |
41 | // Assert
42 | response.EnsureSuccessStatusCode();
43 |
44 | var studentResponse = await response.Content.ReadFromJsonAsync();
45 | Assert.Equal(request.FirstName, studentResponse.FirstName);
46 | Assert.Equal(request.LastName, studentResponse.LastName);
47 | Assert.Equal(request.Email.Value, studentResponse.Email.Value);
48 | Assert.Equal(request.BirthDate, studentResponse.BirthDate);
49 | Assert.Equal(request.GitHubUsername, studentResponse.GitHubUsername);
50 | }
51 |
52 | [Fact]
53 | public async void Put_Returns_Ok_With_StudentResponse()
54 | {
55 | // Arrange
56 | // Create a new student
57 | CreateStudentRequest createRequest = new CreateStudentRequest(
58 | FirstName: "Diana",
59 | LastName: "Moradi",
60 | Email: new Email("diana@gmail.com"),
61 | BirthDate: new DateOnly(2000, 1, 1),
62 | GitHubUsername: "diana"
63 | );
64 |
65 | var postResponse = await _httpClient.PostAsJsonAsync("/api/students", createRequest);
66 | var addedStudent = await postResponse.Content.ReadFromJsonAsync();
67 |
68 | UpdateStudentRequest updateRequest = new UpdateStudentRequest(
69 | Id: addedStudent.Id,
70 | FirstName: "Diana",
71 | LastName: "Rasouli",
72 | Email: new Email("dia@gmail.com"),
73 | BirthDate: new DateOnly(1900, 1, 1),
74 | GitHubUsername: "diana-900");
75 |
76 | // Act
77 | var updateResponse = await _httpClient.PutAsJsonAsync("/api/students/" + addedStudent.Id, updateRequest);
78 |
79 | // Assert
80 | updateResponse.EnsureSuccessStatusCode();
81 | postResponse.EnsureSuccessStatusCode();
82 |
83 | var updatedStudent = await updateResponse.Content.ReadFromJsonAsync();
84 |
85 | Assert.Equal(updateRequest.FirstName, updatedStudent.FirstName);
86 | Assert.Equal(updateRequest.LastName, updatedStudent.LastName);
87 | Assert.Equal(updateRequest.Email.Value, updatedStudent.Email.Value);
88 | Assert.Equal(updateRequest.BirthDate, updatedStudent.BirthDate);
89 | Assert.Equal(updateRequest.GitHubUsername, updatedStudent.GitHubUsername);
90 | }
91 |
92 | [Fact]
93 | public async Task Delete_With_Valid_StudentId_Returns_OK()
94 | {
95 | // Arrange
96 | // Create a new student
97 | CreateStudentRequest request = new CreateStudentRequest(
98 | FirstName: "Arsh",
99 | LastName: "Mirzaee",
100 | Email: new Email("arsh@gmail.com"),
101 | BirthDate: new DateOnly(1984, 06, 01),
102 | GitHubUsername: "arsh84");
103 |
104 | var postResponse = await _httpClient.PostAsJsonAsync("/api/students", request);
105 | StudentResponse addedStudent = await postResponse.Content.ReadFromJsonAsync();
106 |
107 | // Act
108 | var deleteResponse = await _httpClient.DeleteAsync("/api/students/" + addedStudent.Id);
109 |
110 | // Assert
111 | postResponse.EnsureSuccessStatusCode();
112 | deleteResponse.EnsureSuccessStatusCode();
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Integration/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/server/tests/WebAPI.Tests.Integration/WebAPI.Tests.Integration.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------