├── .gitignore ├── Additional files used during course ├── CompanyFramework.Test │ ├── ClassForTestingTests.cs │ └── CompanyFramework.Test.csproj ├── CompanyFramework │ ├── ClassForTesting.cs │ └── CompanyFramework.csproj └── EmployeeTestDataRepository.cs ├── Finished solution ├── CompanyFramework.Test │ ├── ClassForTestingTests.cs │ └── CompanyFramework.Test.csproj ├── CompanyFramework │ ├── ClassForTesting.cs │ └── CompanyFramework.csproj ├── EmployeeManagement.Test │ ├── AnotherTestClass.cs │ ├── CheckShowStatisticsHeaderTests.cs │ ├── CourseTests.cs │ ├── DataDrivenEmployeeServiceTests.cs │ ├── EmployeeFactoryTests.cs │ ├── EmployeeManagement.Test.csproj │ ├── EmployeeManagementSecurityHeadersMiddlewareTests.cs │ ├── EmployeeOverviewControllerTests.cs │ ├── EmployeeOverviewTests.cs │ ├── EmployeeServiceTests.cs │ ├── EmployeeServiceTestsWithAspNetCoreDI.cs │ ├── EmployeeTests.cs │ ├── Fixtures │ │ ├── EmployeeServiceCollectionFixture.cs │ │ ├── EmployeeServiceFixture.cs │ │ └── EmployeeServiceWithAspNetCoreDIFixture.cs │ ├── HttpMessageHandlers │ │ └── TestablePromotionElibilityHandler.cs │ ├── InternalEmployeeControllerTests.cs │ ├── MoqTests.cs │ ├── ServiceCollectionTests.cs │ ├── Services │ │ └── EmployeeTestDataRepository.cs │ ├── StatisticsControllerTests.cs │ ├── TestData │ │ ├── EmployeeServiceTestData.cs │ │ ├── EmployeeServiceTestData.csv │ │ ├── StronglyTypedEmployeeServiceTestData.cs │ │ └── StronglyTypedEmployeeServiceTestData_FromFile.cs │ ├── TestIsolationApproachesTests.cs │ └── appsettings.json ├── EmployeeManagement.sln ├── EmployeeManagement │ ├── ActionFilters │ │ └── CheckShowStatisticsHeader.cs │ ├── Business │ │ ├── EmployeeFactory.cs │ │ ├── EmployeeService.cs │ │ ├── EventArguments │ │ │ └── EmployeeIsAbsentEventArgs.cs │ │ ├── Exceptions │ │ │ └── EmployeeInvalidRaiseException.cs │ │ ├── IEmployeeService.cs │ │ ├── IPromotionService.cs │ │ ├── PromotionEligibility.cs │ │ └── PromotionService.cs │ ├── Controllers │ │ ├── EmployeeOverviewController.cs │ │ ├── InternalEmployeeController.cs │ │ └── StatisticsController.cs │ ├── DataAccess │ │ ├── DbContexts │ │ │ └── EmployeeDbContext.cs │ │ ├── Entities │ │ │ ├── Course.cs │ │ │ ├── Employee.cs │ │ │ ├── ExternalEmployee.cs │ │ │ └── InternalEmployee.cs │ │ └── Services │ │ │ ├── EmployeeManagementRepository.cs │ │ │ └── IEmployeeManagementRepository.cs │ ├── EmployeeManagement.csproj │ ├── EmployeeManagement.db │ ├── MapperProfiles │ │ ├── CourseProfile.cs │ │ ├── EmployeeProfile.cs │ │ └── StatisticsProfile.cs │ ├── Middleware │ │ └── EmployeeManagementSecurityHeadersMiddleware.cs │ ├── Migrations │ │ ├── EmployeeDbContextModelSnapshot.cs │ │ ├── InitialMigration.Designer.cs │ │ └── InitialMigration.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── ServiceRegistrationExtensions.cs │ ├── ViewModels │ │ ├── CourseViewModel.cs │ │ ├── CreateInternalEmployeeViewModel.cs │ │ ├── EmployeeOverviewViewModel.cs │ │ ├── ErrorViewModel.cs │ │ ├── InternalEmployeeDetailViewModel.cs │ │ ├── InternalEmployeeForOverviewViewModel.cs │ │ └── StatisticsViewModel.cs │ ├── Views │ │ ├── EmployeeOverview │ │ │ └── Index.cshtml │ │ ├── InternalEmployee │ │ │ ├── AddInternalEmployee.cshtml │ │ │ └── InternalEmployeeDetails.cshtml │ │ ├── Shared │ │ │ ├── Error.cshtml │ │ │ ├── _Layout.cshtml │ │ │ ├── _Layout.cshtml.css │ │ │ └── _ValidationScriptsPartial.cshtml │ │ ├── Statistics │ │ │ └── Index.cshtml │ │ ├── _ViewImports.cshtml │ │ └── _ViewStart.cshtml │ ├── appsettings.Development.json │ ├── appsettings.json │ └── wwwroot │ │ ├── css │ │ └── site.css │ │ ├── favicon.ico │ │ ├── js │ │ └── site.js │ │ └── lib │ │ ├── bootstrap │ │ ├── LICENSE │ │ └── dist │ │ │ ├── css │ │ │ ├── bootstrap-grid.css │ │ │ ├── bootstrap-grid.css.map │ │ │ ├── bootstrap-grid.min.css │ │ │ ├── bootstrap-grid.min.css.map │ │ │ ├── bootstrap-grid.rtl.css │ │ │ ├── bootstrap-grid.rtl.css.map │ │ │ ├── bootstrap-grid.rtl.min.css │ │ │ ├── bootstrap-grid.rtl.min.css.map │ │ │ ├── bootstrap-reboot.css │ │ │ ├── bootstrap-reboot.css.map │ │ │ ├── bootstrap-reboot.min.css │ │ │ ├── bootstrap-reboot.min.css.map │ │ │ ├── bootstrap-reboot.rtl.css │ │ │ ├── bootstrap-reboot.rtl.css.map │ │ │ ├── bootstrap-reboot.rtl.min.css │ │ │ ├── bootstrap-reboot.rtl.min.css.map │ │ │ ├── bootstrap-utilities.css │ │ │ ├── bootstrap-utilities.css.map │ │ │ ├── bootstrap-utilities.min.css │ │ │ ├── bootstrap-utilities.min.css.map │ │ │ ├── bootstrap-utilities.rtl.css │ │ │ ├── bootstrap-utilities.rtl.css.map │ │ │ ├── bootstrap-utilities.rtl.min.css │ │ │ ├── bootstrap-utilities.rtl.min.css.map │ │ │ ├── bootstrap.css │ │ │ ├── bootstrap.css.map │ │ │ ├── bootstrap.min.css │ │ │ ├── bootstrap.min.css.map │ │ │ ├── bootstrap.rtl.css │ │ │ ├── bootstrap.rtl.css.map │ │ │ ├── bootstrap.rtl.min.css │ │ │ └── bootstrap.rtl.min.css.map │ │ │ └── js │ │ │ ├── bootstrap.bundle.js │ │ │ ├── bootstrap.bundle.js.map │ │ │ ├── bootstrap.bundle.min.js │ │ │ ├── bootstrap.bundle.min.js.map │ │ │ ├── bootstrap.esm.js │ │ │ ├── bootstrap.esm.js.map │ │ │ ├── bootstrap.esm.min.js │ │ │ ├── bootstrap.esm.min.js.map │ │ │ ├── bootstrap.js │ │ │ ├── bootstrap.js.map │ │ │ ├── bootstrap.min.js │ │ │ └── bootstrap.min.js.map │ │ ├── jquery-validation-unobtrusive │ │ ├── LICENSE.txt │ │ ├── jquery.validate.unobtrusive.js │ │ └── jquery.validate.unobtrusive.min.js │ │ ├── jquery-validation │ │ ├── LICENSE.md │ │ └── dist │ │ │ ├── additional-methods.js │ │ │ ├── additional-methods.min.js │ │ │ ├── jquery.validate.js │ │ │ └── jquery.validate.min.js │ │ └── jquery │ │ ├── LICENSE.txt │ │ └── dist │ │ ├── jquery.js │ │ ├── jquery.min.js │ │ └── jquery.min.map └── TopLevelManagement │ ├── Controllers │ └── PromotionEligibilitiesController.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── TopLevelManagement.csproj │ ├── appsettings.Development.json │ └── appsettings.json ├── LICENSE ├── README.md └── Starter files ├── EmployeeManagement.sln ├── EmployeeManagement ├── ActionFilters │ └── CheckShowStatisticsHeader.cs ├── Business │ ├── EmployeeFactory.cs │ ├── EmployeeService.cs │ ├── EventArguments │ │ └── EmployeeIsAbsentEventArgs.cs │ ├── Exceptions │ │ └── EmployeeInvalidRaiseException.cs │ ├── IEmployeeService.cs │ ├── IPromotionService.cs │ ├── PromotionEligibility.cs │ └── PromotionService.cs ├── Controllers │ ├── EmployeeOverviewController.cs │ ├── InternalEmployeeController.cs │ └── StatisticsController.cs ├── DataAccess │ ├── DbContexts │ │ └── EmployeeDbContext.cs │ ├── Entities │ │ ├── Course.cs │ │ ├── Employee.cs │ │ ├── ExternalEmployee.cs │ │ └── InternalEmployee.cs │ └── Services │ │ ├── EmployeeManagementRepository.cs │ │ └── IEmployeeManagementRepository.cs ├── EmployeeManagement.csproj ├── EmployeeManagement.db ├── MapperProfiles │ ├── CourseProfile.cs │ ├── EmployeeProfile.cs │ └── StatisticsProfile.cs ├── Middleware │ └── EmployeeManagementSecurityHeadersMiddleware.cs ├── Migrations │ ├── EmployeeDbContextModelSnapshot.cs │ ├── InitialMigration.Designer.cs │ └── InitialMigration.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── ServiceRegistrationExtensions.cs ├── ViewModels │ ├── CourseViewModel.cs │ ├── CreateInternalEmployeeViewModel.cs │ ├── EmployeeOverviewViewModel.cs │ ├── ErrorViewModel.cs │ ├── InternalEmployeeDetailViewModel.cs │ ├── InternalEmployeeForOverviewViewModel.cs │ └── StatisticsViewModel.cs ├── Views │ ├── EmployeeOverview │ │ └── Index.cshtml │ ├── InternalEmployee │ │ ├── AddInternalEmployee.cshtml │ │ └── InternalEmployeeDetails.cshtml │ ├── Shared │ │ ├── Error.cshtml │ │ ├── _Layout.cshtml │ │ ├── _Layout.cshtml.css │ │ └── _ValidationScriptsPartial.cshtml │ ├── Statistics │ │ └── Index.cshtml │ ├── _ViewImports.cshtml │ └── _ViewStart.cshtml ├── appsettings.Development.json ├── appsettings.json └── wwwroot │ ├── css │ └── site.css │ ├── favicon.ico │ ├── js │ └── site.js │ └── lib │ ├── bootstrap │ ├── LICENSE │ └── dist │ │ ├── css │ │ ├── bootstrap-grid.css │ │ ├── bootstrap-grid.css.map │ │ ├── bootstrap-grid.min.css │ │ ├── bootstrap-grid.min.css.map │ │ ├── bootstrap-grid.rtl.css │ │ ├── bootstrap-grid.rtl.css.map │ │ ├── bootstrap-grid.rtl.min.css │ │ ├── bootstrap-grid.rtl.min.css.map │ │ ├── bootstrap-reboot.css │ │ ├── bootstrap-reboot.css.map │ │ ├── bootstrap-reboot.min.css │ │ ├── bootstrap-reboot.min.css.map │ │ ├── bootstrap-reboot.rtl.css │ │ ├── bootstrap-reboot.rtl.css.map │ │ ├── bootstrap-reboot.rtl.min.css │ │ ├── bootstrap-reboot.rtl.min.css.map │ │ ├── bootstrap-utilities.css │ │ ├── bootstrap-utilities.css.map │ │ ├── bootstrap-utilities.min.css │ │ ├── bootstrap-utilities.min.css.map │ │ ├── bootstrap-utilities.rtl.css │ │ ├── bootstrap-utilities.rtl.css.map │ │ ├── bootstrap-utilities.rtl.min.css │ │ ├── bootstrap-utilities.rtl.min.css.map │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css │ │ ├── bootstrap.min.css.map │ │ ├── bootstrap.rtl.css │ │ ├── bootstrap.rtl.css.map │ │ ├── bootstrap.rtl.min.css │ │ └── bootstrap.rtl.min.css.map │ │ └── js │ │ ├── bootstrap.bundle.js │ │ ├── bootstrap.bundle.js.map │ │ ├── bootstrap.bundle.min.js │ │ ├── bootstrap.bundle.min.js.map │ │ ├── bootstrap.esm.js │ │ ├── bootstrap.esm.js.map │ │ ├── bootstrap.esm.min.js │ │ ├── bootstrap.esm.min.js.map │ │ ├── bootstrap.js │ │ ├── bootstrap.js.map │ │ ├── bootstrap.min.js │ │ └── bootstrap.min.js.map │ ├── jquery-validation-unobtrusive │ ├── LICENSE.txt │ ├── jquery.validate.unobtrusive.js │ └── jquery.validate.unobtrusive.min.js │ ├── jquery-validation │ ├── LICENSE.md │ └── dist │ │ ├── additional-methods.js │ │ ├── additional-methods.min.js │ │ ├── jquery.validate.js │ │ └── jquery.validate.min.js │ └── jquery │ ├── LICENSE.txt │ └── dist │ ├── jquery.js │ ├── jquery.min.js │ └── jquery.min.map └── TopLevelManagement ├── Controllers └── PromotionEligibilitiesController.cs ├── Program.cs ├── Properties └── launchSettings.json ├── TopLevelManagement.csproj ├── appsettings.Development.json └── appsettings.json /Additional files used during course/CompanyFramework.Test/ClassForTestingTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace CompanyFramework.Test 4 | { 5 | public class ClassForTestingTests 6 | { 7 | [Fact] 8 | public void MethodForTesting_Execute_ReturnsTrue() 9 | { 10 | // Arrange 11 | var classForTesting = new ClassForTesting(); 12 | 13 | // Act 14 | var result = classForTesting.MethodForTesting(); 15 | 16 | // Assert 17 | Assert.True(result); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Additional files used during course/CompanyFramework/ClassForTesting.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace CompanyFramework 4 | { 5 | public class ClassForTesting 6 | { 7 | public bool MethodForTesting() 8 | { 9 | Thread.Sleep(4000); 10 | return true; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Additional files used during course/CompanyFramework/CompanyFramework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | enable 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Additional files used during course/EmployeeTestDataRepository.cs: -------------------------------------------------------------------------------- 1 | using EmployeeManagement.DataAccess.Entities; 2 | using EmployeeManagement.DataAccess.Services; 3 | using System.Threading.Tasks; 4 | 5 | namespace EmployeeManagement.Services.Test 6 | { 7 | public class EmployeeManagementTestDataRepository : IEmployeeManagementRepository 8 | { 9 | private List _internalEmployees; 10 | private List _externalEmployees; 11 | private List _courses; 12 | 13 | public EmployeeManagementTestDataRepository() 14 | { 15 | // mimic expensive creation process 16 | Thread.Sleep(3000); 17 | 18 | // initialize with dummy data 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 | _courses = new() 38 | { 39 | obligatoryCourse1, 40 | obligatoryCourse2, 41 | optionalCourse1, 42 | new Course("Dealing with Customers - Advanced") 43 | { 44 | Id = Guid.Parse("d6e0e4b7-9365-4332-9b29-bb7bf09664a6"), 45 | IsNew = false 46 | }, 47 | new Course("Disaster Management 101") 48 | { 49 | Id = Guid.Parse("cbf6db3b-c4ee-46aa-9457-5fa8aefef33a"), 50 | IsNew = false 51 | } 52 | }; 53 | 54 | _internalEmployees = new() 55 | { 56 | new InternalEmployee("Megan", "Jones", 2, 3000, false, 2) 57 | { 58 | Id = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb"), 59 | AttendedCourses = new List { 60 | obligatoryCourse1, obligatoryCourse2 } 61 | }, 62 | new InternalEmployee("Jaimy", "Johnson", 3, 3400, true, 1) 63 | { 64 | Id = Guid.Parse("f484ad8f-78fd-46d1-9f87-bbb1e676e37f"), 65 | AttendedCourses = new List { 66 | obligatoryCourse1, obligatoryCourse2, optionalCourse1 } 67 | } 68 | }; 69 | 70 | _externalEmployees = new() 71 | { 72 | new ExternalEmployee("Amanda", "Smith", "IT for Everyone, Inc") 73 | { 74 | Id = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb") 75 | } 76 | }; 77 | } 78 | 79 | public void AddInternalEmployee(InternalEmployee internalEmployee) 80 | { 81 | // empty on purpose 82 | } 83 | 84 | public Course? GetCourse(Guid courseId) 85 | { 86 | return _courses.FirstOrDefault(c => c.Id == courseId); 87 | } 88 | 89 | public Task GetCourseAsync(Guid courseId) 90 | { 91 | return Task.FromResult(GetCourse(courseId)); 92 | } 93 | 94 | public List GetCourses(params Guid[] courseIds) 95 | { 96 | List coursesToReturn = new(); 97 | foreach (var courseId in courseIds) 98 | { 99 | var course = GetCourse(courseId); 100 | if (course != null) 101 | { 102 | coursesToReturn.Add(course); 103 | } 104 | } 105 | return coursesToReturn; 106 | } 107 | 108 | public Task> GetCoursesAsync(params Guid[] courseIds) 109 | { 110 | return Task.FromResult(GetCourses(courseIds)); 111 | } 112 | 113 | public InternalEmployee? GetInternalEmployee(Guid employeeId) 114 | { 115 | return _internalEmployees.FirstOrDefault(e => e.Id == employeeId); 116 | } 117 | 118 | public Task GetInternalEmployeeAsync(Guid employeeId) 119 | { 120 | return Task.FromResult(GetInternalEmployee(employeeId)); 121 | } 122 | 123 | public Task> GetInternalEmployeesAsync() 124 | { 125 | return Task.FromResult(_internalEmployees.AsEnumerable()); 126 | } 127 | 128 | public Task SaveChangesAsync() 129 | { 130 | // nothing to do here 131 | return Task.CompletedTask; 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /Finished solution/CompanyFramework.Test/ClassForTestingTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace CompanyFramework.Test 4 | { 5 | public class ClassForTestingTests 6 | { 7 | [Fact] 8 | public void MethodForTesting_Execute_ReturnsTrue() 9 | { 10 | // Arrange 11 | var classForTesting = new ClassForTesting(); 12 | 13 | // Act 14 | var result = classForTesting.MethodForTesting(); 15 | 16 | // Assert 17 | Assert.True(result); 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Finished solution/CompanyFramework/ClassForTesting.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | 3 | namespace CompanyFramework 4 | { 5 | public class ClassForTesting 6 | { 7 | public bool MethodForTesting() 8 | { 9 | Thread.Sleep(4000); 10 | return true; 11 | } 12 | } 13 | } -------------------------------------------------------------------------------- /Finished solution/CompanyFramework/CompanyFramework.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | enable 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement.Test/AnotherTestClass.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace EmployeeManagement.Test 4 | { 5 | [Collection("No parallelism")] 6 | public class AnotherTestClass 7 | { 8 | [Fact] 9 | public void SlowTest2() 10 | { 11 | Thread.Sleep(5000); 12 | Assert.True(true); 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement.Test/CourseTests.cs: -------------------------------------------------------------------------------- 1 | using EmployeeManagement.DataAccess.Entities; 2 | using Xunit; 3 | 4 | namespace EmployeeManagement.Test 5 | { 6 | public class CourseTests 7 | { 8 | [Fact] 9 | public void CourseConstructor_ConstructCourse_IsNewMustBeTrue() 10 | { 11 | // Arrange 12 | // nothing to see here 13 | 14 | // Act 15 | var course = new Course("Disaster Management 101"); 16 | 17 | // Assert 18 | Assert.True(course.IsNew); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 ExampleTestDataForGiveRaise_WithProperty 68 | { 69 | get 70 | { 71 | return new List 72 | { 73 | new object[] { 100, true }, 74 | new object[] { 200, false } 75 | }; 76 | } 77 | } 78 | 79 | public static TheoryData StronglyTypedExampleTestDataForGiveRaise_WithProperty 80 | { 81 | get 82 | { 83 | return new TheoryData() 84 | { 85 | { 100, true }, 86 | { 200, false } 87 | }; 88 | } 89 | } 90 | 91 | public static IEnumerable ExampleTestDataForGiveRaise_WithMethod( 92 | int testDataInstancesToProvide) 93 | { 94 | var testData = new List 95 | { 96 | new object[] { 100, true }, 97 | new object[] { 200, false } 98 | }; 99 | 100 | return testData.Take(testDataInstancesToProvide); 101 | } 102 | 103 | 104 | [Theory] 105 | //[MemberData( 106 | // nameof(DataDrivenEmployeeServiceTests.ExampleTestDataForGiveRaise_WithMethod), 107 | // 1, 108 | // MemberType = typeof(DataDrivenEmployeeServiceTests))] 109 | //[ClassData(typeof(EmployeeServiceTestData))] 110 | //[ClassData(typeof(StronglyTypedEmployeeServiceTestData))] 111 | //[MemberData(nameof(StronglyTypedExampleTestDataForGiveRaise_WithProperty))] 112 | [ClassData(typeof(StronglyTypedEmployeeServiceTestData_FromFile))] 113 | public async Task GiveRaise_RaiseGiven_EmployeeMinimumRaiseGivenMatchesValue( 114 | int raiseGiven, bool expectedValueForMinimumRaiseGiven) 115 | { 116 | // Arrange 117 | var internalEmployee = new InternalEmployee( 118 | "Brooklyn", "Cannon", 5, 3000, false, 1); 119 | 120 | // Act 121 | await _employeeServiceFixture.EmployeeService.GiveRaiseAsync( 122 | internalEmployee, raiseGiven); 123 | 124 | // Assert 125 | Assert.Equal(expectedValueForMinimumRaiseGiven, 126 | internalEmployee.MinimumRaiseGiven); 127 | } 128 | 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.Test/EmployeeOverviewControllerTests.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EmployeeManagement.Business; 3 | using EmployeeManagement.Controllers; 4 | using EmployeeManagement.DataAccess.Entities; 5 | using EmployeeManagement.ViewModels; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Moq; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.Linq; 11 | using System.Reflection; 12 | using System.Text; 13 | using System.Threading.Tasks; 14 | using Xunit; 15 | 16 | namespace EmployeeManagement.Test 17 | { 18 | public class EmployeeOverviewControllerTests 19 | { 20 | private readonly EmployeeOverviewController _employeeOverviewController; 21 | private readonly InternalEmployee _firstEmployee; 22 | public EmployeeOverviewControllerTests() 23 | { 24 | _firstEmployee = new InternalEmployee("Megan", "Jones", 2, 3000, false, 2) 25 | { 26 | Id = Guid.Parse("bfdd0acd-d314-48d5-a7ad-0e94dfdd9155"), 27 | SuggestedBonus = 400 28 | }; 29 | 30 | var employeeServiceMock = new Mock(); 31 | employeeServiceMock 32 | .Setup(m => m.FetchInternalEmployeesAsync()) 33 | .ReturnsAsync(new List() { 34 | _firstEmployee, 35 | new InternalEmployee("Jaimy", "Johnson", 3, 3400, true, 1) 36 | { 37 | Id = Guid.Parse("7183748a-ebeb-4355-8084-f190f8a5a68f"), 38 | SuggestedBonus = 500 39 | }, 40 | new InternalEmployee("Anne", "Adams", 3, 4000, false, 3) 41 | { 42 | Id = Guid.Parse("e2bba7cf-dca7-433d-9ba6-79c834a02c48"), 43 | SuggestedBonus = 600 44 | } 45 | }); 46 | 47 | //var mapperMock = new Mock(); 48 | //mapperMock.Setup(m => 49 | // m.Map 50 | // (It.IsAny())) 51 | // .Returns(new ViewModels.InternalEmployeeForOverviewViewModel()); 52 | 53 | //_employeeOverviewController = new EmployeeOverviewController( 54 | // employeeServiceMock.Object, mapperMock.Object); 55 | 56 | var mapperConfiguration = new MapperConfiguration( 57 | cfg => cfg.AddProfile()); 58 | var mapper = new Mapper(mapperConfiguration); 59 | 60 | _employeeOverviewController = new EmployeeOverviewController( 61 | employeeServiceMock.Object, mapper); 62 | } 63 | 64 | 65 | [Fact] 66 | public async Task Index_GetAction_MustReturnViewResult() 67 | { 68 | // Arrange 69 | 70 | // Act 71 | var result = await _employeeOverviewController.Index(); 72 | 73 | // Assert 74 | Assert.IsType(result); 75 | } 76 | 77 | [Fact] 78 | public async Task Index_GetAction_MustReturnEmployeeOverviewViewModelAsModelType() 79 | { 80 | // Arrange 81 | 82 | // Act 83 | var result = await _employeeOverviewController.Index(); 84 | 85 | // Assert 86 | Assert.IsAssignableFrom( 87 | ((ViewResult)result).Model); 88 | } 89 | 90 | [Fact] 91 | public async Task Index_GetAction_MustReturnNumberOfInputtedInternalEmployees() 92 | { 93 | // Arrange 94 | 95 | // Act 96 | var result = await _employeeOverviewController.Index(); 97 | 98 | // Assert 99 | Assert.Equal(3, ((EmployeeOverviewViewModel)((ViewResult)result).Model) 100 | .InternalEmployees.Count); 101 | } 102 | 103 | [Fact] 104 | public async Task Index_GetAction_ReturnsViewResultWithInternalEmployees() 105 | { 106 | // Arrange 107 | 108 | // Act 109 | var result = await _employeeOverviewController.Index(); 110 | 111 | // Assert 112 | var viewResult = Assert.IsType(result); 113 | var viewModel = Assert.IsAssignableFrom(viewResult.Model); 114 | Assert.Equal(3, viewModel.InternalEmployees.Count); 115 | var firstEmployee = viewModel.InternalEmployees[0]; 116 | Assert.Equal(_firstEmployee.Id, firstEmployee.Id); 117 | Assert.Equal(_firstEmployee.FirstName, firstEmployee.FirstName); 118 | Assert.Equal(_firstEmployee.LastName, firstEmployee.LastName); 119 | Assert.Equal(_firstEmployee.Salary, firstEmployee.Salary); 120 | Assert.Equal(_firstEmployee.SuggestedBonus, firstEmployee.SuggestedBonus); 121 | Assert.Equal(_firstEmployee.YearsInService, firstEmployee.YearsInService); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement.Test/Fixtures/EmployeeServiceCollectionFixture.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Xunit; 7 | 8 | namespace EmployeeManagement.Test.Fixtures 9 | { 10 | [CollectionDefinition("EmployeeServiceCollection")] 11 | public class EmployeeServiceCollectionFixture 12 | : ICollectionFixture 13 | { 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.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.Test/MoqTests.cs: -------------------------------------------------------------------------------- 1 | using EmployeeManagement.Business; 2 | using EmployeeManagement.DataAccess.Entities; 3 | using EmployeeManagement.DataAccess.Services; 4 | using EmployeeManagement.Services.Test; 5 | using Moq; 6 | using Xunit; 7 | 8 | namespace EmployeeManagement.Test 9 | { 10 | public class MoqTests 11 | { 12 | [Fact] 13 | public void FetchInternalEmployee_EmployeeFetched_SuggestedBonusMustBeCalculated() 14 | { 15 | // Arrange 16 | var employeeManagementTestDataRepository = 17 | new EmployeeManagementTestDataRepository(); 18 | //var employeeFactory = new EmployeeFactory(); 19 | var employeeFactoryMock = new Mock(); 20 | var employeeService = new EmployeeService( 21 | employeeManagementTestDataRepository, 22 | employeeFactoryMock.Object); 23 | 24 | // Act 25 | var employee = employeeService.FetchInternalEmployee( 26 | Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb")); 27 | 28 | // Assert 29 | Assert.Equal(400, employee.SuggestedBonus); 30 | } 31 | 32 | [Fact] 33 | public void CreateInternalEmployee_InternalEmployeeCreated_SuggestedBonusMustBeCalculated() 34 | { 35 | // Arrange 36 | var employeeManagementTestDataRepository = 37 | new EmployeeManagementTestDataRepository(); 38 | var employeeFactoryMock = new Mock(); 39 | employeeFactoryMock.Setup(m => 40 | m.CreateEmployee( 41 | "Kevin", 42 | It.IsAny(), 43 | null, 44 | false)) 45 | .Returns(new InternalEmployee("Kevin", "Dockx", 5, 2500, false, 1)); 46 | 47 | employeeFactoryMock.Setup(m => 48 | m.CreateEmployee( 49 | "Sandy", 50 | It.IsAny(), 51 | null, 52 | false)) 53 | .Returns(new InternalEmployee("Sandy", "Dockx", 0, 3000, false, 1)); 54 | 55 | employeeFactoryMock.Setup(m => 56 | m.CreateEmployee( 57 | It.Is(value => value.Contains("a")), 58 | It.IsAny(), 59 | null, 60 | false)) 61 | .Returns(new InternalEmployee("SomeoneWithAna", "Dockx", 0, 3000, false, 1)); 62 | 63 | var employeeService = new EmployeeService( 64 | employeeManagementTestDataRepository, 65 | employeeFactoryMock.Object); 66 | 67 | // suggested bonus for new employees = 68 | // (years in service if > 0) * attended courses * 100 69 | decimal suggestedBonus = 1000; 70 | 71 | // Act 72 | var employee = employeeService.CreateInternalEmployee("Sandy", "Dockx"); 73 | 74 | // Assert 75 | Assert.Equal(suggestedBonus, employee.SuggestedBonus); 76 | } 77 | 78 | [Fact] 79 | public void FetchInternalEmployee_EmployeeFetched_SuggestedBonusMustBeCalculated_MoqInterface() 80 | { 81 | // Arrange 82 | //var employeeManagementTestDataRepository = 83 | // new EmployeeManagementTestDataRepository(); 84 | var employeeManagementTestDataRepositoryMock = 85 | new Mock(); 86 | 87 | employeeManagementTestDataRepositoryMock 88 | .Setup(m => m.GetInternalEmployee(It.IsAny())) 89 | .Returns(new InternalEmployee("Tony", "Hall", 2, 2500, false, 2) 90 | { 91 | AttendedCourses = new List() { 92 | new Course("A course"), new Course("Another course") } 93 | }); 94 | 95 | var employeeFactoryMock = new Mock(); 96 | var employeeService = new EmployeeService( 97 | employeeManagementTestDataRepositoryMock.Object, 98 | employeeFactoryMock.Object); 99 | 100 | // Act 101 | var employee = employeeService.FetchInternalEmployee( 102 | Guid.Empty); 103 | 104 | // Assert 105 | Assert.Equal(400, employee.SuggestedBonus); 106 | } 107 | 108 | [Fact] 109 | public async Task FetchInternalEmployee_EmployeeFetched_SuggestedBonusMustBeCalculated_MoqInterface_Async() 110 | { 111 | // Arrange 112 | var employeeManagementTestDataRepositoryMock = 113 | new Mock(); 114 | 115 | employeeManagementTestDataRepositoryMock 116 | .Setup(m => m.GetInternalEmployeeAsync(It.IsAny())) 117 | .ReturnsAsync(new InternalEmployee("Tony", "Hall", 2, 2500, false, 2) 118 | { 119 | AttendedCourses = new List() { 120 | new Course("A course"), new Course("Another course") } 121 | }); 122 | 123 | var employeeFactoryMock = new Mock(); 124 | var employeeService = new EmployeeService( 125 | employeeManagementTestDataRepositoryMock.Object, 126 | employeeFactoryMock.Object); 127 | 128 | // Act 129 | var employee = await employeeService.FetchInternalEmployeeAsync(Guid.Empty); 130 | 131 | // Assert 132 | Assert.Equal(400, employee.SuggestedBonus); 133 | } 134 | 135 | 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement.Test/Services/EmployeeTestDataRepository.cs: -------------------------------------------------------------------------------- 1 | using EmployeeManagement.DataAccess.Entities; 2 | using EmployeeManagement.DataAccess.Services; 3 | using System.Threading.Tasks; 4 | 5 | namespace EmployeeManagement.Services.Test 6 | { 7 | public class EmployeeManagementTestDataRepository : IEmployeeManagementRepository 8 | { 9 | private List _internalEmployees; 10 | private List _externalEmployees; 11 | private List _courses; 12 | 13 | public EmployeeManagementTestDataRepository() 14 | { 15 | // mimic expensive creation process 16 | Thread.Sleep(3000); 17 | 18 | // initialize with dummy data 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 | _courses = new() 38 | { 39 | obligatoryCourse1, 40 | obligatoryCourse2, 41 | optionalCourse1, 42 | new Course("Dealing with Customers - Advanced") 43 | { 44 | Id = Guid.Parse("d6e0e4b7-9365-4332-9b29-bb7bf09664a6"), 45 | IsNew = false 46 | }, 47 | new Course("Disaster Management 101") 48 | { 49 | Id = Guid.Parse("cbf6db3b-c4ee-46aa-9457-5fa8aefef33a"), 50 | IsNew = false 51 | } 52 | }; 53 | 54 | _internalEmployees = new() 55 | { 56 | new InternalEmployee("Megan", "Jones", 2, 3000, false, 2) 57 | { 58 | Id = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb"), 59 | AttendedCourses = new List { 60 | obligatoryCourse1, obligatoryCourse2 } 61 | }, 62 | new InternalEmployee("Jaimy", "Johnson", 3, 3400, true, 1) 63 | { 64 | Id = Guid.Parse("f484ad8f-78fd-46d1-9f87-bbb1e676e37f"), 65 | AttendedCourses = new List { 66 | obligatoryCourse1, obligatoryCourse2, optionalCourse1 } 67 | } 68 | }; 69 | 70 | _externalEmployees = new() 71 | { 72 | new ExternalEmployee("Amanda", "Smith", "IT for Everyone, Inc") 73 | { 74 | Id = Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb") 75 | } 76 | }; 77 | } 78 | 79 | public void AddInternalEmployee(InternalEmployee internalEmployee) 80 | { 81 | // empty on purpose 82 | } 83 | 84 | public Course? GetCourse(Guid courseId) 85 | { 86 | return _courses.FirstOrDefault(c => c.Id == courseId); 87 | } 88 | 89 | public Task GetCourseAsync(Guid courseId) 90 | { 91 | return Task.FromResult(GetCourse(courseId)); 92 | } 93 | 94 | public List GetCourses(params Guid[] courseIds) 95 | { 96 | List coursesToReturn = new(); 97 | foreach (var courseId in courseIds) 98 | { 99 | var course = GetCourse(courseId); 100 | if (course != null) 101 | { 102 | coursesToReturn.Add(course); 103 | } 104 | } 105 | return coursesToReturn; 106 | } 107 | 108 | public Task> GetCoursesAsync(params Guid[] courseIds) 109 | { 110 | return Task.FromResult(GetCourses(courseIds)); 111 | } 112 | 113 | public InternalEmployee? GetInternalEmployee(Guid employeeId) 114 | { 115 | return _internalEmployees.FirstOrDefault(e => e.Id == employeeId); 116 | } 117 | 118 | public Task GetInternalEmployeeAsync(Guid employeeId) 119 | { 120 | return Task.FromResult(GetInternalEmployee(employeeId)); 121 | } 122 | 123 | public Task> GetInternalEmployeesAsync() 124 | { 125 | return Task.FromResult(_internalEmployees.AsEnumerable()); 126 | } 127 | 128 | public Task SaveChangesAsync() 129 | { 130 | // nothing to do here 131 | return Task.CompletedTask; 132 | } 133 | } 134 | } -------------------------------------------------------------------------------- /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.Test/TestData/EmployeeServiceTestData.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace EmployeeManagement.Test.TestData 4 | { 5 | public class EmployeeServiceTestData : IEnumerable 6 | { 7 | public IEnumerator GetEnumerator() 8 | { 9 | yield return new object[] { 100, true }; 10 | yield return new object[] { 200, false }; 11 | } 12 | 13 | IEnumerator IEnumerable.GetEnumerator() 14 | { 15 | return GetEnumerator(); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement.Test/TestData/EmployeeServiceTestData.csv: -------------------------------------------------------------------------------- 1 | 100,true 2 | 200,false -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement.Test/TestData/StronglyTypedEmployeeServiceTestData.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace EmployeeManagement.Test.TestData 4 | { 5 | public class StronglyTypedEmployeeServiceTestData : TheoryData 6 | { 7 | public StronglyTypedEmployeeServiceTestData() 8 | { 9 | Add(100, true); 10 | Add(200, false); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement.Test/TestData/StronglyTypedEmployeeServiceTestData_FromFile.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace EmployeeManagement.Test.TestData 4 | { 5 | public class StronglyTypedEmployeeServiceTestData_FromFile : TheoryData 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 | -------------------------------------------------------------------------------- /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.Test/appsettings.json: -------------------------------------------------------------------------------- 1 | //{ 2 | // "ConnectionStrings": { 3 | // "EmployeeManagementDB": "Data Source=EmployeeManagement.db" 4 | // } 5 | //} 6 | -------------------------------------------------------------------------------- /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/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/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/EventArguments/EmployeeIsAbsentEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.Business.EventArguments 2 | { 3 | public class EmployeeIsAbsentEventArgs : EventArgs 4 | { 5 | public Guid EmployeeId { get; private set; } 6 | 7 | public EmployeeIsAbsentEventArgs(Guid employeeId) 8 | { 9 | EmployeeId = employeeId; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/Business/Exceptions/EmployeeInvalidRaiseException.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.Business.Exceptions 2 | { 3 | public class EmployeeInvalidRaiseException : Exception 4 | { 5 | public int InvalidRaise { get; private set; } 6 | public EmployeeInvalidRaiseException(string message, int raise): 7 | base(message) 8 | { 9 | InvalidRaise = raise; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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/Business/IPromotionService.cs: -------------------------------------------------------------------------------- 1 | using EmployeeManagement.DataAccess.Entities; 2 | 3 | namespace EmployeeManagement.Business 4 | { 5 | public interface IPromotionService 6 | { 7 | Task PromoteInternalEmployeeAsync(InternalEmployee employee); 8 | } 9 | } -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/Business/PromotionEligibility.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.Business 2 | { 3 | public class PromotionEligibility 4 | { 5 | public bool EligibleForPromotion { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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/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 | } -------------------------------------------------------------------------------- /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/Controllers/StatisticsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EmployeeManagement.ActionFilters; 3 | using EmployeeManagement.ViewModels; 4 | using Microsoft.AspNetCore.Http.Features; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace EmployeeManagement.Controllers 8 | { 9 | public class StatisticsController : Controller 10 | { 11 | private readonly IMapper _mapper; 12 | 13 | public StatisticsController(IMapper mapper) 14 | { 15 | _mapper = mapper; 16 | } 17 | 18 | [CheckShowStatisticsHeader] 19 | public IActionResult Index() 20 | { 21 | var httpConnectionFeature = HttpContext.Features 22 | .Get(); 23 | return View(_mapper.Map(httpConnectionFeature)); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/DataAccess/Entities/Course.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace EmployeeManagement.DataAccess.Entities 5 | { 6 | public class Course 7 | { 8 | [Key] 9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 10 | public Guid Id { get; set; } 11 | public bool IsNew { get; set; } = true; 12 | public string Title { get; set; } 13 | public List EmployeesThatAttended { get; set; } 14 | = new List(); 15 | 16 | public Course(string title) 17 | { 18 | Title = title; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/DataAccess/Entities/ExternalEmployee.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.DataAccess.Entities 2 | { 3 | public class ExternalEmployee : Employee 4 | { 5 | public string Company { get; set; } 6 | 7 | public ExternalEmployee( 8 | string firstName, 9 | string lastName, 10 | string company) 11 | : base(firstName, lastName) 12 | { 13 | Company = company; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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/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/IEmployeeManagementRepository.cs: -------------------------------------------------------------------------------- 1 | using EmployeeManagement.DataAccess.Entities; 2 | 3 | namespace EmployeeManagement.DataAccess.Services 4 | { 5 | public interface IEmployeeManagementRepository 6 | { 7 | Task> GetInternalEmployeesAsync(); 8 | 9 | InternalEmployee? GetInternalEmployee(Guid employeeId); 10 | 11 | Task GetInternalEmployeeAsync(Guid employeeId); 12 | 13 | Task GetCourseAsync(Guid courseId); 14 | 15 | Course? GetCourse(Guid courseId); 16 | 17 | List GetCourses(params Guid[] courseIds); 18 | 19 | Task> GetCoursesAsync(params Guid[] courseIds); 20 | 21 | void AddInternalEmployee(InternalEmployee internalEmployee); 22 | 23 | Task SaveChangesAsync(); 24 | } 25 | } -------------------------------------------------------------------------------- /Finished solution/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 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/EmployeeManagement.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/UnitTestingAspNetCoreMVC/37d75208496b50ca2cfc0b07ec8ffb21dd8294e4/Finished solution/EmployeeManagement/EmployeeManagement.db -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/MapperProfiles/CourseProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EmployeeManagement.DataAccess.Entities; 3 | 4 | namespace EmployeeManagement.MapperProfiles 5 | { 6 | public class CourseProfile : Profile 7 | { 8 | public CourseProfile() 9 | { 10 | CreateMap(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/MapperProfiles/EmployeeProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EmployeeManagement.DataAccess.Entities; 3 | 4 | namespace EmployeeManagement.MapperProfiles 5 | { 6 | public class EmployeeProfile : Profile 7 | { 8 | public EmployeeProfile() 9 | { 10 | CreateMap(); 11 | CreateMap(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/MapperProfiles/StatisticsProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.AspNetCore.Http.Features; 3 | 4 | namespace EmployeeManagement.MapperProfiles 5 | { 6 | public class StatisticsProfile : Profile 7 | { 8 | public StatisticsProfile() 9 | { 10 | CreateMap(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/Middleware/EmployeeManagementSecurityHeadersMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.Middleware 2 | { 3 | public class EmployeeManagementSecurityHeadersMiddleware 4 | { 5 | private readonly RequestDelegate _next; 6 | 7 | public EmployeeManagementSecurityHeadersMiddleware(RequestDelegate next) 8 | { 9 | _next = next; 10 | } 11 | 12 | 13 | public async Task InvokeAsync(HttpContext context) 14 | { 15 | IHeaderDictionary headers = context.Response.Headers; 16 | 17 | // Add CSP + X-Content-Type 18 | headers["Content-Security-Policy"] = "default-src 'self';frame-ancestors 'none';"; 19 | headers["X-Content-Type-Options"] = "nosniff"; 20 | 21 | await _next(context); 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:28969", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "EmployeeManagement": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "http://localhost:5129", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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/ViewModels/CourseViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.ViewModels 2 | { 3 | public class CourseViewModel 4 | { 5 | public Guid Id { get; set; } 6 | public bool IsNew { get; set; } 7 | public string Title { get; set; } = string.Empty; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/ViewModels/CreateInternalEmployeeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace EmployeeManagement.ViewModels 4 | { 5 | public class CreateInternalEmployeeViewModel 6 | { 7 | [Required] 8 | [MaxLength(100)] 9 | public string FirstName { get; set; } = string.Empty; 10 | 11 | [Required] 12 | [MaxLength(100)] 13 | public string LastName { get; set; } = string.Empty; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/ViewModels/EmployeeOverviewViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.ViewModels 2 | { 3 | public class EmployeeOverviewViewModel 4 | { 5 | public List InternalEmployees { get; set; } 6 | 7 | public EmployeeOverviewViewModel( 8 | IEnumerable internalEmployeeViewModels) 9 | { 10 | InternalEmployees = internalEmployeeViewModels.ToList(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/ViewModels/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.ViewModels 2 | { 3 | public class ErrorViewModel 4 | { 5 | public string? RequestId { get; set; } 6 | 7 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 8 | } 9 | } -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/ViewModels/InternalEmployeeDetailViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.ViewModels 2 | { 3 | public class InternalEmployeeDetailViewModel 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string FirstName { get; set; } = string.Empty; 8 | 9 | public string LastName { get; set; } = string.Empty; 10 | 11 | public string FullName 12 | { 13 | get { return $"{FirstName} {LastName}"; } 14 | } 15 | 16 | public int YearsInService { get; set; } 17 | 18 | public decimal SuggestedBonus { get; set; } 19 | 20 | public decimal Salary { get; set; } 21 | 22 | public bool MinimumRaiseGiven { get; set; } 23 | 24 | public int JobLevel { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/ViewModels/InternalEmployeeForOverviewViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace EmployeeManagement.ViewModels 4 | { 5 | public class InternalEmployeeForOverviewViewModel 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string FirstName { get; set; } = string.Empty; 10 | 11 | public string LastName { get; set; } = string.Empty; 12 | 13 | public string FullName 14 | { 15 | get { return $"{FirstName} {LastName}"; } 16 | } 17 | 18 | public int YearsInService { get; set; } 19 | 20 | public decimal SuggestedBonus { get; set; } 21 | 22 | public decimal Salary { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/ViewModels/StatisticsViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.ViewModels 2 | { 3 | public class StatisticsViewModel 4 | { 5 | public string LocalIpAddress { get; set; } = string.Empty; 6 | public int LocalPort { get; set; } 7 | public string RemoteIpAddress { get; set; } = string.Empty; 8 | public int RemotePort { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 | 18 | 21 | 24 | 27 | 28 | 29 | 30 | @foreach (var item in Model.InternalEmployees) 31 | { 32 | 33 | 36 | 39 | 42 | 45 | 49 | 50 | } 51 | 52 |
16 | @Html.DisplayNameFor(model => model.InternalEmployees[0].FullName) 17 | 19 | @Html.DisplayNameFor(model => model.InternalEmployees[0].Salary) 20 | 22 | @Html.DisplayNameFor(model => model.InternalEmployees[0].YearsInService) 23 | 25 | @Html.DisplayNameFor(model => model.InternalEmployees[0].SuggestedBonus) 26 |
34 | @Html.DisplayFor(modelItem => item.FullName) 35 | 37 | @Html.DisplayFor(modelItem => item.Salary) 38 | 40 | @Html.DisplayFor(modelItem => item.YearsInService) 41 | 43 | @Html.DisplayFor(modelItem => item.SuggestedBonus) 44 | 46 | Details 48 |
53 | 54 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/Views/InternalEmployee/AddInternalEmployee.cshtml: -------------------------------------------------------------------------------- 1 | @model EmployeeManagement.ViewModels.CreateInternalEmployeeViewModel 2 | @{ 3 | ViewData["Title"] = "Add Internal Employee"; 4 | } 5 | 6 |

Add Internal Employee

7 | 8 |
9 |
10 | First name: 11 | 12 |
13 |
14 | Last name: 15 | 16 |
17 | 18 |
-------------------------------------------------------------------------------- /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 |
28 | 29 | 32 |
33 |
34 |
35 |
@ViewBag.PromotionRequestMessage
36 |
-------------------------------------------------------------------------------- /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/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - Employee Management 7 | 8 | 9 | 10 | 11 | 12 |
13 | 29 |
30 |
31 |
32 | @RenderBody() 33 |
34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | @await RenderSectionAsync("Scripts", required: false) 42 | 43 | 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/Views/Statistics/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model EmployeeManagement.ViewModels.StatisticsViewModel 2 | @{ 3 | ViewData["Title"] = "Statistics"; 4 | } 5 | 6 | 7 |

Statistics

8 | 9 |
Local address: @Model?.LocalIpAddress, port @Model?.LocalPort
10 |
Remote address: @Model?.RemoteIpAddress, port @Model?.RemotePort
11 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using EmployeeManagement 2 | @using EmployeeManagement.ViewModels 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "EmployeeManagementDB": "Data Source=EmployeeManagement.db" 11 | //"EmployeeManagementDB": "Data Source=:memory:" 12 | }, 13 | "TopLevelManagementAPIRoot": "http://localhost:5057" 14 | } 15 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 14px; 3 | } 4 | 5 | @media (min-width: 768px) { 6 | html { 7 | font-size: 16px; 8 | } 9 | } 10 | 11 | html { 12 | position: relative; 13 | min-height: 100%; 14 | } 15 | 16 | body { 17 | margin-bottom: 60px; 18 | } -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/UnitTestingAspNetCoreMVC/37d75208496b50ca2cfc0b07ec8ffb21dd8294e4/Finished solution/EmployeeManagement/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/wwwroot/js/site.js: -------------------------------------------------------------------------------- 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 | // Write your JavaScript code. 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 */ -------------------------------------------------------------------------------- /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/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /Finished solution/EmployeeManagement/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | // Unobtrusive validation support library for jQuery and jQuery Validate 2 | // Copyright (c) .NET Foundation. All rights reserved. 3 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 4 | // @version v3.2.11 5 | !function(a){"function"==typeof define&&define.amd?define("jquery.validate.unobtrusive",["jquery-validation"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery-validation")):jQuery.validator.unobtrusive=a(jQuery)}(function(a){function e(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function n(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function t(a){return a.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function r(a){return a.substr(0,a.lastIndexOf(".")+1)}function i(a,e){return 0===a.indexOf("*.")&&(a=a.replace("*.",e)),a}function o(e,n){var r=a(this).find("[data-valmsg-for='"+t(n[0].name)+"']"),i=r.attr("data-valmsg-replace"),o=i?a.parseJSON(i)!==!1:null;r.removeClass("field-validation-valid").addClass("field-validation-error"),e.data("unobtrusiveContainer",r),o?(r.empty(),e.removeClass("input-validation-error").appendTo(r)):e.hide()}function d(e,n){var t=a(this).find("[data-valmsg-summary=true]"),r=t.find("ul");r&&r.length&&n.errorList.length&&(r.empty(),t.addClass("validation-summary-errors").removeClass("validation-summary-valid"),a.each(n.errorList,function(){a("
  • ").html(this.message).appendTo(r)}))}function s(e){var n=e.data("unobtrusiveContainer");if(n){var t=n.attr("data-valmsg-replace"),r=t?a.parseJSON(t):null;n.addClass("field-validation-valid").removeClass("field-validation-error"),e.removeData("unobtrusiveContainer"),r&&n.empty()}}function l(e){var n=a(this),t="__jquery_unobtrusive_validation_form_reset";if(!n.data(t)){n.data(t,!0);try{n.data("validator").resetForm()}finally{n.removeData(t)}n.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),n.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function u(e){var n=a(e),t=n.data(v),r=a.proxy(l,e),i=f.unobtrusive.options||{},u=function(n,t){var r=i[n];r&&a.isFunction(r)&&r.apply(e,t)};return t||(t={options:{errorClass:i.errorClass||"input-validation-error",errorElement:i.errorElement||"span",errorPlacement:function(){o.apply(e,arguments),u("errorPlacement",arguments)},invalidHandler:function(){d.apply(e,arguments),u("invalidHandler",arguments)},messages:{},rules:{},success:function(){s.apply(e,arguments),u("success",arguments)}},attachValidation:function(){n.off("reset."+v,r).on("reset."+v,r).validate(this.options)},validate:function(){return n.validate(),n.valid()}},n.data(v,t)),t}var m,f=a.validator,v="unobtrusiveValidation";return f.unobtrusive={adapters:[],parseElement:function(e,n){var t,r,i,o=a(e),d=o.parents("form")[0];d&&(t=u(d),t.options.rules[e.name]=r={},t.options.messages[e.name]=i={},a.each(this.adapters,function(){var n="data-val-"+this.name,t=o.attr(n),s={};void 0!==t&&(n+="-",a.each(this.params,function(){s[this]=o.attr(n+this)}),this.adapt({element:e,form:d,message:t,params:s,rules:r,messages:i}))}),a.extend(r,{__dummy__:!0}),n||t.attachValidation())},parse:function(e){var n=a(e),t=n.parents().addBack().filter("form").add(n.find("form")).has("[data-val=true]");n.find("[data-val=true]").each(function(){f.unobtrusive.parseElement(this,!0)}),t.each(function(){var a=u(this);a&&a.attachValidation()})}},m=f.unobtrusive.adapters,m.add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},m.addBool=function(a,n){return this.add(a,function(t){e(t,n||a,!0)})},m.addMinMax=function(a,n,t,r,i,o){return this.add(a,[i||"min",o||"max"],function(a){var i=a.params.min,o=a.params.max;i&&o?e(a,r,[i,o]):i?e(a,n,i):o&&e(a,t,o)})},m.addSingleVal=function(a,n,t){return this.add(a,[n||"val"],function(r){e(r,t||a,r.params[n])})},f.addMethod("__dummy__",function(a,e,n){return!0}),f.addMethod("regex",function(a,e,n){var t;return!!this.optional(e)||(t=new RegExp(n).exec(a),t&&0===t.index&&t[0].length===a.length)}),f.addMethod("nonalphamin",function(a,e,n){var t;return n&&(t=a.match(/\W/g),t=t&&t.length>=n),t}),f.methods.extension?(m.addSingleVal("accept","mimtype"),m.addSingleVal("extension","extension")):m.addSingleVal("extension","extension","accept"),m.addSingleVal("regex","pattern"),m.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),m.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),m.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),m.add("equalto",["other"],function(n){var o=r(n.element.name),d=n.params.other,s=i(d,o),l=a(n.form).find(":input").filter("[name='"+t(s)+"']")[0];e(n,"equalTo",l)}),m.add("required",function(a){"INPUT"===a.element.tagName.toUpperCase()&&"CHECKBOX"===a.element.type.toUpperCase()||e(a,"required",!0)}),m.add("remote",["url","type","additionalfields"],function(o){var d={url:o.params.url,type:o.params.type||"GET",data:{}},s=r(o.element.name);a.each(n(o.params.additionalfields||o.element.name),function(e,n){var r=i(n,s);d.data[r]=function(){var e=a(o.form).find(":input").filter("[name='"+t(r)+"']");return e.is(":checkbox")?e.filter(":checked").val()||e.filter(":hidden").val()||"":e.is(":radio")?e.filter(":checked").val()||"":e.val()}}),e(o,"remote",d)}),m.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&e(a,"minlength",a.params.min),a.params.nonalphamin&&e(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&e(a,"regex",a.params.regex)}),m.add("fileextensions",["extensions"],function(a){e(a,"extension",a.params.extensions)}),a(function(){f.unobtrusive.parse(document)}),f.unobtrusive}); 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Finished solution/TopLevelManagement/Controllers/PromotionEligibilitiesController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace TopLevelManagement.Controllers 4 | { 5 | [ApiController] 6 | [Route("api/promotioneligibilities")] 7 | public class PromotionEligibilitiesController : ControllerBase 8 | { 9 | [HttpGet("{employeeId}")] 10 | public IActionResult EmployeeIsEligibleForPromotion(Guid employeeId) 11 | { 12 | // For demo purposes, Megan (id = 72f2f5fe-e50c-4966-8420-d50258aefdcb) 13 | // is eligible for promotion, other employees aren't 14 | if (employeeId == Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb")) 15 | { 16 | return Ok(new { EligibleForPromotion = true }); 17 | } 18 | 19 | return Ok(new { EligibleForPromotion = false }); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Finished solution/TopLevelManagement/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | // Add services to the container. 4 | 5 | builder.Services.AddControllers(); 6 | 7 | var app = builder.Build(); 8 | 9 | // Configure the HTTP request pipeline. 10 | 11 | app.UseAuthorization(); 12 | 13 | app.MapControllers(); 14 | 15 | app.Run(); 16 | -------------------------------------------------------------------------------- /Finished solution/TopLevelManagement/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:13433", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "TopLevelManagement.API": { 13 | "commandName": "Project", 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | }, 17 | "applicationUrl": "http://localhost:5057", 18 | "dotnetRunMessages": true 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "launchUrl": "weatherforecast", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Finished solution/TopLevelManagement/TopLevelManagement.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Finished solution/TopLevelManagement/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Finished solution/TopLevelManagement/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Unit Testing an ASP.NET Core MVC Web Application 2 | Fully functioning sample code for my Unit Testing an ASP.NET Core MVC Web Application course, over at Pluralsight, currently targeting .NET 8. 3 | 4 | The **main** branch exactly matches the course. 5 | The **latest-and-greatest** branch contains changes that were incorporated after recording. Most often these changes are language features that are relativley new and/or in preview, like primary constructors, switch expressions and so on. Most of these changes will probably make it into the main branch when course updates happen, but if you don't want to wait for that you can already check it out - enjoy :-) 6 | 7 | All my courses can be found at https://app.pluralsight.com/profile/author/kevin-dockx 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Business/EventArguments/EmployeeIsAbsentEventArgs.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.Business.EventArguments 2 | { 3 | public class EmployeeIsAbsentEventArgs : EventArgs 4 | { 5 | public Guid EmployeeId { get; private set; } 6 | 7 | public EmployeeIsAbsentEventArgs(Guid employeeId) 8 | { 9 | EmployeeId = employeeId; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Business/Exceptions/EmployeeInvalidRaiseException.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.Business.Exceptions 2 | { 3 | public class EmployeeInvalidRaiseException : Exception 4 | { 5 | public int InvalidRaise { get; private set; } 6 | public EmployeeInvalidRaiseException(string message, int raise): 7 | base(message) 8 | { 9 | InvalidRaise = raise; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /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/Business/IPromotionService.cs: -------------------------------------------------------------------------------- 1 | using EmployeeManagement.DataAccess.Entities; 2 | 3 | namespace EmployeeManagement.Business 4 | { 5 | public interface IPromotionService 6 | { 7 | Task PromoteInternalEmployeeAsync(InternalEmployee employee); 8 | } 9 | } -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Business/PromotionEligibility.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.Business 2 | { 3 | public class PromotionEligibility 4 | { 5 | public bool EligibleForPromotion { get; set; } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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 | } -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Controllers/StatisticsController.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EmployeeManagement.ActionFilters; 3 | using EmployeeManagement.ViewModels; 4 | using Microsoft.AspNetCore.Http.Features; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace EmployeeManagement.Controllers 8 | { 9 | public class StatisticsController : Controller 10 | { 11 | private readonly IMapper _mapper; 12 | 13 | public StatisticsController(IMapper mapper) 14 | { 15 | _mapper = mapper; 16 | } 17 | 18 | public IActionResult Index() 19 | { 20 | var httpConnectionFeature = HttpContext.Features.Get(); 21 | return View(_mapper.Map(httpConnectionFeature)); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/DataAccess/Entities/Course.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.ComponentModel.DataAnnotations.Schema; 3 | 4 | namespace EmployeeManagement.DataAccess.Entities 5 | { 6 | public class Course 7 | { 8 | [Key] 9 | [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 10 | public Guid Id { get; set; } 11 | public bool IsNew { get; set; } = true; 12 | public string Title { get; set; } 13 | public List EmployeesThatAttended { get; set; } 14 | = new List(); 15 | 16 | public Course(string title) 17 | { 18 | Title = title; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/DataAccess/Entities/ExternalEmployee.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.DataAccess.Entities 2 | { 3 | public class ExternalEmployee : Employee 4 | { 5 | public string Company { get; set; } 6 | 7 | public ExternalEmployee( 8 | string firstName, 9 | string lastName, 10 | string company) 11 | : base(firstName, lastName) 12 | { 13 | Company = company; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/DataAccess/Services/IEmployeeManagementRepository.cs: -------------------------------------------------------------------------------- 1 | using EmployeeManagement.DataAccess.Entities; 2 | 3 | namespace EmployeeManagement.DataAccess.Services 4 | { 5 | public interface IEmployeeManagementRepository 6 | { 7 | Task> GetInternalEmployeesAsync(); 8 | 9 | InternalEmployee? GetInternalEmployee(Guid employeeId); 10 | 11 | Task GetInternalEmployeeAsync(Guid employeeId); 12 | 13 | Task GetCourseAsync(Guid courseId); 14 | 15 | Course? GetCourse(Guid courseId); 16 | 17 | List GetCourses(params Guid[] courseIds); 18 | 19 | Task> GetCoursesAsync(params Guid[] courseIds); 20 | 21 | void AddInternalEmployee(InternalEmployee internalEmployee); 22 | 23 | Task SaveChangesAsync(); 24 | } 25 | } -------------------------------------------------------------------------------- /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/EmployeeManagement.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/UnitTestingAspNetCoreMVC/37d75208496b50ca2cfc0b07ec8ffb21dd8294e4/Starter files/EmployeeManagement/EmployeeManagement.db -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/MapperProfiles/CourseProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EmployeeManagement.DataAccess.Entities; 3 | 4 | namespace EmployeeManagement.MapperProfiles 5 | { 6 | public class CourseProfile : Profile 7 | { 8 | public CourseProfile() 9 | { 10 | CreateMap(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/MapperProfiles/EmployeeProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using EmployeeManagement.DataAccess.Entities; 3 | 4 | namespace EmployeeManagement.MapperProfiles 5 | { 6 | public class EmployeeProfile : Profile 7 | { 8 | public EmployeeProfile() 9 | { 10 | CreateMap(); 11 | CreateMap(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/MapperProfiles/StatisticsProfile.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using Microsoft.AspNetCore.Http.Features; 3 | 4 | namespace EmployeeManagement.MapperProfiles 5 | { 6 | public class StatisticsProfile : Profile 7 | { 8 | public StatisticsProfile() 9 | { 10 | CreateMap(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Middleware/EmployeeManagementSecurityHeadersMiddleware.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.Middleware 2 | { 3 | public class EmployeeManagementSecurityHeadersMiddleware 4 | { 5 | private readonly RequestDelegate _next; 6 | 7 | public EmployeeManagementSecurityHeadersMiddleware(RequestDelegate next) 8 | { 9 | _next = next; 10 | } 11 | 12 | 13 | public async Task InvokeAsync(HttpContext context) 14 | { 15 | IHeaderDictionary headers = context.Response.Headers; 16 | 17 | // Add CSP + X-Content-Type 18 | headers["Content-Security-Policy"] = "default-src 'self';frame-ancestors 'none';"; 19 | headers["X-Content-Type-Options"] = "nosniff"; 20 | 21 | await _next(context); 22 | } 23 | } 24 | } 25 | 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:28969", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "EmployeeManagement": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "http://localhost:5129", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/ViewModels/CourseViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.ViewModels 2 | { 3 | public class CourseViewModel 4 | { 5 | public Guid Id { get; set; } 6 | public bool IsNew { get; set; } 7 | public string Title { get; set; } = string.Empty; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/ViewModels/CreateInternalEmployeeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace EmployeeManagement.ViewModels 4 | { 5 | public class CreateInternalEmployeeViewModel 6 | { 7 | [Required] 8 | [MaxLength(100)] 9 | public string FirstName { get; set; } = string.Empty; 10 | 11 | [Required] 12 | [MaxLength(100)] 13 | public string LastName { get; set; } = string.Empty; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/ViewModels/EmployeeOverviewViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.ViewModels 2 | { 3 | public class EmployeeOverviewViewModel 4 | { 5 | public List InternalEmployees { get; set; } 6 | 7 | public EmployeeOverviewViewModel( 8 | IEnumerable internalEmployeeViewModels) 9 | { 10 | InternalEmployees = internalEmployeeViewModels.ToList(); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/ViewModels/ErrorViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.ViewModels 2 | { 3 | public class ErrorViewModel 4 | { 5 | public string? RequestId { get; set; } 6 | 7 | public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); 8 | } 9 | } -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/ViewModels/InternalEmployeeDetailViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.ViewModels 2 | { 3 | public class InternalEmployeeDetailViewModel 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string FirstName { get; set; } = string.Empty; 8 | 9 | public string LastName { get; set; } = string.Empty; 10 | 11 | public string FullName 12 | { 13 | get { return $"{FirstName} {LastName}"; } 14 | } 15 | 16 | public int YearsInService { get; set; } 17 | 18 | public decimal SuggestedBonus { get; set; } 19 | 20 | public decimal Salary { get; set; } 21 | 22 | public bool MinimumRaiseGiven { get; set; } 23 | 24 | public int JobLevel { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/ViewModels/InternalEmployeeForOverviewViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace EmployeeManagement.ViewModels 4 | { 5 | public class InternalEmployeeForOverviewViewModel 6 | { 7 | public Guid Id { get; set; } 8 | 9 | public string FirstName { get; set; } = string.Empty; 10 | 11 | public string LastName { get; set; } = string.Empty; 12 | 13 | public string FullName 14 | { 15 | get { return $"{FirstName} {LastName}"; } 16 | } 17 | 18 | public int YearsInService { get; set; } 19 | 20 | public decimal SuggestedBonus { get; set; } 21 | 22 | public decimal Salary { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/ViewModels/StatisticsViewModel.cs: -------------------------------------------------------------------------------- 1 | namespace EmployeeManagement.ViewModels 2 | { 3 | public class StatisticsViewModel 4 | { 5 | public string LocalIpAddress { get; set; } = string.Empty; 6 | public int LocalPort { get; set; } 7 | public string RemoteIpAddress { get; set; } = string.Empty; 8 | public int RemotePort { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /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 | 18 | 21 | 24 | 27 | 28 | 29 | 30 | @foreach (var item in Model.InternalEmployees) 31 | { 32 | 33 | 36 | 39 | 42 | 45 | 49 | 50 | } 51 | 52 |
    16 | @Html.DisplayNameFor(model => model.InternalEmployees[0].FullName) 17 | 19 | @Html.DisplayNameFor(model => model.InternalEmployees[0].Salary) 20 | 22 | @Html.DisplayNameFor(model => model.InternalEmployees[0].YearsInService) 23 | 25 | @Html.DisplayNameFor(model => model.InternalEmployees[0].SuggestedBonus) 26 |
    34 | @Html.DisplayFor(modelItem => item.FullName) 35 | 37 | @Html.DisplayFor(modelItem => item.Salary) 38 | 40 | @Html.DisplayFor(modelItem => item.YearsInService) 41 | 43 | @Html.DisplayFor(modelItem => item.SuggestedBonus) 44 | 46 | Details 48 |
    53 | 54 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Views/InternalEmployee/AddInternalEmployee.cshtml: -------------------------------------------------------------------------------- 1 | @model EmployeeManagement.ViewModels.CreateInternalEmployeeViewModel 2 | @{ 3 | ViewData["Title"] = "Add Internal Employee"; 4 | } 5 | 6 |

    Add Internal Employee

    7 | 8 |
    9 |
    10 | First name: 11 | 12 |
    13 |
    14 | Last name: 15 | 16 |
    17 | 18 |
    -------------------------------------------------------------------------------- /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 |
    28 | 29 | 32 |
    33 |
    34 |
    35 |
    @ViewBag.PromotionRequestMessage
    36 |
    -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Views/Shared/_Layout.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | @ViewData["Title"] - Employee Management 7 | 8 | 9 | 10 | 11 | 12 |
    13 | 29 |
    30 |
    31 |
    32 | @RenderBody() 33 |
    34 |
    35 | 36 |
    37 |
    38 | 39 | 40 | 41 | @await RenderSectionAsync("Scripts", required: false) 42 | 43 | 44 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Views/Shared/_ValidationScriptsPartial.cshtml: -------------------------------------------------------------------------------- 1 |  2 | 3 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Views/Statistics/Index.cshtml: -------------------------------------------------------------------------------- 1 | @model EmployeeManagement.ViewModels.StatisticsViewModel 2 | @{ 3 | ViewData["Title"] = "Statistics"; 4 | } 5 | 6 | 7 |

    Statistics

    8 | 9 |
    Local address: @Model?.LocalIpAddress, port @Model?.LocalPort
    10 |
    Remote address: @Model?.RemoteIpAddress, port @Model?.RemotePort
    11 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Views/_ViewImports.cshtml: -------------------------------------------------------------------------------- 1 | @using EmployeeManagement 2 | @using EmployeeManagement.ViewModels 3 | @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers 4 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/Views/_ViewStart.cshtml: -------------------------------------------------------------------------------- 1 | @{ 2 | Layout = "_Layout"; 3 | } 4 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | "ConnectionStrings": { 10 | "EmployeeManagementDB": "Data Source=EmployeeManagement.db" 11 | //"EmployeeManagementDB": "Data Source=:memory:" 12 | }, 13 | "TopLevelManagementAPIRoot": "http://localhost:5057" 14 | } 15 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/wwwroot/css/site.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-size: 14px; 3 | } 4 | 5 | @media (min-width: 768px) { 6 | html { 7 | font-size: 16px; 8 | } 9 | } 10 | 11 | html { 12 | position: relative; 13 | min-height: 100%; 14 | } 15 | 16 | body { 17 | margin-bottom: 60px; 18 | } -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/wwwroot/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KevinDockx/UnitTestingAspNetCoreMVC/37d75208496b50ca2cfc0b07ec8ffb21dd8294e4/Starter files/EmployeeManagement/wwwroot/favicon.ico -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/wwwroot/js/site.js: -------------------------------------------------------------------------------- 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 | // Write your JavaScript code. 5 | -------------------------------------------------------------------------------- /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/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 */ -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation. All rights reserved. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 4 | these files except in compliance with the License. You may obtain a copy of the 5 | License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software distributed 10 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 11 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 12 | specific language governing permissions and limitations under the License. 13 | -------------------------------------------------------------------------------- /Starter files/EmployeeManagement/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js: -------------------------------------------------------------------------------- 1 | // Unobtrusive validation support library for jQuery and jQuery Validate 2 | // Copyright (c) .NET Foundation. All rights reserved. 3 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 4 | // @version v3.2.11 5 | !function(a){"function"==typeof define&&define.amd?define("jquery.validate.unobtrusive",["jquery-validation"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery-validation")):jQuery.validator.unobtrusive=a(jQuery)}(function(a){function e(a,e,n){a.rules[e]=n,a.message&&(a.messages[e]=a.message)}function n(a){return a.replace(/^\s+|\s+$/g,"").split(/\s*,\s*/g)}function t(a){return a.replace(/([!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~])/g,"\\$1")}function r(a){return a.substr(0,a.lastIndexOf(".")+1)}function i(a,e){return 0===a.indexOf("*.")&&(a=a.replace("*.",e)),a}function o(e,n){var r=a(this).find("[data-valmsg-for='"+t(n[0].name)+"']"),i=r.attr("data-valmsg-replace"),o=i?a.parseJSON(i)!==!1:null;r.removeClass("field-validation-valid").addClass("field-validation-error"),e.data("unobtrusiveContainer",r),o?(r.empty(),e.removeClass("input-validation-error").appendTo(r)):e.hide()}function d(e,n){var t=a(this).find("[data-valmsg-summary=true]"),r=t.find("ul");r&&r.length&&n.errorList.length&&(r.empty(),t.addClass("validation-summary-errors").removeClass("validation-summary-valid"),a.each(n.errorList,function(){a("
  • ").html(this.message).appendTo(r)}))}function s(e){var n=e.data("unobtrusiveContainer");if(n){var t=n.attr("data-valmsg-replace"),r=t?a.parseJSON(t):null;n.addClass("field-validation-valid").removeClass("field-validation-error"),e.removeData("unobtrusiveContainer"),r&&n.empty()}}function l(e){var n=a(this),t="__jquery_unobtrusive_validation_form_reset";if(!n.data(t)){n.data(t,!0);try{n.data("validator").resetForm()}finally{n.removeData(t)}n.find(".validation-summary-errors").addClass("validation-summary-valid").removeClass("validation-summary-errors"),n.find(".field-validation-error").addClass("field-validation-valid").removeClass("field-validation-error").removeData("unobtrusiveContainer").find(">*").removeData("unobtrusiveContainer")}}function u(e){var n=a(e),t=n.data(v),r=a.proxy(l,e),i=f.unobtrusive.options||{},u=function(n,t){var r=i[n];r&&a.isFunction(r)&&r.apply(e,t)};return t||(t={options:{errorClass:i.errorClass||"input-validation-error",errorElement:i.errorElement||"span",errorPlacement:function(){o.apply(e,arguments),u("errorPlacement",arguments)},invalidHandler:function(){d.apply(e,arguments),u("invalidHandler",arguments)},messages:{},rules:{},success:function(){s.apply(e,arguments),u("success",arguments)}},attachValidation:function(){n.off("reset."+v,r).on("reset."+v,r).validate(this.options)},validate:function(){return n.validate(),n.valid()}},n.data(v,t)),t}var m,f=a.validator,v="unobtrusiveValidation";return f.unobtrusive={adapters:[],parseElement:function(e,n){var t,r,i,o=a(e),d=o.parents("form")[0];d&&(t=u(d),t.options.rules[e.name]=r={},t.options.messages[e.name]=i={},a.each(this.adapters,function(){var n="data-val-"+this.name,t=o.attr(n),s={};void 0!==t&&(n+="-",a.each(this.params,function(){s[this]=o.attr(n+this)}),this.adapt({element:e,form:d,message:t,params:s,rules:r,messages:i}))}),a.extend(r,{__dummy__:!0}),n||t.attachValidation())},parse:function(e){var n=a(e),t=n.parents().addBack().filter("form").add(n.find("form")).has("[data-val=true]");n.find("[data-val=true]").each(function(){f.unobtrusive.parseElement(this,!0)}),t.each(function(){var a=u(this);a&&a.attachValidation()})}},m=f.unobtrusive.adapters,m.add=function(a,e,n){return n||(n=e,e=[]),this.push({name:a,params:e,adapt:n}),this},m.addBool=function(a,n){return this.add(a,function(t){e(t,n||a,!0)})},m.addMinMax=function(a,n,t,r,i,o){return this.add(a,[i||"min",o||"max"],function(a){var i=a.params.min,o=a.params.max;i&&o?e(a,r,[i,o]):i?e(a,n,i):o&&e(a,t,o)})},m.addSingleVal=function(a,n,t){return this.add(a,[n||"val"],function(r){e(r,t||a,r.params[n])})},f.addMethod("__dummy__",function(a,e,n){return!0}),f.addMethod("regex",function(a,e,n){var t;return!!this.optional(e)||(t=new RegExp(n).exec(a),t&&0===t.index&&t[0].length===a.length)}),f.addMethod("nonalphamin",function(a,e,n){var t;return n&&(t=a.match(/\W/g),t=t&&t.length>=n),t}),f.methods.extension?(m.addSingleVal("accept","mimtype"),m.addSingleVal("extension","extension")):m.addSingleVal("extension","extension","accept"),m.addSingleVal("regex","pattern"),m.addBool("creditcard").addBool("date").addBool("digits").addBool("email").addBool("number").addBool("url"),m.addMinMax("length","minlength","maxlength","rangelength").addMinMax("range","min","max","range"),m.addMinMax("minlength","minlength").addMinMax("maxlength","minlength","maxlength"),m.add("equalto",["other"],function(n){var o=r(n.element.name),d=n.params.other,s=i(d,o),l=a(n.form).find(":input").filter("[name='"+t(s)+"']")[0];e(n,"equalTo",l)}),m.add("required",function(a){"INPUT"===a.element.tagName.toUpperCase()&&"CHECKBOX"===a.element.type.toUpperCase()||e(a,"required",!0)}),m.add("remote",["url","type","additionalfields"],function(o){var d={url:o.params.url,type:o.params.type||"GET",data:{}},s=r(o.element.name);a.each(n(o.params.additionalfields||o.element.name),function(e,n){var r=i(n,s);d.data[r]=function(){var e=a(o.form).find(":input").filter("[name='"+t(r)+"']");return e.is(":checkbox")?e.filter(":checked").val()||e.filter(":hidden").val()||"":e.is(":radio")?e.filter(":checked").val()||"":e.val()}}),e(o,"remote",d)}),m.add("password",["min","nonalphamin","regex"],function(a){a.params.min&&e(a,"minlength",a.params.min),a.params.nonalphamin&&e(a,"nonalphamin",a.params.nonalphamin),a.params.regex&&e(a,"regex",a.params.regex)}),m.add("fileextensions",["extensions"],function(a){e(a,"extension",a.params.extensions)}),a(function(){f.unobtrusive.parse(document)}),f.unobtrusive}); 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/TopLevelManagement/Controllers/PromotionEligibilitiesController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace TopLevelManagement.Controllers 4 | { 5 | [ApiController] 6 | [Route("api/promotioneligibilities")] 7 | public class PromotionEligibilitiesController : ControllerBase 8 | { 9 | [HttpGet("{employeeId}")] 10 | public IActionResult EmployeeIsEligibleForPromotion(Guid employeeId) 11 | { 12 | // For demo purposes, Megan (id = 72f2f5fe-e50c-4966-8420-d50258aefdcb) 13 | // is eligible for promotion, other employees aren't 14 | if (employeeId == Guid.Parse("72f2f5fe-e50c-4966-8420-d50258aefdcb")) 15 | { 16 | return Ok(new { EligibleForPromotion = true }); 17 | } 18 | 19 | return Ok(new { EligibleForPromotion = false }); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /Starter files/TopLevelManagement/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | // Add services to the container. 4 | 5 | builder.Services.AddControllers(); 6 | 7 | var app = builder.Build(); 8 | 9 | // Configure the HTTP request pipeline. 10 | 11 | app.UseAuthorization(); 12 | 13 | app.MapControllers(); 14 | 15 | app.Run(); 16 | -------------------------------------------------------------------------------- /Starter files/TopLevelManagement/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:13433", 8 | "sslPort": 0 9 | } 10 | }, 11 | "profiles": { 12 | "TopLevelManagement.API": { 13 | "commandName": "Project", 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | }, 17 | "applicationUrl": "http://localhost:5057", 18 | "dotnetRunMessages": true 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "launchUrl": "weatherforecast", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /Starter files/TopLevelManagement/TopLevelManagement.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Starter files/TopLevelManagement/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Starter files/TopLevelManagement/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | --------------------------------------------------------------------------------