diff --git a/eSchool.sln b/eSchool.sln index 02ebdc1a..0b22ece3 100644 --- a/eSchool.sln +++ b/eSchool.sln @@ -27,19 +27,31 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebStatus", "src\Web\WebSta EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Libraries", "Libraries", "{74511F4E-FF9D-42C4-9531-A75C61270B73}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry", "src\Libraries\OpenTelemetry\OpenTelemetry.csproj", "{7B410F3B-36E0-4853-9B4E-41D0CC2865B5}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry", "src\Libraries\OpenTelemetry\OpenTelemetry.csproj", "{7B410F3B-36E0-4853-9B4E-41D0CC2865B5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ApiGateways", "ApiGateways", "{256317ED-A2C8-48A0-9C6E-D6EB1F7D0BE0}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "eSchool.GraphQL", "src\ApiGateways\eSchool.GraphQL\eSchool.GraphQL.csproj", "{4053591A-1C1A-4A81-8496-F2FF7EAB2D5C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "eSchool.GraphQL", "src\ApiGateways\eSchool.GraphQL\eSchool.GraphQL.csproj", "{4053591A-1C1A-4A81-8496-F2FF7EAB2D5C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Frontend.Blazor", "Frontend.Blazor", "{0C00A596-0FE3-4FA6-B54B-FE2BE83371EF}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frontend.Blazor.Client", "src\Web\Frontend.Blazor\Frontend.Blazor.Client\Frontend.Blazor.Client.csproj", "{53F4E6F9-6B91-45F9-97F9-F6EFA0EBEFCE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frontend.Blazor.Client", "src\Web\Frontend.Blazor\Frontend.Blazor.Client\Frontend.Blazor.Client.csproj", "{53F4E6F9-6B91-45F9-97F9-F6EFA0EBEFCE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frontend.Blazor.Server", "src\Web\Frontend.Blazor\Frontend.Blazor.Server\Frontend.Blazor.Server.csproj", "{3BABD4D9-56A1-4BA3-B30C-30E6765AB389}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frontend.Blazor.Server", "src\Web\Frontend.Blazor\Frontend.Blazor.Server\Frontend.Blazor.Server.csproj", "{3BABD4D9-56A1-4BA3-B30C-30E6765AB389}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Frontend.Blazor.Shared", "src\Web\Frontend.Blazor\Frontend.Blazor.Shared\Frontend.Blazor.Shared.csproj", "{4EB86635-CF79-4D15-909E-C41C98B0B586}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Frontend.Blazor.Shared", "src\Web\Frontend.Blazor\Frontend.Blazor.Shared\Frontend.Blazor.Shared.csproj", "{4EB86635-CF79-4D15-909E-C41C98B0B586}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Attending", "Attending", "{2BE71A01-0660-4264-B03F-8A386AB840ED}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Attending.API", "src\Services\Attendance\Attendance.API\Attending.API.csproj", "{2A2976A2-4578-49D1-BC3B-A9D466837CCB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Attending.Domain", "src\Services\Attendance\Attendance.Domain\Attending.Domain.csproj", "{69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Attending.FunctionalTests", "src\Services\Attendance\Attendance.FunctionalTests\Attending.FunctionalTests.csproj", "{020D0652-9802-431D-836A-B9ED27D841A2}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Attending.Infrastructure", "src\Services\Attendance\Attendance.Infrastructure\Attending.Infrastructure.csproj", "{767C29FF-C92E-424D-84D4-FF606FACD07A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Attending.UnitTests", "src\Services\Attendance\Attendance.UnitTests\Attending.UnitTests.csproj", "{0D9BD20E-7EBC-4689-B621-676440441223}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -195,6 +207,66 @@ Global {4EB86635-CF79-4D15-909E-C41C98B0B586}.Release|x64.Build.0 = Release|Any CPU {4EB86635-CF79-4D15-909E-C41C98B0B586}.Release|x86.ActiveCfg = Release|Any CPU {4EB86635-CF79-4D15-909E-C41C98B0B586}.Release|x86.Build.0 = Release|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Debug|x64.ActiveCfg = Debug|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Debug|x64.Build.0 = Debug|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Debug|x86.ActiveCfg = Debug|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Debug|x86.Build.0 = Debug|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Release|Any CPU.Build.0 = Release|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Release|x64.ActiveCfg = Release|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Release|x64.Build.0 = Release|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Release|x86.ActiveCfg = Release|Any CPU + {2A2976A2-4578-49D1-BC3B-A9D466837CCB}.Release|x86.Build.0 = Release|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Debug|x64.ActiveCfg = Debug|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Debug|x64.Build.0 = Debug|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Debug|x86.ActiveCfg = Debug|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Debug|x86.Build.0 = Debug|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Release|Any CPU.Build.0 = Release|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Release|x64.ActiveCfg = Release|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Release|x64.Build.0 = Release|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Release|x86.ActiveCfg = Release|Any CPU + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF}.Release|x86.Build.0 = Release|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Debug|x64.ActiveCfg = Debug|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Debug|x64.Build.0 = Debug|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Debug|x86.Build.0 = Debug|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Release|Any CPU.Build.0 = Release|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Release|x64.ActiveCfg = Release|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Release|x64.Build.0 = Release|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Release|x86.ActiveCfg = Release|Any CPU + {020D0652-9802-431D-836A-B9ED27D841A2}.Release|x86.Build.0 = Release|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Debug|x64.ActiveCfg = Debug|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Debug|x64.Build.0 = Debug|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Debug|x86.ActiveCfg = Debug|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Debug|x86.Build.0 = Debug|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Release|Any CPU.Build.0 = Release|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Release|x64.ActiveCfg = Release|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Release|x64.Build.0 = Release|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Release|x86.ActiveCfg = Release|Any CPU + {767C29FF-C92E-424D-84D4-FF606FACD07A}.Release|x86.Build.0 = Release|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Debug|x64.ActiveCfg = Debug|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Debug|x64.Build.0 = Debug|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Debug|x86.ActiveCfg = Debug|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Debug|x86.Build.0 = Debug|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Release|Any CPU.Build.0 = Release|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Release|x64.ActiveCfg = Release|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Release|x64.Build.0 = Release|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Release|x86.ActiveCfg = Release|Any CPU + {0D9BD20E-7EBC-4689-B621-676440441223}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -217,6 +289,12 @@ Global {53F4E6F9-6B91-45F9-97F9-F6EFA0EBEFCE} = {0C00A596-0FE3-4FA6-B54B-FE2BE83371EF} {3BABD4D9-56A1-4BA3-B30C-30E6765AB389} = {0C00A596-0FE3-4FA6-B54B-FE2BE83371EF} {4EB86635-CF79-4D15-909E-C41C98B0B586} = {0C00A596-0FE3-4FA6-B54B-FE2BE83371EF} + {2BE71A01-0660-4264-B03F-8A386AB840ED} = {1C120673-72F4-4679-AC4C-68286E9091A5} + {2A2976A2-4578-49D1-BC3B-A9D466837CCB} = {2BE71A01-0660-4264-B03F-8A386AB840ED} + {69EAD5CE-01BD-4132-B65B-69EAC7DA10AF} = {2BE71A01-0660-4264-B03F-8A386AB840ED} + {020D0652-9802-431D-836A-B9ED27D841A2} = {2BE71A01-0660-4264-B03F-8A386AB840ED} + {767C29FF-C92E-424D-84D4-FF606FACD07A} = {2BE71A01-0660-4264-B03F-8A386AB840ED} + {0D9BD20E-7EBC-4689-B621-676440441223} = {2BE71A01-0660-4264-B03F-8A386AB840ED} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E418719F-3193-403E-AF58-9BE9F94FD8BE} diff --git a/src/Services/Attendance/Attendance.API/Application/Behaviors/LoggingBehavior.cs b/src/Services/Attendance/Attendance.API/Application/Behaviors/LoggingBehavior.cs new file mode 100644 index 00000000..6b67304e --- /dev/null +++ b/src/Services/Attendance/Attendance.API/Application/Behaviors/LoggingBehavior.cs @@ -0,0 +1,45 @@ +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace OpenCodeFoundation.ESchool.Services.Attending.API.Application.Behaviors +{ + public class LoggingBehavior + : IPipelineBehavior + where TRequest : notnull + { + private readonly ILogger> _logger; + + public LoggingBehavior( + ILogger> logger) + { + _logger = logger ?? throw new System.ArgumentNullException(nameof(logger)); + } + + public async Task Handle( + TRequest request, + CancellationToken cancellationToken, + RequestHandlerDelegate next) + { + if (next is null) + { + throw new System.ArgumentNullException(nameof(next)); + } + + _logger.LogInformation( + "Handling request {RequestName} ({@Request})", + request.GetType().Name, + request); + + var response = await next().ConfigureAwait(false); + + _logger.LogInformation( + "Request {RequestName} handled. Response: {@Response}", + request.GetType().Name, + response); + + return response; + } + } +} diff --git a/src/Services/Attendance/Attendance.API/Application/Commands/AttendanceApplicationCommand.cs b/src/Services/Attendance/Attendance.API/Application/Commands/AttendanceApplicationCommand.cs new file mode 100644 index 00000000..e995ff5a --- /dev/null +++ b/src/Services/Attendance/Attendance.API/Application/Commands/AttendanceApplicationCommand.cs @@ -0,0 +1,9 @@ +using MediatR; + +namespace OpenCodeFoundation.ESchool.Services.Attending.API.Application.Commands +{ + public record AttendanceApplicationCommand( + string StudentId, + string CourseId) + : IRequest; +} diff --git a/src/Services/Attendance/Attendance.API/Application/Commands/AttendanceApplicationCommandHandler.cs b/src/Services/Attendance/Attendance.API/Application/Commands/AttendanceApplicationCommandHandler.cs new file mode 100644 index 00000000..ba3b0738 --- /dev/null +++ b/src/Services/Attendance/Attendance.API/Application/Commands/AttendanceApplicationCommandHandler.cs @@ -0,0 +1,41 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.Extensions.Logging; +using OpenCodeFoundation.ESchool.Services.Attending.Infrastructure; + +namespace OpenCodeFoundation.ESchool.Services.Attending.API.Application.Commands +{ + public sealed class AttendanceApplicationCommandHandler + : IRequestHandler + { + private readonly ILogger _logger; + private readonly AttendanceContext _context; + + public AttendanceApplicationCommandHandler( + AttendanceContext context, + ILogger logger) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task Handle( + AttendanceApplicationCommand command, + CancellationToken cancellationToken) + { + if (command == null) + { + throw new ArgumentNullException(nameof(command)); + } + + var attendance = new Domain.AggregatesModel.AttendanceAggregate.Attendance(command.StudentId, command.CourseId); + await _context.Attendances.AddAsync(attendance, cancellationToken) + .ConfigureAwait(false); + await _context.SaveChangesAsync(cancellationToken) + .ConfigureAwait(false); + return true; + } + } +} diff --git a/src/Services/Attendance/Attendance.API/Application/Validations/AttendanceApplicationCommandValidator.cs b/src/Services/Attendance/Attendance.API/Application/Validations/AttendanceApplicationCommandValidator.cs new file mode 100644 index 00000000..473a2197 --- /dev/null +++ b/src/Services/Attendance/Attendance.API/Application/Validations/AttendanceApplicationCommandValidator.cs @@ -0,0 +1,15 @@ +using FluentValidation; +using OpenCodeFoundation.ESchool.Services.Attending.API.Application.Commands; + +namespace OpenCodeFoundation.ESchool.Services.Attending.API.Application.Validations +{ + public class AttendanceApplicationCommandValidator + : AbstractValidator + { + public AttendanceApplicationCommandValidator() + { + RuleFor(application => application.CourseId).NotEmpty(); + RuleFor(application => application.StudentId).NotEmpty(); + } + } +} diff --git a/src/Services/Attendance/Attendance.API/Attending.API.csproj b/src/Services/Attendance/Attendance.API/Attending.API.csproj new file mode 100644 index 00000000..5907651c --- /dev/null +++ b/src/Services/Attendance/Attendance.API/Attending.API.csproj @@ -0,0 +1,43 @@ + + + net5.0 + Attendance.API + OpenCodeFoundation.ESchool.Services.Attending.API + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Services/Attendance/Attendance.API/Controllers/AttendancesController.cs b/src/Services/Attendance/Attendance.API/Controllers/AttendancesController.cs new file mode 100644 index 00000000..0ed9f06e --- /dev/null +++ b/src/Services/Attendance/Attendance.API/Controllers/AttendancesController.cs @@ -0,0 +1,31 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using OpenCodeFoundation.ESchool.Services.Attending.API.Application.Commands; + +namespace OpenCodeFoundation.ESchool.Services.Attending.API.Controllers +{ + [ApiController] + [Route("[controller]")] + public class AttendancesController : ControllerBase + { + private readonly IMediator _mediator; + + public AttendancesController(IMediator mediator) + { + _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator)); + } + + [HttpPost] + public async Task Post( + [FromBody] AttendanceApplicationCommand command, + CancellationToken cancellationToken) + { + await _mediator.Send(command, cancellationToken) + .ConfigureAwait(false); + return Ok(); + } + } +} diff --git a/src/Services/Attendance/Attendance.API/Dockerfile b/src/Services/Attendance/Attendance.API/Dockerfile new file mode 100644 index 00000000..f19202e6 --- /dev/null +++ b/src/Services/Attendance/Attendance.API/Dockerfile @@ -0,0 +1,46 @@ +FROM mcr.microsoft.com/dotnet/aspnet:5.0-buster-slim AS base +WORKDIR /app +EXPOSE 80 + +FROM mcr.microsoft.com/dotnet/sdk:5.0-buster-slim AS build +WORKDIR /src + +COPY "eSchool.sln" "eSchool.sln" + +COPY "src/ApiGateways/eSchool.GraphQL/eSchool.GraphQL.csproj" "src/ApiGateways/eSchool.GraphQL/eSchool.GraphQL.csproj" + +COPY "src/Services/Attending/Attending.API/Attending.API.csproj" "src/Services/Attending/Attending.API/Attending.API.csproj" +COPY "src/Services/Attending/Attending.Domain/Attending.Domain.csproj" "src/Services/Attending/Attending.Domain/Attending.Domain.csproj" +COPY "src/Services/Attending/Attending.Infrastructure/Attending.Infrastructure.csproj" "src/Services/Attending/Attending.Infrastructure/Attending.Infrastructure.csproj" +COPY "src/Services/Attending/Attending.UnitTests/Attending.UnitTests.csproj" "src/Services/Attending/Attending.UnitTests/Attending.UnitTests.csproj" +COPY "src/Services/Attending/Attending.FunctionalTests/Attending.FunctionalTests.csproj" "src/Services/Attending/Attending.FunctionalTests/Attending.FunctionalTests.csproj" + +COPY "src/Libraries/OpenTelemetry/OpenTelemetry.csproj" "src/Libraries/OpenTelemetry/OpenTelemetry.csproj" + +COPY "src/Web/WebStatus/WebStatus.csproj" "src/Web/WebStatus/WebStatus.csproj" + +COPY "src/Web/Frontend.Blazor/Frontend.Blazor.Server/Frontend.Blazor.Server.csproj" "src/Web/Frontend.Blazor/Frontend.Blazor.Server/Frontend.Blazor.Server.csproj" +COPY "src/Web/Frontend.Blazor/Frontend.Blazor.Client/Frontend.Blazor.Client.csproj" "src/Web/Frontend.Blazor/Frontend.Blazor.Client/Frontend.Blazor.Client.csproj" +COPY "src/Web/Frontend.Blazor/Frontend.Blazor.Shared/Frontend.Blazor.Shared.csproj" "src/Web/Frontend.Blazor/Frontend.Blazor.Shared/Frontend.Blazor.Shared.csproj" + +COPY "docker-compose.dcproj" "docker-compose.dcproj" + +RUN dotnet restore eSchool.sln -nowarn:msb3202,nu1503 + +COPY . . +WORKDIR /src/src/Services/Attending/Attending.API +RUN dotnet build --no-restore -c Release -o /app/build + +FROM build as unittest +WORKDIR /src/src/Services/Attending/Attending.UnitTests + +FROM build as functionaltest +WORKDIR /src/src/Services/Attending/Attending.FunctionalTests + +FROM build AS publish +RUN dotnet publish --no-restore -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Attending.API.dll"] \ No newline at end of file diff --git a/src/Services/Attendance/Attendance.API/Extensions/ServiceCollectionExtensions.cs b/src/Services/Attendance/Attendance.API/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..8688afae --- /dev/null +++ b/src/Services/Attendance/Attendance.API/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace OpenCodeFoundation.ESchool.Services.Attending.API.Extensions +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection AddCustomHealthChecks( + this IServiceCollection services, + IConfiguration configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + var hcBuilder = services.AddHealthChecks(); + + hcBuilder + .AddCheck("self", () => HealthCheckResult.Healthy()) + .AddSqlServer( + configuration["ConnectionStrings"], + name: "AttendanceDB-check", + tags: new string[] { "attendancedb" }); + + return services; + } + } +} diff --git a/src/Services/Attendance/Attendance.API/Program.cs b/src/Services/Attendance/Attendance.API/Program.cs new file mode 100644 index 00000000..ec8e11b2 --- /dev/null +++ b/src/Services/Attendance/Attendance.API/Program.cs @@ -0,0 +1,87 @@ +using System; +using System.Diagnostics; +using System.IO; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using OpenCodeFoundation.ESchool.Services.Attending.Infrastructure; +using Serilog; +using Serilog.Enrichers.Span; + +namespace OpenCodeFoundation.ESchool.Services.Attending.API +{ + public static class Program + { + public static readonly string Namespace = typeof(Program).Namespace!; + public static readonly string AppName = Namespace.Substring(Namespace.LastIndexOf('.', Namespace.LastIndexOf('.') - 1) + 1); + + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1031:Do not catch general exception types", + Justification = "Top level all exception catcher")] + public static int Main(string[] args) + { + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + + var configuration = GetConfiguration(); + + Log.Logger = CreateSerilogLogger(configuration); + + try + { + Log.Information("Configuring web host ({ApplicationContext})...", AppName); + var host = CreateHostBuilder(configuration, args).Build(); + + Log.Information("Applying migrations ({ApplicationContext})...", AppName); + host.MigrateDbContext((_, _) => { }); + + Log.Information("Starting web host ({ApplicationContext})...", AppName); + host.Run(); + + return 0; + } + catch (Exception ex) + { + Log.Fatal(ex, "Host terminated unexpectedly"); + return 1; + } + finally + { + Log.CloseAndFlush(); + } + } + + public static IHostBuilder CreateHostBuilder(IConfiguration configuration, string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + webBuilder.UseConfiguration(configuration); + webBuilder.UseSerilog(); + }); + + private static ILogger CreateSerilogLogger(IConfiguration configuration) + { + return new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("ApplicationContext", AppName) + .Enrich.FromLogContext() + .Enrich.WithSpan() + .WriteTo.Console() + .WriteTo.Seq("http://seq") + .ReadFrom.Configuration(configuration) + .CreateLogger(); + } + + private static IConfiguration GetConfiguration() + { + var builder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .AddEnvironmentVariables(); + + // Load other configurations here. Ex. Keyvault or AppConfiguration + return builder.Build(); + } + } +} diff --git a/src/Services/Attendance/Attendance.API/Startup.cs b/src/Services/Attendance/Attendance.API/Startup.cs new file mode 100644 index 00000000..d45a96b1 --- /dev/null +++ b/src/Services/Attendance/Attendance.API/Startup.cs @@ -0,0 +1,115 @@ +using System; +using System.Reflection; +using FluentValidation.AspNetCore; +using HealthChecks.UI.Client; +using MediatR; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.Hosting; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; +using OpenCodeFoundation.ESchool.Services.Attending.API.Application.Behaviors; +using OpenCodeFoundation.ESchool.Services.Attending.API.Application.Validations; +using OpenCodeFoundation.ESchool.Services.Attending.API.Extensions; +using OpenCodeFoundation.ESchool.Services.Attending.Infrastructure; +using OpenCodeFoundation.OpenTelemetry; +using Serilog; + +namespace OpenCodeFoundation.ESchool.Services.Attending.API +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddMediatR(typeof(Startup).GetTypeInfo().Assembly); + + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + + services.AddDbContext(options => + { + options.UseSqlServer( + Configuration["ConnectionStrings"], + sqlServerOptionsAction: sqlOptions => + { + sqlOptions.MigrationsAssembly(typeof(AttendanceContext).GetTypeInfo().Assembly.GetName().Name); + sqlOptions.EnableRetryOnFailure(maxRetryCount: 15, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null); + }); + }); + + + services.AddControllers() + .AddJsonOptions(options => + { + options.JsonSerializerOptions.IgnoreNullValues = true; + }) + .AddFluentValidation(fv => + fv.RegisterValidatorsFromAssemblyContaining()); + + services.AddCustomHealthChecks(Configuration); + + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "Attendance HTTP API", Version = "v1" }); + }); + + services.AddOpenTelemetryIntegration(); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) + { + var pathBase = Configuration["PATH_BASE"]; + if (!string.IsNullOrEmpty(pathBase)) + { + loggerFactory.CreateLogger().LogInformation("Using PATH BASE '{pathBase}'", pathBase); + app.UsePathBase(pathBase); + } + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + + app.UseSerilogRequestLogging(); + + app.UseSwagger() + .UseSwaggerUI(c => + { + c.SwaggerEndpoint($"{pathBase}/swagger/v1/swagger.json", "Attendance HTTP API"); + }); + + app.UseRouting(); + + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + + endpoints.MapHealthChecks("/hc", new HealthCheckOptions() + { + Predicate = _ => true, + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse, + }); + endpoints.MapHealthChecks("/liveness", new HealthCheckOptions() + { + Predicate = r => r.Name.Contains("self", StringComparison.Ordinal), + }); + + endpoints.MapGraphQL(); + }); + } + } +} diff --git a/src/Services/Attendance/Attendance.API/WebHostExtensions.cs b/src/Services/Attendance/Attendance.API/WebHostExtensions.cs new file mode 100644 index 00000000..28305477 --- /dev/null +++ b/src/Services/Attendance/Attendance.API/WebHostExtensions.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Polly; + +namespace OpenCodeFoundation.ESchool.Services.Attending.API +{ + public static class WebHostExtensions + { + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1031:Do not catch general exception types", + Justification = "Top level all exception catcher")] + public static IHost MigrateDbContext( + this IHost webHost, + Action seeder) + where TContext : DbContext + { + if (webHost == null) + { + throw new ArgumentNullException(nameof(webHost)); + } + + using var scope = webHost.Services.CreateScope(); + var services = scope.ServiceProvider; + var logger = services.GetRequiredService>(); + var context = services.GetRequiredService(); + + try + { + logger.LogInformation("Migrating database associated with context {ContextName}", typeof(TContext).Name); + + var retry = Policy.Handle() + .WaitAndRetry(new TimeSpan[] + { + TimeSpan.FromSeconds(5), + TimeSpan.FromSeconds(10), + TimeSpan.FromSeconds(15), + }); + + retry.Execute(() => + { + context.Database.Migrate(); + seeder(context, services); + }); + + logger.LogInformation($"Migrated database associated with context {typeof(TContext).Name}"); + } + catch (Exception ex) + { + logger.LogError(ex, $"An error occurred while migrating the databases used on context {typeof(TContext).Name}"); + } + + return webHost; + } + } +} diff --git a/src/Services/Attendance/Attendance.API/appsettings.json b/src/Services/Attendance/Attendance.API/appsettings.json new file mode 100644 index 00000000..fca964ca --- /dev/null +++ b/src/Services/Attendance/Attendance.API/appsettings.json @@ -0,0 +1,24 @@ +{ + "AllowedHosts": "*", + "ConnectionStrings": "Server=tcp:127.0.0.1,5433;Database=OpenCodeFoundation.AttendingDb;User Id=sa;Password=Pass@word;", + "OpenTelemetry": { + "Enabled": true, + "Istio": false, + "Jaeger": { + "Enabled": true, + "ServiceName": "attending.api", + "Host": "jaeger", + "Port": 6833 + } + }, + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft": "Warning", + "OpenCodeFoundation": "Information", + "System": "Warning" + } + } + } +} \ No newline at end of file diff --git a/src/Services/Attendance/Attendance.Domain/AggregatesModel/AttendanceAggregate/Attendance.cs b/src/Services/Attendance/Attendance.Domain/AggregatesModel/AttendanceAggregate/Attendance.cs new file mode 100644 index 00000000..be2792ec --- /dev/null +++ b/src/Services/Attendance/Attendance.Domain/AggregatesModel/AttendanceAggregate/Attendance.cs @@ -0,0 +1,23 @@ +using System; +using OpenCodeFoundation.ESchool.Services.Attending.Domain.SeedWork; + +namespace OpenCodeFoundation.ESchool.Services.Attending.Domain.AggregatesModel.AttendanceAggregate +{ + public class Attendance + : Entity, IAggregateRoot + { + public Attendance( + string studentId, + string courseId) + { + StudentId = !string.IsNullOrWhiteSpace(studentId) ? studentId + : throw new ArgumentNullException(nameof(studentId)); + CourseId = !string.IsNullOrWhiteSpace(courseId) ? courseId + : throw new ArgumentNullException(nameof(courseId)); + } + + public string StudentId { get; private set; } + + public string CourseId { get; private set; } + } +} diff --git a/src/Services/Attendance/Attendance.Domain/AggregatesModel/AttendanceAggregate/IAttendanceRepository.cs b/src/Services/Attendance/Attendance.Domain/AggregatesModel/AttendanceAggregate/IAttendanceRepository.cs new file mode 100644 index 00000000..afc57fcb --- /dev/null +++ b/src/Services/Attendance/Attendance.Domain/AggregatesModel/AttendanceAggregate/IAttendanceRepository.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using OpenCodeFoundation.ESchool.Services.Attending.Domain.SeedWork; + +namespace OpenCodeFoundation.ESchool.Services.Attending.Domain.AggregatesModel.AttendanceAggregate +{ + public interface IAttendanceRepository + : IRepository + { + Attendance Add(Attendance attendance); + + Attendance Update(Attendance attendance); + + Task FindByIdAsync( + Guid id, + CancellationToken cancellationToken = default); + } +} diff --git a/src/Services/Attendance/Attendance.Domain/Attending.Domain.csproj b/src/Services/Attendance/Attendance.Domain/Attending.Domain.csproj new file mode 100644 index 00000000..b88b57c1 --- /dev/null +++ b/src/Services/Attendance/Attendance.Domain/Attending.Domain.csproj @@ -0,0 +1,10 @@ + + + + net5.0 + + Attending.Domain + OpenCodeFoundation.ESchool.Services.Attending.Domain + + + diff --git a/src/Services/Attendance/Attendance.Domain/SeedWork/Entity.cs b/src/Services/Attendance/Attendance.Domain/SeedWork/Entity.cs new file mode 100644 index 00000000..75110785 --- /dev/null +++ b/src/Services/Attendance/Attendance.Domain/SeedWork/Entity.cs @@ -0,0 +1,9 @@ +using System; + +namespace OpenCodeFoundation.ESchool.Services.Attending.Domain.SeedWork +{ + public abstract class Entity + { + public Guid Id { get; protected set; } + } +} diff --git a/src/Services/Attendance/Attendance.Domain/SeedWork/IAggregateRoot.cs b/src/Services/Attendance/Attendance.Domain/SeedWork/IAggregateRoot.cs new file mode 100644 index 00000000..07c297f9 --- /dev/null +++ b/src/Services/Attendance/Attendance.Domain/SeedWork/IAggregateRoot.cs @@ -0,0 +1,10 @@ +namespace OpenCodeFoundation.ESchool.Services.Attending.Domain.SeedWork +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1040:Avoid empty interfaces", + Justification = "Marker interface")] + public interface IAggregateRoot + { + } +} diff --git a/src/Services/Attendance/Attendance.Domain/SeedWork/IRepository.cs b/src/Services/Attendance/Attendance.Domain/SeedWork/IRepository.cs new file mode 100644 index 00000000..c5982e2b --- /dev/null +++ b/src/Services/Attendance/Attendance.Domain/SeedWork/IRepository.cs @@ -0,0 +1,11 @@ +namespace OpenCodeFoundation.ESchool.Services.Attending.Domain.SeedWork +{ + [System.Diagnostics.CodeAnalysis.SuppressMessage( + "Design", + "CA1040:Avoid empty interfaces", + Justification = "Marker interface")] + public interface IRepository + where T : IAggregateRoot + { + } +} diff --git a/src/Services/Attendance/Attendance.FunctionalTests/AttendanceTests.cs b/src/Services/Attendance/Attendance.FunctionalTests/AttendanceTests.cs new file mode 100644 index 00000000..7ed6d341 --- /dev/null +++ b/src/Services/Attendance/Attendance.FunctionalTests/AttendanceTests.cs @@ -0,0 +1,24 @@ +using System.Threading.Tasks; +using Xunit; + +namespace OpenCodeFoundation.ESchool.Services.Attending.FunctionalTests +{ + [Collection("TestServer")] + public class AttendanceTests + { + private readonly TestServerFixture _testServer; + + public AttendanceTests(TestServerFixture testServer) + { + _testServer = testServer ?? throw new System.ArgumentNullException(nameof(testServer)); + } + + [Fact] + public async Task Get_all_attending_ok_status_code() + { + var response = await _testServer.Client.GetAsync("/Attendances"); + + response.EnsureSuccessStatusCode(); + } + } +} diff --git a/src/Services/Attendance/Attendance.FunctionalTests/Attending.FunctionalTests.csproj b/src/Services/Attendance/Attendance.FunctionalTests/Attending.FunctionalTests.csproj new file mode 100644 index 00000000..2d7e9cb5 --- /dev/null +++ b/src/Services/Attendance/Attendance.FunctionalTests/Attending.FunctionalTests.csproj @@ -0,0 +1,24 @@ + + + net5.0 + OpenCodeFoundation.ESchool.Services.Attending.FunctionalTests + false + + + + + + + + + + + + + + + + Always + + + \ No newline at end of file diff --git a/src/Services/Attendance/Attendance.FunctionalTests/TestServerCollection.cs b/src/Services/Attendance/Attendance.FunctionalTests/TestServerCollection.cs new file mode 100644 index 00000000..6cbf7356 --- /dev/null +++ b/src/Services/Attendance/Attendance.FunctionalTests/TestServerCollection.cs @@ -0,0 +1,13 @@ +using Xunit; + +namespace OpenCodeFoundation.ESchool.Services.Attending.FunctionalTests +{ + [CollectionDefinition("TestServer")] + public class TestServerCollection + : ICollectionFixture + { + // This class has no code, and is never created. Its purpose is simply + // to be the place to apply [CollectionDefinition] and all the + // ICollectionFixture<> interfaces. + } +} diff --git a/src/Services/Attendance/Attendance.FunctionalTests/TestServerFixture.cs b/src/Services/Attendance/Attendance.FunctionalTests/TestServerFixture.cs new file mode 100644 index 00000000..28ab60fc --- /dev/null +++ b/src/Services/Attendance/Attendance.FunctionalTests/TestServerFixture.cs @@ -0,0 +1,55 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Reflection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using OpenCodeFoundation.ESchool.Services.Attending.API; +using OpenCodeFoundation.ESchool.Services.Attending.Infrastructure; +using Serilog; + +namespace OpenCodeFoundation.ESchool.Services.Attending.FunctionalTests +{ + public class TestServerFixture + : IDisposable + { + public TestServerFixture() + { + WebHost = CreateHost(); + WebHost.MigrateDbContext((_, __) => { }); + } + + public HttpClient Client => WebHost.GetTestClient(); + + public IHost WebHost { get; } + + public IHost CreateHost() + { + var path = Assembly.GetAssembly(typeof(TestServerFixture)) + .Location; + + var builder = Host.CreateDefaultBuilder() + .UseContentRoot(Path.GetDirectoryName(path)) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseTestServer(); + webBuilder.UseStartup(); + webBuilder.ConfigureAppConfiguration(config => + { + config.AddJsonFile("appsettings.json", optional: false) + .AddEnvironmentVariables(); + }); + webBuilder.UseSerilog(); + }); + + return builder.Start(); + } + + public void Dispose() + { + WebHost.Dispose(); + } + } +} diff --git a/src/Services/Attendance/Attendance.FunctionalTests/appsettings.json b/src/Services/Attendance/Attendance.FunctionalTests/appsettings.json new file mode 100644 index 00000000..5ca22b67 --- /dev/null +++ b/src/Services/Attendance/Attendance.FunctionalTests/appsettings.json @@ -0,0 +1,3 @@ +{ + "ConnectionStrings": "Server=tcp:127.0.0.1,5433;Database=OpenCodeFoundation.EnrollingDb;User Id=sa;Password=Pass@word;" +} \ No newline at end of file diff --git a/src/Services/Attendance/Attendance.Infrastructure/AttendanceContext.cs b/src/Services/Attendance/Attendance.Infrastructure/AttendanceContext.cs new file mode 100644 index 00000000..14dfa82d --- /dev/null +++ b/src/Services/Attendance/Attendance.Infrastructure/AttendanceContext.cs @@ -0,0 +1,34 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using OpenCodeFoundation.ESchool.Services.Attending.Domain.AggregatesModel.AttendanceAggregate; + +namespace OpenCodeFoundation.ESchool.Services.Attending.Infrastructure +{ + public class AttendanceContext + : DbContext + { + public AttendanceContext(DbContextOptions options) + : base(options) + { + } + + public DbSet Attendances { get; set; } = default!; + } + + /// + /// Helper class for creating migration. To create new migration, run the + /// command from `Enrolling.Intrastructure` folder. + /// + /// $ dotnet ef migrations add name_of_migration --startup-project ../Attending.API. + /// + public class AttendanceContextFactory : IDesignTimeDbContextFactory + { + public AttendanceContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseSqlServer("Server=.;Initial Catalog=OpenCodeFoundation.AttendanceDb;Integrated Security=true"); + + return new AttendanceContext(optionsBuilder.Options); + } + } +} diff --git a/src/Services/Attendance/Attendance.Infrastructure/Attending.Infrastructure.csproj b/src/Services/Attendance/Attendance.Infrastructure/Attending.Infrastructure.csproj new file mode 100644 index 00000000..cb9f3104 --- /dev/null +++ b/src/Services/Attendance/Attendance.Infrastructure/Attending.Infrastructure.csproj @@ -0,0 +1,17 @@ + + + net5.0 + Attendance.Infrastructure + OpenCodeFoundation.ESchool.Services.Attending.Infrastructure + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Services/Attendance/Attendance.Infrastructure/Migrations/AttendanceContextModelSnapshot.cs b/src/Services/Attendance/Attendance.Infrastructure/Migrations/AttendanceContextModelSnapshot.cs new file mode 100644 index 00000000..2c24d785 --- /dev/null +++ b/src/Services/Attendance/Attendance.Infrastructure/Migrations/AttendanceContextModelSnapshot.cs @@ -0,0 +1,39 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; + +namespace OpenCodeFoundation.ESchool.Services.Attending.Infrastructure.Migrations +{ + [DbContext(typeof(AttendanceContext))] + partial class AttendanceContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "3.1.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("OpenCodeFoundation.ESchool.Services.Attendance.Domain.AggregatesModel.AttendanceAggregate.Attendance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("StudentId") + .HasColumnType("nvarchar(max)"); + + b.Property("CourseId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Attendances"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Services/Attendance/Attendance.Infrastructure/Repositories/AttendanceRepository.cs b/src/Services/Attendance/Attendance.Infrastructure/Repositories/AttendanceRepository.cs new file mode 100644 index 00000000..302613fe --- /dev/null +++ b/src/Services/Attendance/Attendance.Infrastructure/Repositories/AttendanceRepository.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using OpenCodeFoundation.ESchool.Services.Attending.Domain.AggregatesModel.AttendanceAggregate; + +namespace OpenCodeFoundation.ESchool.Services.Attending.Infrastructure.Repositories +{ + public class AttendanceRepository + : IAttendanceRepository + { + private readonly AttendanceContext _context; + + public AttendanceRepository(AttendanceContext context) + { + _context = context ?? throw new ArgumentNullException(nameof(context)); + } + + public Attendance Add(Attendance attendance) + { + return _context.Attendances + .Add(attendance) + .Entity; + } + + public async Task FindByIdAsync( + Guid id, + CancellationToken cancellationToken = default) + { + return await _context.Attendances + .Where(e => e.Id == id) + .SingleOrDefaultAsync(cancellationToken) + .ConfigureAwait(false); + } + + public Attendance Update(Attendance attendance) + { + return _context.Attendances + .Update(attendance) + .Entity; + } + } +} diff --git a/src/Services/Attendance/Attendance.UnitTests/Attending.UnitTests.csproj b/src/Services/Attendance/Attendance.UnitTests/Attending.UnitTests.csproj new file mode 100644 index 00000000..0d73abf0 --- /dev/null +++ b/src/Services/Attendance/Attendance.UnitTests/Attending.UnitTests.csproj @@ -0,0 +1,20 @@ + + + net5.0 + false + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + \ No newline at end of file diff --git a/src/Services/Attendance/Attendance.UnitTests/Builders/AttendanceDto.cs b/src/Services/Attendance/Attendance.UnitTests/Builders/AttendanceDto.cs new file mode 100644 index 00000000..d9aa5c5f --- /dev/null +++ b/src/Services/Attendance/Attendance.UnitTests/Builders/AttendanceDto.cs @@ -0,0 +1,9 @@ +namespace Attending.UnitTests.Builders +{ + public class AttendanceDto + { + public string? StudentId { get; set; } + + public string? CourseId { get; set; } + } +} diff --git a/src/Services/Attendance/Attendance.UnitTests/Builders/AttendanceDtoBuilder.cs b/src/Services/Attendance/Attendance.UnitTests/Builders/AttendanceDtoBuilder.cs new file mode 100644 index 00000000..da0c356b --- /dev/null +++ b/src/Services/Attendance/Attendance.UnitTests/Builders/AttendanceDtoBuilder.cs @@ -0,0 +1,24 @@ +namespace Attending.UnitTests.Builders +{ + public class AttendanceDtoBuilder + { + private string? _studentId; + private string? _courseId; + + public AttendanceDtoBuilder WithDefaults() + { + _studentId = "2d16af83-15b7-4e28-be1d-25ed1630a365"; + _courseId = "8a5e3a17-115f-4df6-b6e4-000441a6b672"; + return this; + } + + public AttendanceDto Build() + { + return new AttendanceDto + { + StudentId = _studentId, + CourseId = _courseId + }; + } + } +} diff --git a/src/Services/Attendance/Attendance.UnitTests/Domain/AttendanceAggregateTests.cs b/src/Services/Attendance/Attendance.UnitTests/Domain/AttendanceAggregateTests.cs new file mode 100644 index 00000000..eff1055c --- /dev/null +++ b/src/Services/Attendance/Attendance.UnitTests/Domain/AttendanceAggregateTests.cs @@ -0,0 +1,24 @@ +using Attending.UnitTests.Builders; +using OpenCodeFoundation.ESchool.Services.Attending.Domain.AggregatesModel.AttendanceAggregate; +using Xunit; + +namespace Attending.UnitTests.Domain +{ + public class AttendanceAggregateTests + { + [Fact] + public void NewApplicationShouldSuccessWithValidInput() + { + var dto = new AttendanceDtoBuilder() + .WithDefaults() + .Build(); + + var attendance = new Attendance(dto.StudentId!, dto.CourseId!); + + Assert.NotNull(attendance); + Assert.Equal(dto.StudentId, attendance.StudentId); + Assert.Equal(dto.CourseId, attendance.CourseId); + } + + } +}