6 | {
7 | public StronglyTypedEmployeeServiceTestData_FromFile()
8 | {
9 | var testDataLines = File.ReadAllLines("TestData/EmployeeServiceTestData.csv");
10 |
11 | foreach (var line in testDataLines)
12 | {
13 | // split the string
14 | var splitString = line.Split(',');
15 | // try parsing
16 | if (int.TryParse(splitString[0], out int raise)
17 | && bool.TryParse(splitString[1], out bool minimumRaiseGiven))
18 | {
19 | // add test data
20 | Add(raise, minimumRaiseGiven);
21 | }
22 | }
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/EmployeeManagement.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | true
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | all
18 | runtime; build; native; contentfiles; analyzers; buildtransitive
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Views/Shared/Error.cshtml:
--------------------------------------------------------------------------------
1 | @model ErrorViewModel
2 | @{
3 | ViewData["Title"] = "Error";
4 | }
5 |
6 | Error.
7 | An error occurred while processing your request.
8 |
9 | @if (Model?.ShowRequestId ?? false)
10 | {
11 |
12 | Request ID: @Model?.RequestId
13 |
14 | }
15 |
16 | Development Mode
17 |
18 | Swapping to Development environment will display more detailed information about the error that occurred.
19 |
20 |
21 | The Development environment shouldn't be enabled for deployed applications.
22 | It can result in displaying sensitive information from exceptions to end users.
23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
24 | and restarting the app.
25 |
26 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Views/Shared/Error.cshtml:
--------------------------------------------------------------------------------
1 | @model ErrorViewModel
2 | @{
3 | ViewData["Title"] = "Error";
4 | }
5 |
6 | Error.
7 | An error occurred while processing your request.
8 |
9 | @if (Model?.ShowRequestId ?? false)
10 | {
11 |
12 | Request ID: @Model?.RequestId
13 |
14 | }
15 |
16 | Development Mode
17 |
18 | Swapping to Development environment will display more detailed information about the error that occurred.
19 |
20 |
21 | The Development environment shouldn't be enabled for deployed applications.
22 | It can result in displaying sensitive information from exceptions to end users.
23 | For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development
24 | and restarting the app.
25 |
26 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/DataAccess/Entities/Employee.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.ComponentModel.DataAnnotations.Schema;
3 |
4 | namespace EmployeeManagement.DataAccess.Entities
5 | {
6 | ///
7 | /// Base class for all employees
8 | ///
9 | public abstract class Employee
10 | {
11 | [Key]
12 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
13 | public Guid Id { get; set; }
14 |
15 | [Required]
16 | [MaxLength(100)]
17 | public string FirstName { get; set; }
18 |
19 | [Required]
20 | [MaxLength(100)]
21 | public string LastName { get; set; }
22 |
23 | [NotMapped]
24 | public string FullName
25 | {
26 | get { return $"{FirstName} {LastName}"; }
27 | }
28 |
29 | public Employee(
30 | string firstName,
31 | string lastName)
32 | {
33 | FirstName = firstName;
34 | LastName = lastName;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/DataAccess/Entities/Employee.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.ComponentModel.DataAnnotations.Schema;
3 |
4 | namespace EmployeeManagement.DataAccess.Entities
5 | {
6 | ///
7 | /// Base class for all employees
8 | ///
9 | public abstract class Employee
10 | {
11 | [Key]
12 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
13 | public Guid Id { get; set; }
14 |
15 | [Required]
16 | [MaxLength(100)]
17 | public string FirstName { get; set; }
18 |
19 | [Required]
20 | [MaxLength(100)]
21 | public string LastName { get; set; }
22 |
23 | [NotMapped]
24 | public string FullName
25 | {
26 | get { return $"{FirstName} {LastName}"; }
27 | }
28 |
29 | public Employee(
30 | string firstName,
31 | string lastName)
32 | {
33 | FirstName = firstName;
34 | LastName = lastName;
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Finished solution/CompanyFramework.Test/CompanyFramework.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0;net6.0
5 | enable
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 | all
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Additional files used during course/CompanyFramework.Test/CompanyFramework.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0;net5.0
5 | enable
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 | runtime; build; native; contentfiles; analyzers; buildtransitive
14 | all
15 |
16 |
17 | runtime; build; native; contentfiles; analyzers; buildtransitive
18 | all
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Views/Shared/_Layout.cshtml.css:
--------------------------------------------------------------------------------
1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
2 | for details on configuring this project to bundle and minify static web assets. */
3 |
4 | a.navbar-brand {
5 | white-space: normal;
6 | text-align: center;
7 | word-break: break-all;
8 | }
9 |
10 | a {
11 | color: #0077cc;
12 | }
13 |
14 | .btn-primary {
15 | color: #fff;
16 | background-color: #1b6ec2;
17 | border-color: #1861ac;
18 | }
19 |
20 | .nav-pills .nav-link.active, .nav-pills .show > .nav-link {
21 | color: #fff;
22 | background-color: #1b6ec2;
23 | border-color: #1861ac;
24 | }
25 |
26 | .border-top {
27 | border-top: 1px solid #e5e5e5;
28 | }
29 | .border-bottom {
30 | border-bottom: 1px solid #e5e5e5;
31 | }
32 |
33 | .box-shadow {
34 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
35 | }
36 |
37 | button.accept-policy {
38 | font-size: 1rem;
39 | line-height: inherit;
40 | }
41 |
42 | .footer {
43 | position: absolute;
44 | bottom: 0;
45 | width: 100%;
46 | white-space: nowrap;
47 | line-height: 60px;
48 | }
49 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Views/Shared/_Layout.cshtml.css:
--------------------------------------------------------------------------------
1 | /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification
2 | for details on configuring this project to bundle and minify static web assets. */
3 |
4 | a.navbar-brand {
5 | white-space: normal;
6 | text-align: center;
7 | word-break: break-all;
8 | }
9 |
10 | a {
11 | color: #0077cc;
12 | }
13 |
14 | .btn-primary {
15 | color: #fff;
16 | background-color: #1b6ec2;
17 | border-color: #1861ac;
18 | }
19 |
20 | .nav-pills .nav-link.active, .nav-pills .show > .nav-link {
21 | color: #fff;
22 | background-color: #1b6ec2;
23 | border-color: #1861ac;
24 | }
25 |
26 | .border-top {
27 | border-top: 1px solid #e5e5e5;
28 | }
29 | .border-bottom {
30 | border-bottom: 1px solid #e5e5e5;
31 | }
32 |
33 | .box-shadow {
34 | box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05);
35 | }
36 |
37 | button.accept-policy {
38 | font-size: 1rem;
39 | line-height: inherit;
40 | }
41 |
42 | .footer {
43 | position: absolute;
44 | bottom: 0;
45 | width: 100%;
46 | white-space: nowrap;
47 | line-height: 60px;
48 | }
49 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Kevin Dockx
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 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/Fixtures/EmployeeServiceFixture.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Business;
2 | using EmployeeManagement.DataAccess.Services;
3 | using EmployeeManagement.Services.Test;
4 | using System;
5 | using System.Collections.Generic;
6 | using System.Linq;
7 | using System.Text;
8 | using System.Threading.Tasks;
9 |
10 | namespace EmployeeManagement.Test.Fixtures
11 | {
12 | public class EmployeeServiceFixture : IDisposable
13 | {
14 | public IEmployeeManagementRepository EmployeeManagementTestDataRepository
15 | { get; }
16 | public EmployeeService EmployeeService
17 | { get; }
18 |
19 | public EmployeeServiceFixture()
20 | {
21 | EmployeeManagementTestDataRepository =
22 | new EmployeeManagementTestDataRepository();
23 | EmployeeService = new EmployeeService(
24 | EmployeeManagementTestDataRepository,
25 | new EmployeeFactory());
26 | }
27 |
28 | public void Dispose()
29 | {
30 | // clean up the setup code, if required
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Business/IEmployeeService.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Business.EventArguments;
2 | using EmployeeManagement.DataAccess.Entities;
3 |
4 | namespace EmployeeManagement.Business
5 | {
6 | public interface IEmployeeService
7 | {
8 | event EventHandler? EmployeeIsAbsent;
9 | Task AddInternalEmployeeAsync(InternalEmployee internalEmployee);
10 | Task AttendCourseAsync(InternalEmployee employee, Course attendedCourse);
11 | ExternalEmployee CreateExternalEmployee(string firstName, string lastName, string company);
12 | InternalEmployee CreateInternalEmployee(string firstName, string lastName);
13 | Task CreateInternalEmployeeAsync(string firstName, string lastName);
14 | InternalEmployee? FetchInternalEmployee(Guid employeeId);
15 | Task FetchInternalEmployeeAsync(Guid employeeId);
16 | Task> FetchInternalEmployeesAsync();
17 | Task GiveMinimumRaiseAsync(InternalEmployee employee);
18 | Task GiveRaiseAsync(InternalEmployee employee, int raise);
19 | void NotifyOfAbsence(Employee employee);
20 | }
21 | }
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/wwwroot/lib/jquery-validation/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright Jörn Zaefferer
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/wwwroot/lib/jquery-validation/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright Jörn Zaefferer
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/ActionFilters/CheckShowStatisticsHeader.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Microsoft.AspNetCore.Mvc.Filters;
3 |
4 | namespace EmployeeManagement.ActionFilters
5 | {
6 | public class CheckShowStatisticsHeader : ActionFilterAttribute
7 | {
8 | public override void OnActionExecuting(ActionExecutingContext context)
9 | {
10 | // if the ShowStatistics header is missing or set to false,
11 | // a BadRequest must be returned.
12 | if (!context.HttpContext.Request.Headers.ContainsKey("ShowStatistics"))
13 | {
14 | context.Result = new BadRequestResult();
15 | }
16 |
17 | // get the ShowStatistics header
18 | if (!bool.TryParse(
19 | context.HttpContext.Request.Headers["ShowStatistics"].ToString(),
20 | out bool showStatisticsValue))
21 | {
22 | context.Result = new BadRequestResult();
23 | }
24 |
25 | // check the value
26 | if (!showStatisticsValue)
27 | {
28 | context.Result = new BadRequestResult();
29 | }
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/EmployeeManagementSecurityHeadersMiddlewareTests.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Middleware;
2 | using Microsoft.AspNetCore.Http;
3 | using Xunit;
4 |
5 | namespace EmployeeManagement.Test
6 | {
7 | public class EmployeeManagementSecurityHeadersMiddlewareTests
8 | {
9 | [Fact]
10 | public async Task InvokeAsync_Invoke_SetsExpectedResponseHeaders()
11 | {
12 | // Arrange
13 | var httpContext = new DefaultHttpContext();
14 | RequestDelegate next = (HttpContext httpContext) => Task.CompletedTask;
15 | var middleware = new EmployeeManagementSecurityHeadersMiddleware(next);
16 |
17 | // Act
18 | await middleware.InvokeAsync(httpContext);
19 |
20 | // Assert
21 | var cspHeader = httpContext
22 | .Response.Headers["Content-Security-Policy"].ToString();
23 | var xContentTypeOptionsHeader = httpContext
24 | .Response.Headers["X-Content-Type-Options"].ToString();
25 |
26 | Assert.Equal("default-src 'self';frame-ancestors 'none';", cspHeader);
27 | Assert.Equal("nosniff", xContentTypeOptionsHeader);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Business/IEmployeeService.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Business.EventArguments;
2 | using EmployeeManagement.DataAccess.Entities;
3 |
4 | namespace EmployeeManagement.Business
5 | {
6 | public interface IEmployeeService
7 | {
8 | event EventHandler? EmployeeIsAbsent;
9 | Task AddInternalEmployeeAsync(InternalEmployee internalEmployee);
10 | Task AttendCourseAsync(InternalEmployee employee, Course attendedCourse);
11 | ExternalEmployee CreateExternalEmployee(string firstName,
12 | string lastName, string company);
13 | InternalEmployee CreateInternalEmployee(string firstName, string lastName);
14 | Task CreateInternalEmployeeAsync(string firstName,
15 | string lastName);
16 | InternalEmployee? FetchInternalEmployee(Guid employeeId);
17 | Task FetchInternalEmployeeAsync(Guid employeeId);
18 | Task> FetchInternalEmployeesAsync();
19 | Task GiveMinimumRaiseAsync(InternalEmployee employee);
20 | Task GiveRaiseAsync(InternalEmployee employee, int raise);
21 | void NotifyOfAbsence(Employee employee);
22 | }
23 | }
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/ActionFilters/CheckShowStatisticsHeader.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.AspNetCore.Mvc;
2 | using Microsoft.AspNetCore.Mvc.Filters;
3 |
4 | namespace EmployeeManagement.ActionFilters
5 | {
6 | public class CheckShowStatisticsHeader : ActionFilterAttribute
7 | {
8 | public override void OnActionExecuting(ActionExecutingContext context)
9 | {
10 | // if the ShowStatistics header is missing or set to false,
11 | // a BadRequest must be returned.
12 | if (!context.HttpContext.Request.Headers
13 | .ContainsKey("ShowStatistics"))
14 | {
15 | context.Result = new BadRequestResult();
16 | }
17 |
18 | // get the ShowStatistics header
19 | if (!bool.TryParse(
20 | context.HttpContext.Request.Headers["ShowStatistics"].ToString(),
21 | out bool showStatisticsValue))
22 | {
23 | context.Result = new BadRequestResult();
24 | }
25 |
26 | // check the value
27 | if (!showStatisticsValue)
28 | {
29 | context.Result = new BadRequestResult();
30 | }
31 | }
32 | }
33 | }
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/wwwroot/lib/bootstrap/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2011-2021 Twitter, Inc.
4 | Copyright (c) 2011-2021 The Bootstrap Authors
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/wwwroot/lib/bootstrap/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2011-2021 Twitter, Inc.
4 | Copyright (c) 2011-2021 The Bootstrap Authors
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in
14 | all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/DataAccess/Entities/InternalEmployee.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.ComponentModel.DataAnnotations.Schema;
3 |
4 | namespace EmployeeManagement.DataAccess.Entities
5 | {
6 | public class InternalEmployee : Employee
7 | {
8 | [Required]
9 | public int YearsInService { get; set; }
10 |
11 | [NotMapped]
12 | public decimal SuggestedBonus { get; set; }
13 |
14 | [Required]
15 | public decimal Salary { get; set; }
16 |
17 | [Required]
18 | public bool MinimumRaiseGiven { get; set; }
19 |
20 | public List AttendedCourses { get; set; } = new List();
21 |
22 | [Required]
23 | public int JobLevel { get; set; }
24 |
25 | public InternalEmployee(
26 | string firstName,
27 | string lastName,
28 | int yearsInService,
29 | decimal salary,
30 | bool minimumRaiseGiven,
31 | int jobLevel)
32 | : base(firstName, lastName)
33 | {
34 | YearsInService = yearsInService;
35 | Salary = salary;
36 | MinimumRaiseGiven = minimumRaiseGiven;
37 | JobLevel = jobLevel;
38 | }
39 |
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/DataAccess/Entities/InternalEmployee.cs:
--------------------------------------------------------------------------------
1 | using System.ComponentModel.DataAnnotations;
2 | using System.ComponentModel.DataAnnotations.Schema;
3 |
4 | namespace EmployeeManagement.DataAccess.Entities
5 | {
6 | public class InternalEmployee : Employee
7 | {
8 | [Required]
9 | public int YearsInService { get; set; }
10 |
11 | [NotMapped]
12 | public decimal SuggestedBonus { get; set; }
13 |
14 | [Required]
15 | public decimal Salary { get; set; }
16 |
17 | [Required]
18 | public bool MinimumRaiseGiven { get; set; }
19 |
20 | public List AttendedCourses { get; set; } = new List();
21 |
22 | [Required]
23 | public int JobLevel { get; set; }
24 |
25 | public InternalEmployee(
26 | string firstName,
27 | string lastName,
28 | int yearsInService,
29 | decimal salary,
30 | bool minimumRaiseGiven,
31 | int jobLevel)
32 | : base(firstName, lastName)
33 | {
34 | YearsInService = yearsInService;
35 | Salary = salary;
36 | MinimumRaiseGiven = minimumRaiseGiven;
37 | JobLevel = jobLevel;
38 | }
39 |
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/ServiceCollectionTests.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.Services;
2 | using Microsoft.Extensions.Configuration;
3 | using Microsoft.Extensions.DependencyInjection;
4 | using Xunit;
5 |
6 | namespace EmployeeManagement.Test
7 | {
8 | public class ServiceCollectionTests
9 | {
10 | [Fact]
11 | public void RegisterDataServices_Execute_DataServicesAreRegistered()
12 | {
13 | // Arrange
14 | var serviceCollection = new ServiceCollection();
15 | var configuration = new ConfigurationBuilder()
16 | .AddInMemoryCollection(
17 | new Dictionary {
18 | {"ConnectionStrings:EmployeeManagementDB", "AnyValueWillDo"}})
19 | .Build();
20 |
21 | // Act
22 | serviceCollection.RegisterDataServices(configuration);
23 | var serviceProvider = serviceCollection.BuildServiceProvider();
24 |
25 | // Assert
26 | Assert.NotNull(
27 | serviceProvider.GetService());
28 | Assert.IsType(
29 | serviceProvider.GetService());
30 |
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/ServiceRegistrationExtensions.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Business;
2 | using EmployeeManagement.DataAccess.DbContexts;
3 | using EmployeeManagement.DataAccess.Services;
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace EmployeeManagement
7 | {
8 | public static class ServiceRegistrationExtensions
9 | {
10 | public static IServiceCollection RegisterBusinessServices(
11 | this IServiceCollection services)
12 | {
13 | services.AddScoped();
14 | services.AddScoped();
15 | services.AddScoped();
16 | return services;
17 | }
18 |
19 | public static IServiceCollection RegisterDataServices(
20 | this IServiceCollection services, IConfiguration configuration)
21 | {
22 | // add the DbContext
23 | services.AddDbContext(options =>
24 | options.UseSqlite(configuration.GetConnectionString("EmployeeManagementDB")));
25 |
26 | // register the repository
27 | services.AddScoped();
28 | return services;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/ServiceRegistrationExtensions.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Business;
2 | using EmployeeManagement.DataAccess.DbContexts;
3 | using EmployeeManagement.DataAccess.Services;
4 | using Microsoft.EntityFrameworkCore;
5 |
6 | namespace EmployeeManagement
7 | {
8 | public static class ServiceRegistrationExtensions
9 | {
10 | public static IServiceCollection RegisterBusinessServices(
11 | this IServiceCollection services)
12 | {
13 | services.AddScoped();
14 | services.AddScoped();
15 | services.AddScoped();
16 | return services;
17 | }
18 |
19 | public static IServiceCollection RegisterDataServices(
20 | this IServiceCollection services, IConfiguration configuration)
21 | {
22 | // add the DbContext
23 | services.AddDbContext(options =>
24 | options.UseSqlite(configuration.GetConnectionString("EmployeeManagementDB")));
25 |
26 | // register the repository
27 | services.AddScoped();
28 | return services;
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/CheckShowStatisticsHeaderTests.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.ActionFilters;
2 | using Microsoft.AspNetCore.Http;
3 | using Microsoft.AspNetCore.Mvc;
4 | using Microsoft.AspNetCore.Mvc.Filters;
5 | using Xunit;
6 |
7 | namespace EmployeeManagement.Test
8 | {
9 | public class CheckShowStatisticsHeaderTests
10 | {
11 | [Fact]
12 | public void OnActionExecuting_InvokeWithoutShowStatisticsHeader_ReturnsBadRequest()
13 | {
14 | // Arrange
15 | var checkShowStatisticsHeaderActionFilter =
16 | new CheckShowStatisticsHeader();
17 |
18 | var httpContext = new DefaultHttpContext();
19 |
20 | var actionContext = new ActionContext(httpContext, new(), new(), new());
21 |
22 | var actionExecutingContext = new ActionExecutingContext(
23 | actionContext,
24 | new List(),
25 | new Dictionary(),
26 | controller: null);
27 |
28 | // Act
29 | checkShowStatisticsHeaderActionFilter
30 | .OnActionExecuting(actionExecutingContext);
31 |
32 | // Assert
33 | Assert.IsType(actionExecutingContext.Result);
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Views/InternalEmployee/InternalEmployeeDetails.cshtml:
--------------------------------------------------------------------------------
1 | @model EmployeeManagement.ViewModels.InternalEmployeeDetailViewModel
2 | @{
3 | ViewData["Title"] = "Internal Employee Details";
4 | }
5 |
6 |
7 |
Name:
8 |
@Model?.FullName
9 |
10 |
11 |
Salary:
12 |
@Model?.Salary
13 |
14 |
15 |
Years in service:
16 |
@Model?.YearsInService
17 |
18 |
19 |
Suggested bonus:
20 |
@Model?.SuggestedBonus
21 |
22 |
23 |
Current job level:
24 |
@Model?.JobLevel
25 |
26 |
27 |
33 |
34 |
35 |
@ViewBag.PromotionRequestMessage
36 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Views/InternalEmployee/InternalEmployeeDetails.cshtml:
--------------------------------------------------------------------------------
1 | @model EmployeeManagement.ViewModels.InternalEmployeeDetailViewModel
2 | @{
3 | ViewData["Title"] = "Internal Employee Details";
4 | }
5 |
6 |
7 |
Name:
8 |
@Model?.FullName
9 |
10 |
11 |
Salary:
12 |
@Model?.Salary
13 |
14 |
15 |
Years in service:
16 |
@Model?.YearsInService
17 |
18 |
19 |
Suggested bonus:
20 |
@Model?.SuggestedBonus
21 |
22 |
23 |
Current job level:
24 |
@Model?.JobLevel
25 |
26 |
27 |
33 |
34 |
35 |
@ViewBag.PromotionRequestMessage
36 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/EmployeeServiceTestsWithAspNetCoreDI.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Test.Fixtures;
2 | using Xunit;
3 |
4 | namespace EmployeeManagement.Test
5 | {
6 | public class EmployeeServiceTestsWithAspNetCoreDI
7 | : IClassFixture
8 | {
9 | private readonly EmployeeServiceWithAspNetCoreDIFixture
10 | _employeeServiceFixture;
11 |
12 | public EmployeeServiceTestsWithAspNetCoreDI(
13 | EmployeeServiceWithAspNetCoreDIFixture employeeServiceFixture)
14 | {
15 | _employeeServiceFixture = employeeServiceFixture;
16 | }
17 |
18 | [Fact]
19 | public void CreateInternalEmployee_InternalEmployeeCreated_MustHaveAttendedFirstObligatoryCourse_WithObject()
20 | {
21 | // Arrange
22 |
23 | var obligatoryCourse = _employeeServiceFixture
24 | .EmployeeManagementTestDataRepository
25 | .GetCourse(Guid.Parse("37e03ca7-c730-4351-834c-b66f280cdb01"));
26 |
27 | // Act
28 | var internalEmployee = _employeeServiceFixture
29 | .EmployeeService.CreateInternalEmployee("Brooklyn", "Cannon");
30 |
31 | // Assert
32 | Assert.Contains(obligatoryCourse, internalEmployee.AttendedCourses);
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/EmployeeManagement.Test.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net8.0
5 | enable
6 | enable
7 | false
8 | true
9 |
10 |
11 |
12 |
13 |
14 |
15 | runtime; build; native; contentfiles; analyzers; buildtransitive
16 | all
17 |
18 |
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 | all
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Always
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Program.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement;
2 | using EmployeeManagement.Business;
3 | using EmployeeManagement.DataAccess.DbContexts;
4 | using EmployeeManagement.DataAccess.Services;
5 | using EmployeeManagement.Middleware;
6 | using Microsoft.EntityFrameworkCore;
7 |
8 | var builder = WebApplication.CreateBuilder(args);
9 |
10 | // Add services to the container.
11 | builder.Services.AddControllersWithViews();
12 |
13 |
14 |
15 | // add HttpClient support
16 | builder.Services.AddHttpClient("TopLevelManagementAPIClient");
17 |
18 | // add AutoMapper for mapping between entities and viewmodels
19 | builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
20 |
21 | // add support for Sessions (requires a store)
22 | builder.Services.AddDistributedMemoryCache();
23 | builder.Services.AddSession();
24 |
25 | // add other services
26 | builder.Services.RegisterBusinessServices();
27 | builder.Services.RegisterDataServices(builder.Configuration);
28 |
29 | var app = builder.Build();
30 |
31 | // Configure the HTTP request pipeline.
32 |
33 | // custom middleware
34 | app.UseMiddleware();
35 |
36 | app.UseStaticFiles();
37 |
38 | app.UseRouting();
39 |
40 | app.UseAuthorization();
41 |
42 | app.UseSession();
43 |
44 | app.MapControllerRoute(
45 | name: "default",
46 | pattern: "{controller=EmployeeOverview}/{action=Index}/{id?}");
47 |
48 | app.Run();
49 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/HttpMessageHandlers/TestablePromotionElibilityHandler.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Business;
2 | using System.Text;
3 | using System.Text.Json;
4 |
5 | namespace EmployeeManagement.Test.HttpMessageHandlers
6 | {
7 | public class TestablePromotionEligibilityHandler : HttpMessageHandler
8 | {
9 | private readonly bool _isEligibleForPromotion;
10 |
11 | public TestablePromotionEligibilityHandler(bool isEligibleForPromotion)
12 | {
13 | _isEligibleForPromotion = isEligibleForPromotion;
14 | }
15 | protected override Task SendAsync(
16 | HttpRequestMessage request, CancellationToken cancellationToken)
17 | {
18 | var promotionEligibility = new PromotionEligibility()
19 | {
20 | EligibleForPromotion = _isEligibleForPromotion
21 | };
22 |
23 | var response = new HttpResponseMessage(System.Net.HttpStatusCode.OK)
24 | {
25 | Content = new StringContent(
26 | JsonSerializer.Serialize(promotionEligibility,
27 | new JsonSerializerOptions
28 | {
29 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase
30 | }),
31 | Encoding.ASCII,
32 | "application/json")
33 | };
34 | return Task.FromResult(response);
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Program.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement;
2 | using EmployeeManagement.Business;
3 | using EmployeeManagement.DataAccess.DbContexts;
4 | using EmployeeManagement.DataAccess.Services;
5 | using EmployeeManagement.Middleware;
6 | using Microsoft.AspNetCore.Authentication.Cookies;
7 | using Microsoft.EntityFrameworkCore;
8 |
9 | var builder = WebApplication.CreateBuilder(args);
10 |
11 | // Add services to the container.
12 | builder.Services.AddControllersWithViews();
13 |
14 |
15 |
16 | // add HttpClient support
17 | builder.Services.AddHttpClient("TopLevelManagementAPIClient");
18 |
19 | // add AutoMapper for mapping between entities and viewmodels
20 | builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies());
21 |
22 | // add support for Sessions (requires a store)
23 | builder.Services.AddDistributedMemoryCache();
24 | builder.Services.AddSession();
25 |
26 | // add other services
27 | builder.Services.RegisterBusinessServices();
28 | builder.Services.RegisterDataServices(builder.Configuration);
29 |
30 | builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
31 | .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme);
32 |
33 |
34 | var app = builder.Build();
35 |
36 | // Configure the HTTP request pipeline.
37 |
38 | // custom middleware
39 | app.UseMiddleware();
40 |
41 | app.UseStaticFiles();
42 |
43 | app.UseRouting();
44 |
45 | app.UseAuthentication();
46 |
47 | app.UseAuthorization();
48 |
49 | app.UseSession();
50 |
51 | app.MapControllerRoute(
52 | name: "default",
53 | pattern: "{controller=EmployeeOverview}/{action=Index}/{id?}");
54 |
55 | app.Run();
56 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement.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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EmployeeManagement", "EmployeeManagement\EmployeeManagement.csproj", "{A9D381CA-BB06-4106-AA5F-CD2FC0D436F6}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TopLevelManagement", "TopLevelManagement\TopLevelManagement.csproj", "{6807FF4E-072D-4AE3-81BC-67D75FE791DC}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {A9D381CA-BB06-4106-AA5F-CD2FC0D436F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {A9D381CA-BB06-4106-AA5F-CD2FC0D436F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {A9D381CA-BB06-4106-AA5F-CD2FC0D436F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {A9D381CA-BB06-4106-AA5F-CD2FC0D436F6}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {6807FF4E-072D-4AE3-81BC-67D75FE791DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {6807FF4E-072D-4AE3-81BC-67D75FE791DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {6807FF4E-072D-4AE3-81BC-67D75FE791DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {6807FF4E-072D-4AE3-81BC-67D75FE791DC}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {4771D570-E205-4450-92AC-5F5F9D2C9729}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/wwwroot/lib/jquery/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright JS Foundation and other contributors, https://js.foundation/
2 |
3 | This software consists of voluntary contributions made by many
4 | individuals. For exact contribution history, see the revision history
5 | available at https://github.com/jquery/jquery
6 |
7 | The following license applies to all parts of this software except as
8 | documented below:
9 |
10 | ====
11 |
12 | Permission is hereby granted, free of charge, to any person obtaining
13 | a copy of this software and associated documentation files (the
14 | "Software"), to deal in the Software without restriction, including
15 | without limitation the rights to use, copy, modify, merge, publish,
16 | distribute, sublicense, and/or sell copies of the Software, and to
17 | permit persons to whom the Software is furnished to do so, subject to
18 | the following conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 |
31 | ====
32 |
33 | All files located in the node_modules and external directories are
34 | externally maintained libraries used by this software which have their
35 | own licenses; we recommend you read them, as their terms may differ from
36 | the terms above.
37 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/wwwroot/lib/jquery/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright JS Foundation and other contributors, https://js.foundation/
2 |
3 | This software consists of voluntary contributions made by many
4 | individuals. For exact contribution history, see the revision history
5 | available at https://github.com/jquery/jquery
6 |
7 | The following license applies to all parts of this software except as
8 | documented below:
9 |
10 | ====
11 |
12 | Permission is hereby granted, free of charge, to any person obtaining
13 | a copy of this software and associated documentation files (the
14 | "Software"), to deal in the Software without restriction, including
15 | without limitation the rights to use, copy, modify, merge, publish,
16 | distribute, sublicense, and/or sell copies of the Software, and to
17 | permit persons to whom the Software is furnished to do so, subject to
18 | the following conditions:
19 |
20 | The above copyright notice and this permission notice shall be
21 | included in all copies or substantial portions of the Software.
22 |
23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
30 |
31 | ====
32 |
33 | All files located in the node_modules and external directories are
34 | externally maintained libraries used by this software which have their
35 | own licenses; we recommend you read them, as their terms may differ from
36 | the terms above.
37 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Business/EmployeeFactory.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.Entities;
2 |
3 | namespace EmployeeManagement.Business
4 | {
5 | ///
6 | /// Factory for creation employees
7 | ///
8 | public class EmployeeFactory
9 | {
10 | ///
11 | /// Create an employee
12 | ///
13 | public virtual Employee CreateEmployee(string firstName,
14 | string lastName,
15 | string? company = null,
16 | bool isExternal = false)
17 | {
18 | if (string.IsNullOrEmpty(firstName))
19 | {
20 | throw new ArgumentException($"'{nameof(firstName)}' cannot be null or empty.",
21 | nameof(firstName));
22 | }
23 |
24 | if (string.IsNullOrEmpty(lastName))
25 | {
26 | throw new ArgumentException($"'{nameof(lastName)}' cannot be null or empty.",
27 | nameof(lastName));
28 | }
29 |
30 | if (company == null && isExternal)
31 | {
32 | throw new ArgumentException($"'{nameof(company)}' cannot be null or empty when the employee is external.",
33 | nameof(company));
34 | }
35 |
36 | if (isExternal)
37 | {
38 | // we know company won't be null here due to the check above, so
39 | // we can use the null-forgiving operator to notify the compiler of this
40 | return new ExternalEmployee(firstName, lastName, company = null!);
41 | }
42 |
43 | // create a new employee with default values
44 | return new InternalEmployee(firstName, lastName, 0, 2500, false, 1);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Business/EmployeeFactory.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.Entities;
2 |
3 | namespace EmployeeManagement.Business
4 | {
5 | ///
6 | /// Factory for creation employees
7 | ///
8 | public class EmployeeFactory
9 | {
10 | ///
11 | /// Create an employee
12 | ///
13 | public virtual Employee CreateEmployee(string firstName,
14 | string lastName,
15 | string? company = null,
16 | bool isExternal = false)
17 | {
18 | if (string.IsNullOrEmpty(firstName))
19 | {
20 | throw new ArgumentException($"'{nameof(firstName)}' cannot be null or empty.",
21 | nameof(firstName));
22 | }
23 |
24 | if (string.IsNullOrEmpty(lastName))
25 | {
26 | throw new ArgumentException($"'{nameof(lastName)}' cannot be null or empty.",
27 | nameof(lastName));
28 | }
29 |
30 | if (company == null && isExternal)
31 | {
32 | throw new ArgumentException($"'{nameof(company)}' cannot be null or empty when the employee is external.",
33 | nameof(company));
34 | }
35 |
36 | if (isExternal)
37 | {
38 | // we know company won't be null here due to the check above, so
39 | // we can use the null-forgiving operator to notify the compiler of this
40 | return new ExternalEmployee(firstName, lastName, company = null!);
41 | }
42 |
43 | // create a new employee with default values
44 | return new InternalEmployee(firstName, lastName, 0, 2500, false, 1);
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/Fixtures/EmployeeServiceWithAspNetCoreDIFixture.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Business;
2 | using EmployeeManagement.DataAccess.Services;
3 | using EmployeeManagement.Services.Test;
4 | using Microsoft.Extensions.DependencyInjection;
5 |
6 | namespace EmployeeManagement.Test.Fixtures
7 | {
8 | public class EmployeeServiceWithAspNetCoreDIFixture : IDisposable
9 | {
10 | private readonly ServiceProvider _serviceProvider;
11 |
12 | public IEmployeeManagementRepository EmployeeManagementTestDataRepository
13 | {
14 | get
15 | {
16 | #pragma warning disable CS8603 // Possible null reference return.
17 | return _serviceProvider.GetService();
18 | #pragma warning restore CS8603 // Possible null reference return.
19 | }
20 | }
21 |
22 | public IEmployeeService EmployeeService
23 | {
24 | get
25 | {
26 | #pragma warning disable CS8603 // Possible null reference return.
27 | return _serviceProvider.GetService();
28 | #pragma warning restore CS8603 // Possible null reference return.
29 | }
30 | }
31 |
32 |
33 | public EmployeeServiceWithAspNetCoreDIFixture()
34 | {
35 | var services = new ServiceCollection();
36 | services.AddScoped();
37 | services.AddScoped();
39 | services.AddScoped();
40 |
41 | // build provider
42 | _serviceProvider = services.BuildServiceProvider();
43 | }
44 |
45 | public void Dispose()
46 | {
47 | // clean up the setup code, if required
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Views/EmployeeOverview/Index.cshtml:
--------------------------------------------------------------------------------
1 | @model EmployeeManagement.ViewModels.EmployeeOverviewViewModel
2 | @{
3 | ViewData["Title"] = "Employee Management";
4 | }
5 |
6 |
7 | Employee Management
8 |
9 |
10 | Add new internal employee
11 |
12 |
13 |
14 |
15 | |
16 | @Html.DisplayNameFor(model => model.InternalEmployees[0].FullName)
17 | |
18 |
19 | @Html.DisplayNameFor(model => model.InternalEmployees[0].Salary)
20 | |
21 |
22 | @Html.DisplayNameFor(model => model.InternalEmployees[0].YearsInService)
23 | |
24 |
25 | @Html.DisplayNameFor(model => model.InternalEmployees[0].SuggestedBonus)
26 | |
27 |
28 |
29 |
30 | @foreach (var item in Model.InternalEmployees)
31 | {
32 |
33 | |
34 | @Html.DisplayFor(modelItem => item.FullName)
35 | |
36 |
37 | @Html.DisplayFor(modelItem => item.Salary)
38 | |
39 |
40 | @Html.DisplayFor(modelItem => item.YearsInService)
41 | |
42 |
43 | @Html.DisplayFor(modelItem => item.SuggestedBonus)
44 | |
45 |
46 | Details
48 | |
49 |
50 | }
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Views/EmployeeOverview/Index.cshtml:
--------------------------------------------------------------------------------
1 | @model EmployeeManagement.ViewModels.EmployeeOverviewViewModel
2 | @{
3 | ViewData["Title"] = "Employee Management";
4 | }
5 |
6 |
7 | Employee Management
8 |
9 |
10 | Add new internal employee
11 |
12 |
13 |
14 |
15 | |
16 | @Html.DisplayNameFor(model => model.InternalEmployees[0].FullName)
17 | |
18 |
19 | @Html.DisplayNameFor(model => model.InternalEmployees[0].Salary)
20 | |
21 |
22 | @Html.DisplayNameFor(model => model.InternalEmployees[0].YearsInService)
23 | |
24 |
25 | @Html.DisplayNameFor(model => model.InternalEmployees[0].SuggestedBonus)
26 | |
27 |
28 |
29 |
30 | @foreach (var item in Model.InternalEmployees)
31 | {
32 |
33 | |
34 | @Html.DisplayFor(modelItem => item.FullName)
35 | |
36 |
37 | @Html.DisplayFor(modelItem => item.Salary)
38 | |
39 |
40 | @Html.DisplayFor(modelItem => item.YearsInService)
41 | |
42 |
43 | @Html.DisplayFor(modelItem => item.SuggestedBonus)
44 | |
45 |
46 | Details
48 | |
49 |
50 | }
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Controllers/EmployeeOverviewController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using EmployeeManagement.Business;
3 | using EmployeeManagement.ViewModels;
4 | using Microsoft.AspNetCore.Mvc;
5 | using System.Diagnostics;
6 |
7 | namespace EmployeeManagement.Controllers
8 | {
9 | public class EmployeeOverviewController : Controller
10 | {
11 | private readonly IEmployeeService _employeeService;
12 | private readonly IMapper _mapper;
13 |
14 | public EmployeeOverviewController(IEmployeeService employeeService,
15 | IMapper mapper)
16 | {
17 | _employeeService = employeeService;
18 | _mapper = mapper;
19 | }
20 |
21 | public async Task Index()
22 | {
23 | var internalEmployees = await _employeeService.FetchInternalEmployeesAsync();
24 |
25 | // with manual mapping
26 | var internalEmployeeForOverviewViewModels =
27 | internalEmployees.Select(e => new InternalEmployeeForOverviewViewModel()
28 | {
29 | Id = e.Id,
30 | FirstName = e.FirstName,
31 | LastName = e.LastName,
32 | Salary = e.Salary,
33 | SuggestedBonus = e.SuggestedBonus,
34 | YearsInService = e.YearsInService
35 | });
36 |
37 | // with AutoMapper
38 | //var internalEmployeeForOverviewViewModels =
39 | // _mapper.Map>(internalEmployees);
40 |
41 | return View(new EmployeeOverviewViewModel(internalEmployeeForOverviewViewModels));
42 | }
43 |
44 |
45 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
46 | public IActionResult Error()
47 | {
48 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
49 | }
50 | }
51 | }
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Views/Shared/_Layout.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | @ViewData["Title"] - Employee Management
7 |
8 |
9 |
10 |
11 |
12 |
30 |
31 |
32 | @RenderBody()
33 |
34 |
35 |
36 |
38 |
39 |
40 |
41 | @await RenderSectionAsync("Scripts", required: false)
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Views/Shared/_Layout.cshtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | @ViewData["Title"] - Employee Management
7 |
8 |
9 |
10 |
11 |
12 |
30 |
31 |
32 | @RenderBody()
33 |
34 |
35 |
36 |
38 |
39 |
40 |
41 | @await RenderSectionAsync("Scripts", required: false)
42 |
43 |
44 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/StatisticsControllerTests.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using EmployeeManagement.Controllers;
3 | using EmployeeManagement.ViewModels;
4 | using Microsoft.AspNetCore.Http;
5 | using Microsoft.AspNetCore.Http.Features;
6 | using Microsoft.AspNetCore.Mvc;
7 | using Moq;
8 | using Xunit;
9 |
10 | namespace EmployeeManagement.Test
11 | {
12 | public class StatisticsControllerTests
13 | {
14 | [Fact]
15 | public void Index_InputFromHttpConnectionFeature_MustReturnInputtedIps()
16 | {
17 | // Arrange
18 | var localIpAddress = System.Net.IPAddress.Parse("111.111.111.111");
19 | var localPort = 5000;
20 | var remoteIpAddress = System.Net.IPAddress.Parse("222.222.222.222");
21 | var remotePort = 8080;
22 |
23 | var featureCollectionMock = new Mock();
24 | featureCollectionMock.Setup(e => e.Get())
25 | .Returns(new HttpConnectionFeature
26 | {
27 | LocalIpAddress = localIpAddress,
28 | LocalPort = localPort,
29 | RemoteIpAddress = remoteIpAddress,
30 | RemotePort = remotePort
31 | });
32 |
33 | var httpContextMock = new Mock();
34 | httpContextMock.Setup(e => e.Features)
35 | .Returns(featureCollectionMock.Object);
36 |
37 | var mapperConfiguration = new MapperConfiguration(
38 | cfg => cfg.AddProfile());
39 | var mapper = new Mapper(mapperConfiguration);
40 |
41 | var statisticsController = new StatisticsController(mapper);
42 |
43 | statisticsController.ControllerContext = new ControllerContext()
44 | {
45 | HttpContext = httpContextMock.Object
46 | };
47 |
48 | // Act
49 | var result = statisticsController.Index();
50 |
51 | // Assert
52 | var viewResult = Assert.IsType(result);
53 | var viewModel = Assert.IsType(
54 | viewResult.Model);
55 | Assert.Equal(localIpAddress.ToString(), viewModel.LocalIpAddress);
56 | Assert.Equal(localPort, viewModel.LocalPort);
57 | Assert.Equal(remoteIpAddress.ToString(), viewModel.RemoteIpAddress);
58 | Assert.Equal(remotePort, viewModel.RemotePort);
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Controllers/EmployeeOverviewController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using EmployeeManagement.Business;
3 | using EmployeeManagement.ViewModels;
4 | using Microsoft.AspNetCore.Authorization;
5 | using Microsoft.AspNetCore.Mvc;
6 | using System.Diagnostics;
7 |
8 | namespace EmployeeManagement.Controllers
9 | {
10 | public class EmployeeOverviewController : Controller
11 | {
12 | private readonly IEmployeeService _employeeService;
13 | private readonly IMapper _mapper;
14 |
15 | public EmployeeOverviewController(IEmployeeService employeeService,
16 | IMapper mapper)
17 | {
18 | _employeeService = employeeService;
19 | _mapper = mapper;
20 | }
21 |
22 | public async Task Index()
23 | {
24 | var internalEmployees = await _employeeService
25 | .FetchInternalEmployeesAsync();
26 |
27 | // with manual mapping
28 | //var internalEmployeeForOverviewViewModels =
29 | // internalEmployees.Select(e =>
30 | // new InternalEmployeeForOverviewViewModel()
31 | // {
32 | // Id = e.Id,
33 | // FirstName = e.FirstName,
34 | // LastName = e.LastName,
35 | // Salary = e.Salary,
36 | // SuggestedBonus = e.SuggestedBonus,
37 | // YearsInService = e.YearsInService
38 | // });
39 |
40 | // with AutoMapper
41 | var internalEmployeeForOverviewViewModels =
42 | _mapper.Map>(internalEmployees);
43 |
44 | return View(
45 | new EmployeeOverviewViewModel(internalEmployeeForOverviewViewModels));
46 | }
47 |
48 | [Authorize]
49 | public IActionResult ProtectedIndex()
50 | {
51 | // depending on the role, return a different result
52 |
53 | if (User.IsInRole("Admin"))
54 | {
55 | return RedirectToAction("AdminIndex", "EmployeeManagement");
56 | }
57 |
58 | return RedirectToAction("Index", "EmployeeManagement");
59 | }
60 |
61 |
62 | [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
63 | public IActionResult Error()
64 | {
65 | return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
66 | }
67 | }
68 | }
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Business/PromotionService.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.Entities;
2 | using EmployeeManagement.DataAccess.Services;
3 | using System.Net.Http.Headers;
4 | using System.Text.Json;
5 |
6 | namespace EmployeeManagement.Business
7 | {
8 | public class PromotionService : IPromotionService
9 | {
10 | private readonly HttpClient _httpClient;
11 | private readonly IEmployeeManagementRepository _employeeManagementRepository;
12 |
13 | public PromotionService(
14 | HttpClient httpClient,
15 | IEmployeeManagementRepository employeeManagementRepository)
16 | {
17 | _httpClient = httpClient;
18 | _employeeManagementRepository = employeeManagementRepository;
19 | }
20 |
21 | ///
22 | /// Promote an internal employee if eligible for promotion
23 | ///
24 | ///
25 | ///
26 | public async Task PromoteInternalEmployeeAsync(InternalEmployee employee)
27 | {
28 | if (await CheckIfInternalEmployeeIsEligibleForPromotion(employee.Id))
29 | {
30 | employee.JobLevel++;
31 | await _employeeManagementRepository.SaveChangesAsync();
32 | return true;
33 | }
34 | return false;
35 | }
36 |
37 | ///
38 | /// Calls into external API (containing a data source only
39 | /// the top level managers can manage) to check whether
40 | /// an internal employee is eligible for promotion
41 | ///
42 | private async Task CheckIfInternalEmployeeIsEligibleForPromotion(
43 | Guid employeeId)
44 | {
45 | // call into API
46 | var apiRoot = "http://localhost:5057";
47 |
48 | var request = new HttpRequestMessage(HttpMethod.Get,
49 | $"{apiRoot}/api/promotioneligibilities/{employeeId}");
50 | request.Headers.Accept.Add(
51 | new MediaTypeWithQualityHeaderValue("application/json"));
52 |
53 | var response = await _httpClient.SendAsync(request);
54 | response.EnsureSuccessStatusCode();
55 |
56 | // deserialize content
57 | var content = await response.Content.ReadAsStringAsync();
58 | var promotionEligibility = JsonSerializer.Deserialize(content,
59 | new JsonSerializerOptions
60 | {
61 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase
62 | });
63 |
64 | // return value
65 | return promotionEligibility == null ?
66 | false : promotionEligibility.EligibleForPromotion;
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Business/PromotionService.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.Entities;
2 | using EmployeeManagement.DataAccess.Services;
3 | using System.Net.Http.Headers;
4 | using System.Text.Json;
5 |
6 | namespace EmployeeManagement.Business
7 | {
8 | public class PromotionService : IPromotionService
9 | {
10 | private readonly HttpClient _httpClient;
11 | private readonly IEmployeeManagementRepository _employeeManagementRepository;
12 |
13 | public PromotionService(
14 | HttpClient httpClient,
15 | IEmployeeManagementRepository employeeManagementRepository)
16 | {
17 | _httpClient = httpClient;
18 | _employeeManagementRepository = employeeManagementRepository;
19 | }
20 |
21 | ///
22 | /// Promote an internal employee if eligible for promotion
23 | ///
24 | ///
25 | ///
26 | public async Task PromoteInternalEmployeeAsync(
27 | InternalEmployee employee)
28 | {
29 | if (await CheckIfInternalEmployeeIsEligibleForPromotion(employee.Id))
30 | {
31 | employee.JobLevel++;
32 | await _employeeManagementRepository.SaveChangesAsync();
33 | return true;
34 | }
35 | return false;
36 | }
37 |
38 | ///
39 | /// Calls into external API (containing a data source only
40 | /// the top level managers can manage) to check whether
41 | /// an internal employee is eligible for promotion
42 | ///
43 | private async Task CheckIfInternalEmployeeIsEligibleForPromotion(
44 | Guid employeeId)
45 | {
46 | // call into API
47 | var apiRoot = "http://localhost:5057";
48 |
49 | var request = new HttpRequestMessage(HttpMethod.Get,
50 | $"{apiRoot}/api/promotioneligibilities/{employeeId}");
51 | request.Headers.Accept.Add(
52 | new MediaTypeWithQualityHeaderValue("application/json"));
53 |
54 | var response = await _httpClient.SendAsync(request);
55 | response.EnsureSuccessStatusCode();
56 |
57 | // deserialize content
58 | var content = await response.Content.ReadAsStringAsync();
59 | var promotionEligibility = JsonSerializer.Deserialize(content,
60 | new JsonSerializerOptions
61 | {
62 | PropertyNamingPolicy = JsonNamingPolicy.CamelCase
63 | });
64 |
65 | // return value
66 | return promotionEligibility == null ?
67 | false : promotionEligibility.EligibleForPromotion;
68 | }
69 | }
70 | }
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/EmployeeTests.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.Entities;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using System.Text;
6 | using System.Threading.Tasks;
7 | using Xunit;
8 |
9 | namespace EmployeeManagement.Test
10 | {
11 | public class EmployeeTests
12 | {
13 | [Fact]
14 | public void EmployeeFullNamePropertyGetter_InputFirstNameAndLastName_FullNameIsConcatenation()
15 | {
16 | // Arrange
17 | var employee = new InternalEmployee("Kevin", "Dockx", 0, 2500, false, 1);
18 |
19 | // Act
20 | employee.FirstName = "Lucia";
21 | employee.LastName = "SHELTON";
22 |
23 | // Assert
24 | Assert.Equal("Lucia Shelton", employee.FullName, ignoreCase:true);
25 | }
26 |
27 | [Fact]
28 | public void EmployeeFullNamePropertyGetter_InputFirstNameAndLastName_FullNameStartsWithFirstName()
29 | {
30 | // Arrange
31 | var employee = new InternalEmployee("Kevin", "Dockx", 0, 2500, false, 1);
32 |
33 | // Act
34 | employee.FirstName = "Lucia";
35 | employee.LastName = "Shelton";
36 |
37 | // Assert
38 | Assert.StartsWith(employee.FirstName, employee.FullName);
39 | }
40 |
41 | [Fact]
42 | public void EmployeeFullNamePropertyGetter_InputFirstNameAndLastName_FullNameEndsWithFirstName()
43 | {
44 | // Arrange
45 | var employee = new InternalEmployee("Kevin", "Dockx", 0, 2500, false, 1);
46 |
47 | // Act
48 | employee.FirstName = "Lucia";
49 | employee.LastName = "Shelton";
50 |
51 | // Assert
52 | Assert.EndsWith(employee.LastName, employee.FullName);
53 | }
54 |
55 | [Fact]
56 | public void EmployeeFullNamePropertyGetter_InputFirstNameAndLastName_FullNameContainsPartOfConcatenation()
57 | {
58 | // Arrange
59 | var employee = new InternalEmployee("Kevin", "Dockx", 0, 2500, false, 1);
60 |
61 | // Act
62 | employee.FirstName = "Lucia";
63 | employee.LastName = "Shelton";
64 |
65 | // Assert
66 | Assert.Contains("ia Sh", employee.FullName);
67 | }
68 |
69 | [Fact]
70 | public void EmployeeFullNamePropertyGetter_InputFirstNameAndLastName_FullNameSoundsLikeConcatenation()
71 | {
72 | // Arrange
73 | var employee = new InternalEmployee("Kevin", "Dockx", 0, 2500, false, 1);
74 |
75 | // Act
76 | employee.FirstName = "Lucia";
77 | employee.LastName = "Shelton";
78 |
79 | // Assert
80 | Assert.Matches("Lu(c|s|z)ia Shel(t|d)on", employee.FullName);
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/DataAccess/Services/EmployeeManagementRepository.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.DbContexts;
2 | using EmployeeManagement.DataAccess.Entities;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace EmployeeManagement.DataAccess.Services
6 | {
7 | public class EmployeeManagementRepository : IEmployeeManagementRepository
8 | {
9 | private readonly EmployeeDbContext _context;
10 |
11 | public EmployeeManagementRepository(EmployeeDbContext context)
12 | {
13 | _context = context ?? throw new ArgumentNullException(nameof(context));
14 | }
15 |
16 | public async Task> GetInternalEmployeesAsync()
17 | {
18 | return await _context.InternalEmployees
19 | .Include(e => e.AttendedCourses)
20 | .ToListAsync();
21 | }
22 |
23 | public async Task GetInternalEmployeeAsync(Guid employeeId)
24 | {
25 | return await _context.InternalEmployees
26 | .Include(e => e.AttendedCourses)
27 | .FirstOrDefaultAsync(e => e.Id == employeeId);
28 | }
29 |
30 | public InternalEmployee? GetInternalEmployee(Guid employeeId)
31 | {
32 | return _context.InternalEmployees
33 | .Include(e => e.AttendedCourses)
34 | .FirstOrDefault(e => e.Id == employeeId);
35 | }
36 |
37 | public async Task GetCourseAsync(Guid courseId)
38 | {
39 | return await _context.Courses.FirstOrDefaultAsync(e => e.Id == courseId);
40 | }
41 |
42 | public Course? GetCourse(Guid courseId)
43 | {
44 | return _context.Courses.FirstOrDefault(e => e.Id == courseId);
45 | }
46 |
47 | public List GetCourses(params Guid[] courseIds)
48 | {
49 | List coursesToReturn = new();
50 | foreach (var courseId in courseIds)
51 | {
52 | var course = GetCourse(courseId);
53 | if (course != null)
54 | {
55 | coursesToReturn.Add(course);
56 | }
57 | }
58 | return coursesToReturn;
59 | }
60 |
61 | public async Task> GetCoursesAsync(params Guid[] courseIds)
62 | {
63 | List coursesToReturn = new();
64 | foreach (var courseId in courseIds)
65 | {
66 | var course = await GetCourseAsync(courseId);
67 | if (course != null)
68 | {
69 | coursesToReturn.Add(course);
70 | }
71 | }
72 | return coursesToReturn;
73 | }
74 |
75 | public void AddInternalEmployee(InternalEmployee internalEmployee)
76 | {
77 | _context.InternalEmployees.Add(internalEmployee);
78 | }
79 |
80 | public async Task SaveChangesAsync()
81 | {
82 | await _context.SaveChangesAsync();
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/DataAccess/Services/EmployeeManagementRepository.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.DbContexts;
2 | using EmployeeManagement.DataAccess.Entities;
3 | using Microsoft.EntityFrameworkCore;
4 |
5 | namespace EmployeeManagement.DataAccess.Services
6 | {
7 | public class EmployeeManagementRepository : IEmployeeManagementRepository
8 | {
9 | private readonly EmployeeDbContext _context;
10 |
11 | public EmployeeManagementRepository(EmployeeDbContext context)
12 | {
13 | _context = context ?? throw new ArgumentNullException(nameof(context));
14 | }
15 |
16 | public async Task> GetInternalEmployeesAsync()
17 | {
18 | return await _context.InternalEmployees
19 | .Include(e => e.AttendedCourses)
20 | .ToListAsync();
21 | }
22 |
23 | public async Task GetInternalEmployeeAsync(Guid employeeId)
24 | {
25 | return await _context.InternalEmployees
26 | .Include(e => e.AttendedCourses)
27 | .FirstOrDefaultAsync(e => e.Id == employeeId);
28 | }
29 |
30 | public InternalEmployee? GetInternalEmployee(Guid employeeId)
31 | {
32 | return _context.InternalEmployees
33 | .Include(e => e.AttendedCourses)
34 | .FirstOrDefault(e => e.Id == employeeId);
35 | }
36 |
37 | public async Task GetCourseAsync(Guid courseId)
38 | {
39 | return await _context.Courses.FirstOrDefaultAsync(e => e.Id == courseId);
40 | }
41 |
42 | public Course? GetCourse(Guid courseId)
43 | {
44 | return _context.Courses.FirstOrDefault(e => e.Id == courseId);
45 | }
46 |
47 | public List GetCourses(params Guid[] courseIds)
48 | {
49 | List coursesToReturn = new();
50 | foreach (var courseId in courseIds)
51 | {
52 | var course = GetCourse(courseId);
53 | if (course != null)
54 | {
55 | coursesToReturn.Add(course);
56 | }
57 | }
58 | return coursesToReturn;
59 | }
60 |
61 | public async Task> GetCoursesAsync(params Guid[] courseIds)
62 | {
63 | List coursesToReturn = new();
64 | foreach (var courseId in courseIds)
65 | {
66 | var course = await GetCourseAsync(courseId);
67 | if (course != null)
68 | {
69 | coursesToReturn.Add(course);
70 | }
71 | }
72 | return coursesToReturn;
73 | }
74 |
75 | public void AddInternalEmployee(InternalEmployee internalEmployee)
76 | {
77 | _context.InternalEmployees.Add(internalEmployee);
78 | }
79 |
80 | public async Task SaveChangesAsync()
81 | {
82 | await _context.SaveChangesAsync();
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/Controllers/InternalEmployeeController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using EmployeeManagement.Business;
3 | using EmployeeManagement.ViewModels;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | namespace EmployeeManagement.Controllers
7 | {
8 | public class InternalEmployeeController : Controller
9 | {
10 | private readonly IEmployeeService _employeeService;
11 | private readonly IMapper _mapper;
12 |
13 | public InternalEmployeeController(IEmployeeService employeeService,
14 | IMapper mapper)
15 | {
16 | _employeeService = employeeService;
17 | _mapper = mapper;
18 | }
19 |
20 | [HttpGet]
21 | public IActionResult AddInternalEmployee()
22 | {
23 | return View(new CreateInternalEmployeeViewModel());
24 | }
25 |
26 | [HttpPost]
27 | public async Task AddInternalEmployee(CreateInternalEmployeeViewModel model)
28 | {
29 | if (!ModelState.IsValid)
30 | {
31 | return BadRequest(ModelState);
32 | }
33 | else
34 | {
35 | // create an internal employee entity with default values filled out
36 | // and the values the user inputted
37 | var internalEmplooyee =
38 | await _employeeService.CreateInternalEmployeeAsync(model.FirstName, model.LastName);
39 |
40 | // persist it
41 | await _employeeService.AddInternalEmployeeAsync(internalEmplooyee);
42 | }
43 |
44 | return RedirectToAction("Index", "EmployeeOverview");
45 | }
46 |
47 | [HttpGet]
48 | public async Task InternalEmployeeDetails(
49 | [FromRoute(Name = "id")] Guid? employeeId)
50 | {
51 | if (!employeeId.HasValue)
52 | {
53 | return RedirectToAction("Index", "EmployeeOverview");
54 | }
55 |
56 | var internalEmployee = await _employeeService.FetchInternalEmployeeAsync(employeeId.Value);
57 | if (internalEmployee == null)
58 | {
59 | return RedirectToAction("Index", "EmployeeOverview");
60 | }
61 |
62 | return View(_mapper.Map(internalEmployee));
63 | }
64 |
65 | [HttpPost]
66 | public async Task ExecutePromotionRequest(
67 | [FromForm(Name = "id")] Guid? employeeId)
68 | {
69 | if (!employeeId.HasValue)
70 | {
71 | return RedirectToAction("Index", "EmployeeOverview");
72 | }
73 |
74 | var internalEmployee = await _employeeService
75 | .FetchInternalEmployeeAsync(employeeId.Value);
76 |
77 | if (internalEmployee == null)
78 | {
79 | return RedirectToAction("Index", "EmployeeOverview");
80 | }
81 |
82 | return View("InternalEmployeeDetails",
83 | _mapper.Map(internalEmployee));
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/TestIsolationApproachesTests.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Business;
2 | using EmployeeManagement.DataAccess.DbContexts;
3 | using EmployeeManagement.DataAccess.Entities;
4 | using EmployeeManagement.DataAccess.Services;
5 | using EmployeeManagement.Services.Test;
6 | using EmployeeManagement.Test.HttpMessageHandlers;
7 | using Microsoft.Data.Sqlite;
8 | using Microsoft.EntityFrameworkCore;
9 | using Xunit;
10 | using Xunit.Sdk;
11 |
12 | namespace EmployeeManagement.Test
13 | {
14 | public class TestIsolationApproachesTests
15 | {
16 | [Fact]
17 | public async Task AttendCourseAsync_CourseAttended_SuggestedBonusMustCorrectlyBeRecalculated()
18 | {
19 | // Arrange
20 | var connection = new SqliteConnection("Data Source=:memory:");
21 | connection.Open();
22 |
23 | var optionsBuilder = new DbContextOptionsBuilder()
24 | .UseSqlite(connection);
25 |
26 | var dbContext = new EmployeeDbContext(optionsBuilder.Options);
27 | dbContext.Database.Migrate();
28 |
29 | var employeeManagementDataRepository =
30 | new EmployeeManagementRepository(dbContext);
31 |
32 | var employeeService = new EmployeeService(
33 | employeeManagementDataRepository,
34 | new EmployeeFactory());
35 |
36 | // get course from database - "Dealing with Customers 101"
37 | var courseToAttend = await employeeManagementDataRepository
38 | .GetCourseAsync(Guid.Parse("844e14ce-c055-49e9-9610-855669c9859b"));
39 |
40 | // get existing employee - "Megan Jones"
41 | var internalEmployee = await employeeManagementDataRepository
42 | .GetInternalEmployeeAsync(Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb"));
43 |
44 | if (courseToAttend == null || internalEmployee == null)
45 | {
46 | throw new XunitException("Arranging the test failed");
47 | }
48 |
49 | // expected suggested bonus after attending the course
50 | var expectedSuggestedBonus = internalEmployee.YearsInService
51 | * (internalEmployee.AttendedCourses.Count + 1) * 100;
52 |
53 | // Act
54 | await employeeService.AttendCourseAsync(internalEmployee, courseToAttend);
55 |
56 | // Assert
57 | Assert.Equal(expectedSuggestedBonus, internalEmployee.SuggestedBonus);
58 | }
59 |
60 | [Fact]
61 | public async Task PromoteInternalEmployeeAsync_IsEligible_JobLevelMustBeIncreased()
62 | {
63 | // Arrange
64 | var httpClient = new HttpClient(
65 | new TestablePromotionEligibilityHandler(true));
66 | var internalEmployee = new InternalEmployee(
67 | "Brooklyn", "Cannon", 5, 3000, false, 1);
68 | var promotionService = new PromotionService(httpClient,
69 | new EmployeeManagementTestDataRepository());
70 |
71 | // Act
72 | await promotionService.PromoteInternalEmployeeAsync(internalEmployee);
73 |
74 | // Assert
75 | Assert.Equal(2, internalEmployee.JobLevel);
76 | }
77 |
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EmployeeManagement", "EmployeeManagement\EmployeeManagement.csproj", "{A9D381CA-BB06-4106-AA5F-CD2FC0D436F6}"
7 | EndProject
8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TopLevelManagement", "TopLevelManagement\TopLevelManagement.csproj", "{6807FF4E-072D-4AE3-81BC-67D75FE791DC}"
9 | EndProject
10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EmployeeManagement.Test", "EmployeeManagement.Test\EmployeeManagement.Test.csproj", "{23174600-2570-4F11-9045-2388DBC0B015}"
11 | EndProject
12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyFramework", "CompanyFramework\CompanyFramework.csproj", "{3E581340-63F0-44D7-A00E-910D4B7FCAAB}"
13 | EndProject
14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompanyFramework.Test", "CompanyFramework.Test\CompanyFramework.Test.csproj", "{0CD1901A-1518-4A64-8602-53A291E05375}"
15 | EndProject
16 | Global
17 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
18 | Debug|Any CPU = Debug|Any CPU
19 | Release|Any CPU = Release|Any CPU
20 | EndGlobalSection
21 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
22 | {A9D381CA-BB06-4106-AA5F-CD2FC0D436F6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
23 | {A9D381CA-BB06-4106-AA5F-CD2FC0D436F6}.Debug|Any CPU.Build.0 = Debug|Any CPU
24 | {A9D381CA-BB06-4106-AA5F-CD2FC0D436F6}.Release|Any CPU.ActiveCfg = Release|Any CPU
25 | {A9D381CA-BB06-4106-AA5F-CD2FC0D436F6}.Release|Any CPU.Build.0 = Release|Any CPU
26 | {6807FF4E-072D-4AE3-81BC-67D75FE791DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {6807FF4E-072D-4AE3-81BC-67D75FE791DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {6807FF4E-072D-4AE3-81BC-67D75FE791DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {6807FF4E-072D-4AE3-81BC-67D75FE791DC}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {23174600-2570-4F11-9045-2388DBC0B015}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {23174600-2570-4F11-9045-2388DBC0B015}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {23174600-2570-4F11-9045-2388DBC0B015}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {23174600-2570-4F11-9045-2388DBC0B015}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {3E581340-63F0-44D7-A00E-910D4B7FCAAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {3E581340-63F0-44D7-A00E-910D4B7FCAAB}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {3E581340-63F0-44D7-A00E-910D4B7FCAAB}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {3E581340-63F0-44D7-A00E-910D4B7FCAAB}.Release|Any CPU.Build.0 = Release|Any CPU
38 | {0CD1901A-1518-4A64-8602-53A291E05375}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
39 | {0CD1901A-1518-4A64-8602-53A291E05375}.Debug|Any CPU.Build.0 = Debug|Any CPU
40 | {0CD1901A-1518-4A64-8602-53A291E05375}.Release|Any CPU.ActiveCfg = Release|Any CPU
41 | {0CD1901A-1518-4A64-8602-53A291E05375}.Release|Any CPU.Build.0 = Release|Any CPU
42 | EndGlobalSection
43 | GlobalSection(SolutionProperties) = preSolution
44 | HideSolutionNode = FALSE
45 | EndGlobalSection
46 | GlobalSection(ExtensibilityGlobals) = postSolution
47 | SolutionGuid = {4771D570-E205-4450-92AC-5F5F9D2C9729}
48 | EndGlobalSection
49 | EndGlobal
50 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/EmployeeFactoryTests.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.Business;
2 | using EmployeeManagement.DataAccess.Entities;
3 | using Xunit;
4 |
5 | namespace EmployeeManagement.Test
6 | {
7 | [Collection("No parallelism")]
8 | public class EmployeeFactoryTests : IDisposable
9 | {
10 | private EmployeeFactory _employeeFactory;
11 |
12 | public EmployeeFactoryTests()
13 | {
14 | _employeeFactory = new EmployeeFactory();
15 | }
16 |
17 | public void Dispose()
18 | {
19 | // clean up the setup code, if required
20 | }
21 |
22 |
23 | [Fact(Skip = "Skipping this one for demo reasons.")]
24 | [Trait("Category", "EmployeeFactory_CreateEmployee_Salary")]
25 | public void CreateEmployee_ConstructInternalEmployee_SalaryMustBe2500()
26 | {
27 |
28 | var employee = (InternalEmployee)_employeeFactory
29 | .CreateEmployee("Kevin", "Dockx");
30 |
31 | Assert.Equal(2500, employee.Salary);
32 | }
33 |
34 | [Fact]
35 | [Trait("Category", "EmployeeFactory_CreateEmployee_Salary")]
36 | public void CreateEmployee_ConstructInternalEmployee_SalaryMustBeBetween2500And3500()
37 | {
38 | // Arrange
39 |
40 | // Act
41 | var employee = (InternalEmployee)_employeeFactory
42 | .CreateEmployee("Kevin", "Dockx");
43 |
44 | // Assert
45 | Assert.True(employee.Salary >= 3000 && employee.Salary <= 3500,
46 | "Salary not in acceptable range.");
47 | }
48 |
49 | [Fact]
50 | [Trait("Category", "EmployeeFactory_CreateEmployee_Salary")]
51 | public void CreateEmployee_ConstructInternalEmployee_SalaryMustBeBetween2500And3500_Alternative()
52 | {
53 | // Arrange
54 |
55 | // Act
56 | var employee = (InternalEmployee)_employeeFactory
57 | .CreateEmployee("Kevin", "Dockx");
58 |
59 | // Assert
60 | Assert.True(employee.Salary >= 2500);
61 | Assert.True(employee.Salary <= 3500);
62 | }
63 |
64 | [Fact]
65 | [Trait("Category", "EmployeeFactory_CreateEmployee_Salary")]
66 | public void CreateEmployee_ConstructInternalEmployee_SalaryMustBeBetween2500And3500_AlternativeWithInRange()
67 | {
68 | // Arrange
69 |
70 | // Act
71 | var employee = (InternalEmployee)_employeeFactory
72 | .CreateEmployee("Kevin", "Dockx");
73 |
74 | // Assert
75 | Assert.InRange(employee.Salary, 2500, 3500);
76 | }
77 |
78 | [Fact]
79 | [Trait("Category", "EmployeeFactory_CreateEmployee_Salary")]
80 | public void CreateEmployee_ConstructInternalEmployee_SalaryMustBe2500_PrecisionExample()
81 | {
82 | // Arrange
83 |
84 | // Act
85 | var employee = (InternalEmployee)_employeeFactory
86 | .CreateEmployee("Kevin", "Dockx");
87 | employee.Salary = 2500.123m;
88 |
89 | // Assert
90 | Assert.Equal(2500, employee.Salary, 0);
91 | }
92 |
93 | [Fact]
94 | [Trait("Category", "EmployeeFactory_CreateEmployee_ReturnType")]
95 | public void CreateEmployee_IsExternalIsTrue_ReturnTypeMustBeExternalEmployee()
96 | {
97 | // Arrange
98 |
99 | // Act
100 | var employee = _employeeFactory
101 | .CreateEmployee("Kevin", "Dockx", "Marvin", true);
102 |
103 | // Assert
104 | Assert.IsType(employee);
105 | //Assert.IsAssignableFrom(employee);
106 | }
107 |
108 | [Fact]
109 | public void SlowTest1()
110 | {
111 | Thread.Sleep(5000);
112 | Assert.True(true);
113 | }
114 |
115 |
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/DataAccess/DbContexts/EmployeeDbContext.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace EmployeeManagement.DataAccess.DbContexts
5 | {
6 | public class EmployeeDbContext : DbContext
7 | {
8 | public DbSet InternalEmployees { get; set; } = null!;
9 | public DbSet ExternalEmployees { get; set; } = null!;
10 | public DbSet Courses { get; set; } = null!;
11 |
12 | public EmployeeDbContext(DbContextOptions options)
13 | : base(options)
14 | {
15 | }
16 |
17 | protected override void OnModelCreating(ModelBuilder modelBuilder)
18 | {
19 | var obligatoryCourse1 = new Course("Company Introduction")
20 | {
21 | Id = Guid.Parse("37e03ca7-c730-4351-834c-b66f280cdb01"),
22 | IsNew = false
23 | };
24 |
25 | var obligatoryCourse2 = new Course("Respecting Your Colleagues")
26 | {
27 | Id = Guid.Parse("1fd115cf-f44c-4982-86bc-a8fe2e4ff83e"),
28 | IsNew = false
29 | };
30 |
31 | var optionalCourse1 = new Course("Dealing with Customers 101")
32 | {
33 | Id = Guid.Parse("844e14ce-c055-49e9-9610-855669c9859b"),
34 | IsNew = false
35 | };
36 |
37 | modelBuilder.Entity()
38 | .HasData(obligatoryCourse1,
39 | obligatoryCourse2,
40 | optionalCourse1,
41 | new Course("Dealing with Customers - Advanced")
42 | {
43 | Id = Guid.Parse("d6e0e4b7-9365-4332-9b29-bb7bf09664a6"),
44 | IsNew = false
45 | },
46 | new Course("Disaster Management 101")
47 | {
48 | Id = Guid.Parse("cbf6db3b-c4ee-46aa-9457-5fa8aefef33a"),
49 | IsNew = false
50 | }
51 | );
52 |
53 | modelBuilder.Entity()
54 | .HasData(
55 | new InternalEmployee("Megan", "Jones", 2, 3000, false, 2)
56 | {
57 | Id = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb")
58 | },
59 | new InternalEmployee("Jaimy", "Johnson", 3, 3400, true, 1)
60 | {
61 | Id = Guid.Parse("f484ad8f-78fd-46d1-9f87-bbb1e676e37f")
62 | });
63 |
64 | modelBuilder
65 | .Entity()
66 | .HasMany(p => p.AttendedCourses)
67 | .WithMany(p => p.EmployeesThatAttended)
68 | .UsingEntity(j => j.ToTable("CourseInternalEmployee").HasData(new[]
69 | {
70 | new { AttendedCoursesId = Guid.Parse("37e03ca7-c730-4351-834c-b66f280cdb01"),
71 | EmployeesThatAttendedId = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb") },
72 | new { AttendedCoursesId = Guid.Parse("1fd115cf-f44c-4982-86bc-a8fe2e4ff83e"),
73 | EmployeesThatAttendedId = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb") },
74 | new { AttendedCoursesId = Guid.Parse("37e03ca7-c730-4351-834c-b66f280cdb01"),
75 | EmployeesThatAttendedId = Guid.Parse("f484ad8f-78fd-46d1-9f87-bbb1e676e37f") },
76 | new { AttendedCoursesId = Guid.Parse("1fd115cf-f44c-4982-86bc-a8fe2e4ff83e"),
77 | EmployeesThatAttendedId = Guid.Parse("f484ad8f-78fd-46d1-9f87-bbb1e676e37f") },
78 | new { AttendedCoursesId = Guid.Parse("844e14ce-c055-49e9-9610-855669c9859b"),
79 | EmployeesThatAttendedId = Guid.Parse("f484ad8f-78fd-46d1-9f87-bbb1e676e37f") }
80 | }
81 | ));
82 |
83 | modelBuilder.Entity()
84 | .HasData(
85 | new ExternalEmployee("Amanda", "Smith", "IT for Everyone, Inc")
86 | {
87 | Id = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb")
88 | });
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/DataAccess/DbContexts/EmployeeDbContext.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.Entities;
2 | using Microsoft.EntityFrameworkCore;
3 |
4 | namespace EmployeeManagement.DataAccess.DbContexts
5 | {
6 | public class EmployeeDbContext : DbContext
7 | {
8 | public DbSet InternalEmployees { get; set; } = null!;
9 | public DbSet ExternalEmployees { get; set; } = null!;
10 | public DbSet Courses { get; set; } = null!;
11 |
12 | public EmployeeDbContext(DbContextOptions options)
13 | : base(options)
14 | {
15 | }
16 |
17 | protected override void OnModelCreating(ModelBuilder modelBuilder)
18 | {
19 | var obligatoryCourse1 = new Course("Company Introduction")
20 | {
21 | Id = Guid.Parse("37e03ca7-c730-4351-834c-b66f280cdb01"),
22 | IsNew = false
23 | };
24 |
25 | var obligatoryCourse2 = new Course("Respecting Your Colleagues")
26 | {
27 | Id = Guid.Parse("1fd115cf-f44c-4982-86bc-a8fe2e4ff83e"),
28 | IsNew = false
29 | };
30 |
31 | var optionalCourse1 = new Course("Dealing with Customers 101")
32 | {
33 | Id = Guid.Parse("844e14ce-c055-49e9-9610-855669c9859b"),
34 | IsNew = false
35 | };
36 |
37 | modelBuilder.Entity()
38 | .HasData(obligatoryCourse1,
39 | obligatoryCourse2,
40 | optionalCourse1,
41 | new Course("Dealing with Customers - Advanced")
42 | {
43 | Id = Guid.Parse("d6e0e4b7-9365-4332-9b29-bb7bf09664a6"),
44 | IsNew = false
45 | },
46 | new Course("Disaster Management 101")
47 | {
48 | Id = Guid.Parse("cbf6db3b-c4ee-46aa-9457-5fa8aefef33a"),
49 | IsNew = false
50 | }
51 | );
52 |
53 | modelBuilder.Entity()
54 | .HasData(
55 | new InternalEmployee("Megan", "Jones", 2, 3000, false, 2)
56 | {
57 | Id = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb")
58 | },
59 | new InternalEmployee("Jaimy", "Johnson", 3, 3400, true, 1)
60 | {
61 | Id = Guid.Parse("f484ad8f-78fd-46d1-9f87-bbb1e676e37f")
62 | });
63 |
64 | modelBuilder
65 | .Entity()
66 | .HasMany(p => p.AttendedCourses)
67 | .WithMany(p => p.EmployeesThatAttended)
68 | .UsingEntity(j => j.ToTable("CourseInternalEmployee").HasData(new[]
69 | {
70 | new { AttendedCoursesId = Guid.Parse("37e03ca7-c730-4351-834c-b66f280cdb01"),
71 | EmployeesThatAttendedId = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb") },
72 | new { AttendedCoursesId = Guid.Parse("1fd115cf-f44c-4982-86bc-a8fe2e4ff83e"),
73 | EmployeesThatAttendedId = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb") },
74 | new { AttendedCoursesId = Guid.Parse("37e03ca7-c730-4351-834c-b66f280cdb01"),
75 | EmployeesThatAttendedId = Guid.Parse("f484ad8f-78fd-46d1-9f87-bbb1e676e37f") },
76 | new { AttendedCoursesId = Guid.Parse("1fd115cf-f44c-4982-86bc-a8fe2e4ff83e"),
77 | EmployeesThatAttendedId = Guid.Parse("f484ad8f-78fd-46d1-9f87-bbb1e676e37f") },
78 | new { AttendedCoursesId = Guid.Parse("844e14ce-c055-49e9-9610-855669c9859b"),
79 | EmployeesThatAttendedId = Guid.Parse("f484ad8f-78fd-46d1-9f87-bbb1e676e37f") }
80 | }
81 | ));
82 |
83 | modelBuilder.Entity()
84 | .HasData(
85 | new ExternalEmployee("Amanda", "Smith", "IT for Everyone, Inc")
86 | {
87 | Id = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb")
88 | });
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2021 The Bootstrap Authors
4 | * Copyright 2011-2021 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2021 The Bootstrap Authors
4 | * Copyright 2011-2021 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.min.css.map */
--------------------------------------------------------------------------------
/Starter files/EmployeeManagement/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2021 The Bootstrap Authors
4 | * Copyright 2011-2021 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.rtl.min.css:
--------------------------------------------------------------------------------
1 | /*!
2 | * Bootstrap Reboot v5.1.0 (https://getbootstrap.com/)
3 | * Copyright 2011-2021 The Bootstrap Authors
4 | * Copyright 2011-2021 Twitter, Inc.
5 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
6 | * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md)
7 | */*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){h1{font-size:2.5rem}}h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){h2{font-size:2rem}}h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){h3{font-size:1.75rem}}h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){h4{font-size:1.5rem}}h5{font-size:1.25rem}h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-right:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-right:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}small{font-size:.875em}mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:right}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:right;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:right}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}[type=email],[type=number],[type=tel],[type=url]{direction:ltr}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}
8 | /*# sourceMappingURL=bootstrap-reboot.rtl.min.css.map */
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement/Controllers/InternalEmployeeController.cs:
--------------------------------------------------------------------------------
1 | using AutoMapper;
2 | using EmployeeManagement.Business;
3 | using EmployeeManagement.ViewModels;
4 | using Microsoft.AspNetCore.Mvc;
5 |
6 | namespace EmployeeManagement.Controllers
7 | {
8 | public class InternalEmployeeController : Controller
9 | {
10 | private readonly IEmployeeService _employeeService;
11 | private readonly IMapper _mapper;
12 | private readonly IPromotionService _promotionService;
13 |
14 | public InternalEmployeeController(IEmployeeService employeeService,
15 | IMapper mapper,
16 | IPromotionService promotionService)
17 | {
18 | _employeeService = employeeService;
19 | _mapper = mapper;
20 | _promotionService = promotionService;
21 | }
22 |
23 | [HttpGet]
24 | public IActionResult AddInternalEmployee()
25 | {
26 | return View(new CreateInternalEmployeeViewModel());
27 | }
28 |
29 | [HttpPost]
30 | public async Task AddInternalEmployee(
31 | CreateInternalEmployeeViewModel model)
32 | {
33 | if (!ModelState.IsValid)
34 | {
35 | return BadRequest(ModelState);
36 | }
37 | else
38 | {
39 | // create an internal employee entity with default values filled out
40 | // and the values the user inputted
41 | var internalEmplooyee =
42 | await _employeeService.CreateInternalEmployeeAsync(
43 | model.FirstName, model.LastName);
44 |
45 | // persist it
46 | await _employeeService.AddInternalEmployeeAsync(internalEmplooyee);
47 | }
48 |
49 | return RedirectToAction("Index", "EmployeeOverview");
50 | }
51 |
52 | [HttpGet]
53 | public async Task InternalEmployeeDetails(
54 | [FromRoute(Name = "id")] Guid? employeeId)
55 | {
56 | if (!employeeId.HasValue)
57 | {
58 | if (Guid.TryParse(HttpContext?.Session?.GetString("EmployeeId"),
59 | out Guid employeeIdFromSession))
60 | {
61 | employeeId = employeeIdFromSession;
62 | }
63 | else if (Guid.TryParse(TempData["EmployeeId"]?.ToString(),
64 | out Guid employeeIdFromTempData))
65 | {
66 | employeeId = employeeIdFromTempData;
67 | }
68 | else
69 | {
70 | return RedirectToAction("Index", "EmployeeOverview");
71 | }
72 | }
73 |
74 | var internalEmployee = await _employeeService
75 | .FetchInternalEmployeeAsync(employeeId.Value);
76 | if (internalEmployee == null)
77 | {
78 | return RedirectToAction("Index", "EmployeeOverview");
79 | }
80 |
81 | return View(_mapper.Map(
82 | internalEmployee));
83 | }
84 |
85 | [HttpPost]
86 | public async Task ExecutePromotionRequest(
87 | [FromForm(Name = "id")] Guid? employeeId)
88 | {
89 | if (!employeeId.HasValue)
90 | {
91 | return RedirectToAction("Index", "EmployeeOverview");
92 | }
93 |
94 | var internalEmployee = await _employeeService
95 | .FetchInternalEmployeeAsync(employeeId.Value);
96 |
97 | if (internalEmployee == null)
98 | {
99 | return RedirectToAction("Index", "EmployeeOverview");
100 | }
101 |
102 | if (await _promotionService.PromoteInternalEmployeeAsync(
103 | internalEmployee))
104 | {
105 | ViewBag.PromotionRequestMessage = "Employee was promoted.";
106 |
107 | // get the updated employee values
108 | internalEmployee = await _employeeService
109 | .FetchInternalEmployeeAsync(employeeId.Value);
110 | }
111 | else
112 | {
113 | ViewBag.PromotionRequestMessage =
114 | "Sorry, this employee isn't eligible for promotion.";
115 | }
116 |
117 | return View("InternalEmployeeDetails",
118 | _mapper.Map(internalEmployee));
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/Finished solution/EmployeeManagement.Test/DataDrivenEmployeeServiceTests.cs:
--------------------------------------------------------------------------------
1 | using EmployeeManagement.DataAccess.Entities;
2 | using EmployeeManagement.Test.Fixtures;
3 | using EmployeeManagement.Test.TestData;
4 | using Xunit;
5 |
6 | namespace EmployeeManagement.Test
7 | {
8 | [Collection("EmployeeServiceCollection")]
9 | public class DataDrivenEmployeeServiceTests //: IClassFixture
10 | {
11 | private readonly EmployeeServiceFixture _employeeServiceFixture;
12 |
13 | public DataDrivenEmployeeServiceTests(
14 | EmployeeServiceFixture employeeServiceFixture)
15 | {
16 | _employeeServiceFixture = employeeServiceFixture;
17 | }
18 |
19 | [Theory]
20 | [InlineData("1fd115cf-f44c-4982-86bc-a8fe2e4ff83e")]
21 | [InlineData("37e03ca7-c730-4351-834c-b66f280cdb01")]
22 | public void CreateInternalEmployee_InternalEmployeeCreated_MustHaveAttendedSecondObligatoryCourse(
23 | Guid courseId)
24 | {
25 | // Arrange
26 |
27 | // Act
28 | var internalEmployee = _employeeServiceFixture.EmployeeService
29 | .CreateInternalEmployee("Brooklyn", "Cannon");
30 |
31 | // Assert
32 | Assert.Contains(internalEmployee.AttendedCourses,
33 | course => course.Id == courseId);
34 | }
35 |
36 | [Fact]
37 | public async Task GiveRaise_MinimumRaiseGiven_EmployeeMinimumRaiseGivenMustBeTrue()
38 | {
39 | // Arrange
40 | var internalEmployee = new InternalEmployee(
41 | "Brooklyn", "Cannon", 5, 3000, false, 1);
42 |
43 | // Act
44 | await _employeeServiceFixture
45 | .EmployeeService.GiveRaiseAsync(internalEmployee, 100);
46 |
47 | // Assert
48 | Assert.True(internalEmployee.MinimumRaiseGiven);
49 | }
50 |
51 |
52 | [Fact]
53 | public async Task GiveRaise_MoreThanMinimumRaiseGiven_EmployeeMinimumRaiseGivenMustBeFalse()
54 | {
55 | // Arrange
56 | var internalEmployee = new InternalEmployee(
57 | "Brooklyn", "Cannon", 5, 3000, false, 1);
58 |
59 | // Act
60 | await _employeeServiceFixture.EmployeeService
61 | .GiveRaiseAsync(internalEmployee, 200);
62 |
63 | // Assert
64 | Assert.False(internalEmployee.MinimumRaiseGiven);
65 | }
66 |
67 | public static IEnumerable