From 4ca234aec82a5d161692019881dd382d34097106 Mon Sep 17 00:00:00 2001 From: farcasclaudiu Date: Sat, 8 Oct 2022 09:54:30 +0300 Subject: [PATCH 1/3] WIP refactoring --- .../Controllers/EventsController.cs | 13 +++++--- PracticeCalendar.Api/Model/EventModel.cs | 10 +++--- .../PracticeCalendar.Application.csproj | 21 ++++++++++++ .../GetPracticeEventsQuery.cs | 32 +++++++++++++++++++ PracticeCalendar.Domain/Entities/Attendee.cs | 1 - .../PracticeCalendar.Infrastructure.csproj | 4 +-- PracticeCalendar.sln | 10 ++++-- 7 files changed, 77 insertions(+), 14 deletions(-) create mode 100644 PracticeCalendar.Application/PracticeCalendar.Application.csproj create mode 100644 PracticeCalendar.Application/PracticeEvents/Queries/GetPracticeEvents/GetPracticeEventsQuery.cs diff --git a/PracticeCalendar.Api/Controllers/EventsController.cs b/PracticeCalendar.Api/Controllers/EventsController.cs index 506dde0..aecb49b 100644 --- a/PracticeCalendar.Api/Controllers/EventsController.cs +++ b/PracticeCalendar.Api/Controllers/EventsController.cs @@ -13,7 +13,7 @@ namespace PrcaticeCalendar.Controllers { private readonly IRepository eventsRepo; private readonly IMapper mapper; - private readonly ILogger _logger; + private readonly ILogger logger; public EventsController(IRepository eventsRepo, IMapper mapper, @@ -21,12 +21,13 @@ namespace PrcaticeCalendar.Controllers { this.eventsRepo = eventsRepo; this.mapper = mapper; - _logger = logger; + this.logger = logger; } [HttpGet(Name = "GetAll")] public async Task>> Get() { + var spec = new PracticeEventsWithAttendees(); var repoList = await eventsRepo.ListAsync(spec); var evList = repoList.Select(x=> { @@ -69,6 +70,10 @@ namespace PrcaticeCalendar.Controllers public async Task DeleteEvent(int practiceEventId) { var org = await eventsRepo.GetByIdAsync(practiceEventId); + if (org == null) + { + return NotFound(); + } await eventsRepo.DeleteAsync(org); await eventsRepo.SaveChangesAsync(); return Ok(); @@ -79,7 +84,7 @@ namespace PrcaticeCalendar.Controllers public async Task AttendeeAcceptEvent(int eventId, int attendeeId) { var spec = new PracticeEventByIdWithAttendees(eventId); - var practiceEvent = await eventsRepo.GetBySpecAsync(spec); + var practiceEvent = await eventsRepo.FirstOrDefaultAsync(spec); if (practiceEvent == null) { return NotFound(); @@ -94,7 +99,7 @@ namespace PrcaticeCalendar.Controllers public async Task AttendeeDeclineEvent(int eventId, int attendeeId) { var spec = new PracticeEventByIdWithAttendees(eventId); - var practiceEvent = await eventsRepo.GetBySpecAsync(spec); + var practiceEvent = await eventsRepo.FirstOrDefaultAsync(spec); if (practiceEvent == null) { return NotFound(); diff --git a/PracticeCalendar.Api/Model/EventModel.cs b/PracticeCalendar.Api/Model/EventModel.cs index 8738d4a..e121af4 100644 --- a/PracticeCalendar.Api/Model/EventModel.cs +++ b/PracticeCalendar.Api/Model/EventModel.cs @@ -6,19 +6,19 @@ namespace PracticeCalendar.API.Model public class EventModel { public int Id { get; set; } - public string Title { get; set; } - public string Description { get; set; } + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; public DateTime StartTime { get; set; } public DateTime EndTime { get; set; } - public AttendeeModel[] Attendees { get; set; } = new AttendeeModel[0]; + public AttendeeModel[] Attendees { get; set; } = Array.Empty(); } public class AttendeeModel { - public string Name { get; set; } - public string EmailAddress { get; set; } + public string Name { get; set; } = string.Empty; + public string EmailAddress { get; set; } = string.Empty; public bool IsAttending { get; set; } } diff --git a/PracticeCalendar.Application/PracticeCalendar.Application.csproj b/PracticeCalendar.Application/PracticeCalendar.Application.csproj new file mode 100644 index 0000000..147046d --- /dev/null +++ b/PracticeCalendar.Application/PracticeCalendar.Application.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + + + + + + + + + + + + + + + diff --git a/PracticeCalendar.Application/PracticeEvents/Queries/GetPracticeEvents/GetPracticeEventsQuery.cs b/PracticeCalendar.Application/PracticeEvents/Queries/GetPracticeEvents/GetPracticeEventsQuery.cs new file mode 100644 index 0000000..21d45c0 --- /dev/null +++ b/PracticeCalendar.Application/PracticeEvents/Queries/GetPracticeEvents/GetPracticeEventsQuery.cs @@ -0,0 +1,32 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using PracticeCalendar.Domain.Common.Interfaces; +using PracticeCalendar.Domain.Entities; +using PracticeCalendar.Domain.Entities.Specifications; + +namespace PracticeCalendar.Application.PracticeEvents.Queries.GetPracticeEvents +{ + public record class GetPracticeEventsQuery : IRequest> + { + } + + public class GetPracticeEventsQueryHandler : IRequestHandler> + { + private readonly ILogger logger; + private readonly IRepository eventsRepo; + + public GetPracticeEventsQueryHandler(IRepository eventsRepo, + ILogger logger) + { + this.eventsRepo = eventsRepo; + this.logger = logger; + } + + public async Task> Handle(GetPracticeEventsQuery request, CancellationToken cancellationToken) + { + var spec = new PracticeEventsWithAttendees(); + var evList = await eventsRepo.ListAsync(spec); + return evList; + } + } +} diff --git a/PracticeCalendar.Domain/Entities/Attendee.cs b/PracticeCalendar.Domain/Entities/Attendee.cs index d376b21..8a78a53 100644 --- a/PracticeCalendar.Domain/Entities/Attendee.cs +++ b/PracticeCalendar.Domain/Entities/Attendee.cs @@ -1,6 +1,5 @@ using Ardalis.GuardClauses; using PracticeCalendar.Domain.Common; -using System.Diagnostics.Contracts; namespace PracticeCalendar.Domain.Entities { diff --git a/PracticeCalendar.Infrastructure/PracticeCalendar.Infrastructure.csproj b/PracticeCalendar.Infrastructure/PracticeCalendar.Infrastructure.csproj index 09d0693..02e0568 100644 --- a/PracticeCalendar.Infrastructure/PracticeCalendar.Infrastructure.csproj +++ b/PracticeCalendar.Infrastructure/PracticeCalendar.Infrastructure.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -13,7 +13,7 @@ - + diff --git a/PracticeCalendar.sln b/PracticeCalendar.sln index 3598091..4e6da5b 100644 --- a/PracticeCalendar.sln +++ b/PracticeCalendar.sln @@ -7,15 +7,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PracticeCalendar.API", "Pra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PracticeCalendar.Domain", "PracticeCalendar.Domain\PracticeCalendar.Domain.csproj", "{002B8118-8B5A-4CF3-A29D-12A06803221B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PracticeCalendar.Infrastructure", "PracticeCalendar.Infrastructure\PracticeCalendar.Infrastructure.csproj", "{211BEF2A-5FB1-4F55-84FB-88FEF90A8316}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PracticeCalendar.Infrastructure", "PracticeCalendar.Infrastructure\PracticeCalendar.Infrastructure.csproj", "{211BEF2A-5FB1-4F55-84FB-88FEF90A8316}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PracticeCalendar.UnitTests", "PracticeCalendar.UnitTests\PracticeCalendar.UnitTests.csproj", "{74849455-5E08-43FE-A718-0872DE7BC350}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PracticeCalendar.UnitTests", "PracticeCalendar.UnitTests\PracticeCalendar.UnitTests.csproj", "{74849455-5E08-43FE-A718-0872DE7BC350}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{5F2B7855-F03D-48C9-8733-FF1E077F18F5}" ProjectSection(SolutionItems) = preProject README.md = README.md EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PracticeCalendar.Application", "PracticeCalendar.Application\PracticeCalendar.Application.csproj", "{094CA45E-92DD-47A5-A7EF-F867DB8B0625}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {74849455-5E08-43FE-A718-0872DE7BC350}.Debug|Any CPU.Build.0 = Debug|Any CPU {74849455-5E08-43FE-A718-0872DE7BC350}.Release|Any CPU.ActiveCfg = Release|Any CPU {74849455-5E08-43FE-A718-0872DE7BC350}.Release|Any CPU.Build.0 = Release|Any CPU + {094CA45E-92DD-47A5-A7EF-F867DB8B0625}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {094CA45E-92DD-47A5-A7EF-F867DB8B0625}.Debug|Any CPU.Build.0 = Debug|Any CPU + {094CA45E-92DD-47A5-A7EF-F867DB8B0625}.Release|Any CPU.ActiveCfg = Release|Any CPU + {094CA45E-92DD-47A5-A7EF-F867DB8B0625}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From 28a6981001834f92614c2891cdecd02be1290493 Mon Sep 17 00:00:00 2001 From: farcasclaudiu Date: Mon, 10 Oct 2022 03:17:14 +0300 Subject: [PATCH 2/3] nice refactorings --- .../Controllers/ApiControllerBase.cs | 14 +++ .../Controllers/EventsController.cs | 92 ++++-------------- PracticeCalendar.Api/Model/EventModel.cs | 34 ------- .../PracticeCalendar.API.csproj | 6 +- PracticeCalendar.Api/Program.cs | 9 +- PracticeCalendar.Api/practicecalendar.sqlite | Bin 20480 -> 20480 bytes .../ConfigureServices.cs | 18 ++++ .../PracticeCalendar.Application.csproj | 9 +- .../Commands/AttendeeAcceptEventCommand.cs | 44 +++++++++ .../Commands/AttendeeDeclineEventCommand.cs | 43 ++++++++ .../Commands/CreatePracticeEventCommand.cs | 41 ++++++++ .../Commands/DeletePracticeEventCommand.cs | 39 ++++++++ .../Commands/UpdatePracticeEventCommand.cs | 43 ++++++++ .../PracticeEvents/Queries/AttendeeDto.cs | 10 ++ .../GetPracticeEventsQuery.cs | 22 +++-- .../Queries/PracticeEventDto.cs | 14 +++ .../Entities/PracticeEvent.cs | 5 +- .../PracticeEventsWithAttendees.cs | 3 +- .../Exceptions/PracticeEventNotFound.cs | 8 ++ ...frastructureDI.cs => ConfigureServices.cs} | 6 +- .../{ => Domain}/PracticeEventTest.cs | 27 ++--- .../Integration/BaseTest.cs | 12 +++ .../CustomWebApplicationFactory.cs | 33 +++++++ .../CreatePracticeEventsTest.cs | 43 ++++++++ .../PracticeEvents/GetPracticeEventsTest.cs | 48 +++++++++ .../ServiceCollectionExtensions.cs | 20 ++++ .../Integration/Testing.cs | 76 +++++++++++++++ .../PracticeCalendar.UnitTests.csproj | 27 ++++- PracticeCalendar.UnitTests/appsettings.json | 13 +++ PracticeCalendar.UnitTests/xunit.runner.json | 4 + 30 files changed, 622 insertions(+), 141 deletions(-) create mode 100644 PracticeCalendar.Api/Controllers/ApiControllerBase.cs delete mode 100644 PracticeCalendar.Api/Model/EventModel.cs create mode 100644 PracticeCalendar.Application/ConfigureServices.cs create mode 100644 PracticeCalendar.Application/PracticeEvents/Commands/AttendeeAcceptEventCommand.cs create mode 100644 PracticeCalendar.Application/PracticeEvents/Commands/AttendeeDeclineEventCommand.cs create mode 100644 PracticeCalendar.Application/PracticeEvents/Commands/CreatePracticeEventCommand.cs create mode 100644 PracticeCalendar.Application/PracticeEvents/Commands/DeletePracticeEventCommand.cs create mode 100644 PracticeCalendar.Application/PracticeEvents/Commands/UpdatePracticeEventCommand.cs create mode 100644 PracticeCalendar.Application/PracticeEvents/Queries/AttendeeDto.cs create mode 100644 PracticeCalendar.Application/PracticeEvents/Queries/PracticeEventDto.cs create mode 100644 PracticeCalendar.Domain/Exceptions/PracticeEventNotFound.cs rename PracticeCalendar.Infrastructure/{InfrastructureDI.cs => ConfigureServices.cs} (83%) rename PracticeCalendar.UnitTests/{ => Domain}/PracticeEventTest.cs (72%) create mode 100644 PracticeCalendar.UnitTests/Integration/BaseTest.cs create mode 100644 PracticeCalendar.UnitTests/Integration/CustomWebApplicationFactory.cs create mode 100644 PracticeCalendar.UnitTests/Integration/PracticeEvents/CreatePracticeEventsTest.cs create mode 100644 PracticeCalendar.UnitTests/Integration/PracticeEvents/GetPracticeEventsTest.cs create mode 100644 PracticeCalendar.UnitTests/Integration/ServiceCollectionExtensions.cs create mode 100644 PracticeCalendar.UnitTests/Integration/Testing.cs create mode 100644 PracticeCalendar.UnitTests/appsettings.json create mode 100644 PracticeCalendar.UnitTests/xunit.runner.json diff --git a/PracticeCalendar.Api/Controllers/ApiControllerBase.cs b/PracticeCalendar.Api/Controllers/ApiControllerBase.cs new file mode 100644 index 0000000..6121624 --- /dev/null +++ b/PracticeCalendar.Api/Controllers/ApiControllerBase.cs @@ -0,0 +1,14 @@ +using MediatR; +using Microsoft.AspNetCore.Mvc; + +namespace PracticeCalendar.API.Controllers +{ + [ApiController] + [Route("api/[controller]")] + public class ApiControllerBase : ControllerBase + { + private ISender _mediator = null!; + + protected ISender Mediator => _mediator ??= HttpContext.RequestServices.GetRequiredService(); + } +} diff --git a/PracticeCalendar.Api/Controllers/EventsController.cs b/PracticeCalendar.Api/Controllers/EventsController.cs index aecb49b..a521e5c 100644 --- a/PracticeCalendar.Api/Controllers/EventsController.cs +++ b/PracticeCalendar.Api/Controllers/EventsController.cs @@ -1,111 +1,61 @@ -using AutoMapper; using Microsoft.AspNetCore.Mvc; -using PracticeCalendar.API.Model; -using PracticeCalendar.Domain.Common.Interfaces; -using PracticeCalendar.Domain.Entities; -using PracticeCalendar.Domain.Entities.Specifications; +using PracticeCalendar.API.Controllers; +using PracticeCalendar.Application.PracticeEvents.Commands; +using PracticeCalendar.Application.PracticeEvents.Queries; +using PracticeCalendar.Application.PracticeEvents.Queries.GetPracticeEvents; namespace PrcaticeCalendar.Controllers { - [ApiController] - [Route("api/[controller]")] - public class EventsController : ControllerBase + public class EventsController : ApiControllerBase { - private readonly IRepository eventsRepo; - private readonly IMapper mapper; private readonly ILogger logger; - public EventsController(IRepository eventsRepo, - IMapper mapper, - ILogger logger) + public EventsController(ILogger logger) { - this.eventsRepo = eventsRepo; - this.mapper = mapper; this.logger = logger; } [HttpGet(Name = "GetAll")] - public async Task>> Get() + public async Task>> Get() { - - var spec = new PracticeEventsWithAttendees(); - var repoList = await eventsRepo.ListAsync(spec); - var evList = repoList.Select(x=> { - var model = mapper.Map(x); - model.Attendees = x.Attendees.Select(m=>mapper.Map(m)).ToArray(); - return model; - }) - .ToList(); - return evList; + return await Mediator.Send(new GetPracticeEventsQuery()); } [HttpPost(Name = "Create practice event")] - public async Task CreateEvent(EventModel eventModel) + public async Task> CreateEvent(PracticeEventDto eventModel) { - var practiceEvent = new PracticeEvent(eventModel.Title, eventModel.Description); - foreach (var att in eventModel.Attendees) - { - practiceEvent.AddAttendee(new Attendee(att.Name, att.EmailAddress)); - } - var result = await eventsRepo.AddAsync(practiceEvent); - await eventsRepo.SaveChangesAsync(); - return Ok(mapper.Map(result)); + return await Mediator.Send(new CreatePracticeEventCommand(eventModel)); } [HttpPut(Name = "Update practice event")] - public async Task UpdateEvent(EventModel eventModel) + public async Task> UpdateEvent(PracticeEventDto eventModel) { - var practiceEvent = await eventsRepo.GetByIdAsync(eventModel.Id); - if(practiceEvent == null) - { - return NotFound(); - } - practiceEvent.UpdateTitleAndDescription(eventModel.Title, eventModel.Description); - await eventsRepo.UpdateAsync(practiceEvent); - await eventsRepo.SaveChangesAsync(); - return Ok(mapper.Map(practiceEvent)); + return await Mediator.Send(new UpdatePracticeEventCommand(eventModel)); } [HttpDelete(Name = "Delete practice event")] - public async Task DeleteEvent(int practiceEventId) + public async Task DeleteEvent(int practiceEventId) { - var org = await eventsRepo.GetByIdAsync(practiceEventId); - if (org == null) - { - return NotFound(); - } - await eventsRepo.DeleteAsync(org); - await eventsRepo.SaveChangesAsync(); + await Mediator.Send(new DeletePracticeEventCommand(practiceEventId)); + return Ok(); } [HttpPost] [Route("accept/{eventId}/{attendeeId}")] - public async Task AttendeeAcceptEvent(int eventId, int attendeeId) + public async Task AttendeeAcceptEvent(int eventId, int attendeeId) { - var spec = new PracticeEventByIdWithAttendees(eventId); - var practiceEvent = await eventsRepo.FirstOrDefaultAsync(spec); - if (practiceEvent == null) - { - return NotFound(); - } - practiceEvent.AttendeeAcceptEvent(attendeeId); - await eventsRepo.SaveChangesAsync(); + await Mediator.Send(new AttendeeAcceptEventCommand(eventId, attendeeId)); + return Ok(); } [HttpPost] [Route("decline/{eventId}/{attendeeId}")] - public async Task AttendeeDeclineEvent(int eventId, int attendeeId) + public async Task AttendeeDeclineEvent(int eventId, int attendeeId) { - var spec = new PracticeEventByIdWithAttendees(eventId); - var practiceEvent = await eventsRepo.FirstOrDefaultAsync(spec); - if (practiceEvent == null) - { - return NotFound(); - } - practiceEvent.AttendeeDeclineEvent(attendeeId); - await eventsRepo.SaveChangesAsync(); + await Mediator.Send(new AttendeeDeclineEventCommand(eventId, attendeeId)); + return Ok(); } } diff --git a/PracticeCalendar.Api/Model/EventModel.cs b/PracticeCalendar.Api/Model/EventModel.cs deleted file mode 100644 index e121af4..0000000 --- a/PracticeCalendar.Api/Model/EventModel.cs +++ /dev/null @@ -1,34 +0,0 @@ -using AutoMapper; -using PracticeCalendar.Domain.Entities; - -namespace PracticeCalendar.API.Model -{ - public class EventModel - { - public int Id { get; set; } - public string Title { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; - - public DateTime StartTime { get; set; } - public DateTime EndTime { get; set; } - - public AttendeeModel[] Attendees { get; set; } = Array.Empty(); - } - - public class AttendeeModel - { - public string Name { get; set; } = string.Empty; - public string EmailAddress { get; set; } = string.Empty; - public bool IsAttending { get; set; } - } - - public class EventModelProfile : Profile - { - public EventModelProfile() - { - CreateMap(); - CreateMap(); - CreateMap(); - } - } -} diff --git a/PracticeCalendar.Api/PracticeCalendar.API.csproj b/PracticeCalendar.Api/PracticeCalendar.API.csproj index d9e4b7b..40f69c1 100644 --- a/PracticeCalendar.Api/PracticeCalendar.API.csproj +++ b/PracticeCalendar.Api/PracticeCalendar.API.csproj @@ -9,8 +9,8 @@ - + @@ -19,4 +19,8 @@ + + + + diff --git a/PracticeCalendar.Api/Program.cs b/PracticeCalendar.Api/Program.cs index 773fe32..9c4d85c 100644 --- a/PracticeCalendar.Api/Program.cs +++ b/PracticeCalendar.Api/Program.cs @@ -1,5 +1,6 @@ using Hellang.Middleware.ProblemDetails; using Microsoft.AspNetCore.Hosting; +using PracticeCalendar.Application; using PracticeCalendar.Infrastructure; using PracticeCalendar.Infrastructure.Persistence; using System; @@ -16,14 +17,18 @@ namespace PracticeCalendar builder.Services.AddProblemDetails(); - builder.Services.AddAutoMapper(typeof(Program)); builder.Services.AddControllers(); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); + //inject application + builder.Services.AddApplicationServices(); //inject infrastructure - builder.Services.AddInfrastructure(builder.Configuration); + builder.Services.AddInfrastructureServices(builder.Configuration); + + //mapster + var app = builder.Build(); diff --git a/PracticeCalendar.Api/practicecalendar.sqlite b/PracticeCalendar.Api/practicecalendar.sqlite index b7646ed4573d62b8ce8701cdf29a3f17260a5139..5e6e9144cf8b9e9f0d453bfb00c7976d597c02ae 100644 GIT binary patch delta 337 zcmZozz}T>Wae_3X=tLQ3M$wH4OZY`N_#_$l_wYCId+_t|o#X4}3+9vDtSG?9$7I1Z zSwP;BkrPN-NZWI;F~}<#8yA-pW#*+TSXjUr7L#M-)uim$fb#Nic{4MpyqOtLT75IW z{2c*ZP9a8SO~!zt#N?992CkKe+UWae_1>_e2?IM(&LXOZfR%`7SW<@8NIa_u%K_yRcbMVIQA_4I3MStgNwd zaY<2TUb=!AjGnA3r@onA{*Hh;s}LizCSyQRVsc4la;j@tYF0K%*e?M0D5{cE&u=k diff --git a/PracticeCalendar.Application/ConfigureServices.cs b/PracticeCalendar.Application/ConfigureServices.cs new file mode 100644 index 0000000..8f0013d --- /dev/null +++ b/PracticeCalendar.Application/ConfigureServices.cs @@ -0,0 +1,18 @@ +using MapsterMapper; +using MediatR; +using Microsoft.Extensions.DependencyInjection; +using System.Reflection; + +namespace PracticeCalendar.Application +{ + public static class ConfigureServices + { + public static IServiceCollection AddApplicationServices(this IServiceCollection services) + { + services.AddSingleton(new Mapper(new Mapster.TypeAdapterConfig())); + services.AddMediatR(Assembly.GetExecutingAssembly()); + + return services; + } + } +} diff --git a/PracticeCalendar.Application/PracticeCalendar.Application.csproj b/PracticeCalendar.Application/PracticeCalendar.Application.csproj index 147046d..a8ccadf 100644 --- a/PracticeCalendar.Application/PracticeCalendar.Application.csproj +++ b/PracticeCalendar.Application/PracticeCalendar.Application.csproj @@ -11,11 +11,10 @@ - - - - - + + + + diff --git a/PracticeCalendar.Application/PracticeEvents/Commands/AttendeeAcceptEventCommand.cs b/PracticeCalendar.Application/PracticeEvents/Commands/AttendeeAcceptEventCommand.cs new file mode 100644 index 0000000..0461478 --- /dev/null +++ b/PracticeCalendar.Application/PracticeEvents/Commands/AttendeeAcceptEventCommand.cs @@ -0,0 +1,44 @@ +using MediatR; +using PracticeCalendar.Domain.Common.Interfaces; +using PracticeCalendar.Domain.Entities; +using PracticeCalendar.Domain.Entities.Specifications; +using PracticeCalendar.Domain.Exceptions; + +namespace PracticeCalendar.Application.PracticeEvents.Commands +{ + public record AttendeeAcceptEventCommand : IRequest + { + public AttendeeAcceptEventCommand(int eventId, int attendeeId) + { + EventId = eventId; + AttendeeId = attendeeId; + } + + public int EventId { get; } + public int AttendeeId { get; } + } + + public class AttendeeAcceptEventCommandHandler : IRequestHandler + { + private readonly IRepository eventsRepo; + + public AttendeeAcceptEventCommandHandler(IRepository eventsRepo) + { + this.eventsRepo = eventsRepo; + } + + public async Task Handle(AttendeeAcceptEventCommand request, CancellationToken cancellationToken) + { + var spec = new PracticeEventByIdWithAttendees(request.EventId); + var practiceEvent = await eventsRepo.FirstOrDefaultAsync(spec, cancellationToken); + if (practiceEvent == null) + { + throw new PracticeEventNotFoundException(); + } + practiceEvent.AttendeeAcceptEvent(request.AttendeeId); + await eventsRepo.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } + } +} diff --git a/PracticeCalendar.Application/PracticeEvents/Commands/AttendeeDeclineEventCommand.cs b/PracticeCalendar.Application/PracticeEvents/Commands/AttendeeDeclineEventCommand.cs new file mode 100644 index 0000000..a7d1908 --- /dev/null +++ b/PracticeCalendar.Application/PracticeEvents/Commands/AttendeeDeclineEventCommand.cs @@ -0,0 +1,43 @@ +using MediatR; +using PracticeCalendar.Domain.Common.Interfaces; +using PracticeCalendar.Domain.Entities; +using PracticeCalendar.Domain.Entities.Specifications; +using PracticeCalendar.Domain.Exceptions; + +namespace PracticeCalendar.Application.PracticeEvents.Commands +{ + public record AttendeeDeclineEventCommand : IRequest + { + public AttendeeDeclineEventCommand(int eventId, int attendeeId) + { + EventId = eventId; + AttendeeId = attendeeId; + } + + public int EventId { get; } + public int AttendeeId { get; } + } + + public class AttendeeDeclineEventCommandHandler : IRequestHandler + { + private readonly IRepository eventsRepo; + + public AttendeeDeclineEventCommandHandler(IRepository eventsRepo) + { + this.eventsRepo = eventsRepo; + } + + public async Task Handle(AttendeeDeclineEventCommand request, CancellationToken cancellationToken) + { + var spec = new PracticeEventByIdWithAttendees(request.EventId); + var practiceEvent = await eventsRepo.FirstOrDefaultAsync(spec, cancellationToken); + if (practiceEvent == null) + { + throw new PracticeEventNotFoundException(); + } + practiceEvent.AttendeeDeclineEvent(request.AttendeeId); + + return Unit.Value; + } + } +} diff --git a/PracticeCalendar.Application/PracticeEvents/Commands/CreatePracticeEventCommand.cs b/PracticeCalendar.Application/PracticeEvents/Commands/CreatePracticeEventCommand.cs new file mode 100644 index 0000000..81ea633 --- /dev/null +++ b/PracticeCalendar.Application/PracticeEvents/Commands/CreatePracticeEventCommand.cs @@ -0,0 +1,41 @@ +using Mapster; +using MediatR; +using PracticeCalendar.Application.PracticeEvents.Queries; +using PracticeCalendar.Domain.Common.Interfaces; +using PracticeCalendar.Domain.Entities; + +namespace PracticeCalendar.Application.PracticeEvents.Commands +{ + public record CreatePracticeEventCommand : IRequest + { + public CreatePracticeEventCommand(PracticeEventDto eventDto) + { + Event = eventDto; + } + + public PracticeEventDto Event { get; } + } + + public class CreatePracticeEventCommandHandler : IRequestHandler + { + private readonly IRepository eventsRepo; + + public CreatePracticeEventCommandHandler(IRepository eventsRepo) + { + this.eventsRepo = eventsRepo; + } + + public async Task Handle(CreatePracticeEventCommand request, CancellationToken cancellationToken) + { + var input = request.Event; + var practiceEvent = new PracticeEvent(input.Title, input.Description); + foreach (var att in input.Attendees) + { + practiceEvent.AddAttendee(new Attendee(att.Name, att.EmailAddress)); + } + var result = await eventsRepo.AddAsync(practiceEvent, cancellationToken); + await eventsRepo.SaveChangesAsync(cancellationToken); + return result.Adapt(); + } + } +} diff --git a/PracticeCalendar.Application/PracticeEvents/Commands/DeletePracticeEventCommand.cs b/PracticeCalendar.Application/PracticeEvents/Commands/DeletePracticeEventCommand.cs new file mode 100644 index 0000000..fe82131 --- /dev/null +++ b/PracticeCalendar.Application/PracticeEvents/Commands/DeletePracticeEventCommand.cs @@ -0,0 +1,39 @@ +using MediatR; +using PracticeCalendar.Domain.Common.Interfaces; +using PracticeCalendar.Domain.Entities; +using PracticeCalendar.Domain.Exceptions; + +namespace PracticeCalendar.Application.PracticeEvents.Commands +{ + public record DeletePracticeEventCommand : IRequest + { + public DeletePracticeEventCommand(int practiceEventId) + { + PracticeEventId = practiceEventId; + } + + public int PracticeEventId { get; } + } + public class DeletePracticeEventCommandHandler : IRequestHandler + { + private readonly IRepository eventsRepo; + + public DeletePracticeEventCommandHandler(IRepository eventsRepo) + { + this.eventsRepo = eventsRepo; + } + + public async Task Handle(DeletePracticeEventCommand request, CancellationToken cancellationToken) + { + var org = await eventsRepo.GetByIdAsync(request.PracticeEventId, cancellationToken); + if (org == null) + { + throw new PracticeEventNotFoundException(); + } + await eventsRepo.DeleteAsync(org, cancellationToken); + await eventsRepo.SaveChangesAsync(cancellationToken); + + return Unit.Value; + } + } +} diff --git a/PracticeCalendar.Application/PracticeEvents/Commands/UpdatePracticeEventCommand.cs b/PracticeCalendar.Application/PracticeEvents/Commands/UpdatePracticeEventCommand.cs new file mode 100644 index 0000000..d829ff8 --- /dev/null +++ b/PracticeCalendar.Application/PracticeEvents/Commands/UpdatePracticeEventCommand.cs @@ -0,0 +1,43 @@ +using Mapster; +using MediatR; +using PracticeCalendar.Application.PracticeEvents.Queries; +using PracticeCalendar.Domain.Common.Interfaces; +using PracticeCalendar.Domain.Entities; +using PracticeCalendar.Domain.Exceptions; + +namespace PracticeCalendar.Application.PracticeEvents.Commands +{ + public record UpdatePracticeEventCommand : IRequest + { + public UpdatePracticeEventCommand(PracticeEventDto eventDto) + { + Event = eventDto; + } + + public PracticeEventDto Event { get; } + } + + public class UpdatePracticeEventCommandHandler : IRequestHandler + { + private readonly IRepository eventsRepo; + + public UpdatePracticeEventCommandHandler(IRepository eventsRepo) + { + this.eventsRepo = eventsRepo; + } + + public async Task Handle(UpdatePracticeEventCommand request, CancellationToken cancellationToken) + { + var eventModel = request.Event; + var practiceEvent = await eventsRepo.GetByIdAsync(eventModel.Id, cancellationToken); + if (practiceEvent == null) + { + throw new PracticeEventNotFoundException(); + } + practiceEvent.UpdateTitleAndDescription(eventModel.Title, eventModel.Description); + await eventsRepo.UpdateAsync(practiceEvent, cancellationToken); + await eventsRepo.SaveChangesAsync(cancellationToken); + return practiceEvent.Adapt(); + } + } +} diff --git a/PracticeCalendar.Application/PracticeEvents/Queries/AttendeeDto.cs b/PracticeCalendar.Application/PracticeEvents/Queries/AttendeeDto.cs new file mode 100644 index 0000000..3795e46 --- /dev/null +++ b/PracticeCalendar.Application/PracticeEvents/Queries/AttendeeDto.cs @@ -0,0 +1,10 @@ +namespace PracticeCalendar.Application.PracticeEvents.Queries +{ + public class AttendeeDto + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string EmailAddress { get; set; } = string.Empty; + public bool IsAttending { get; set; } + } +} diff --git a/PracticeCalendar.Application/PracticeEvents/Queries/GetPracticeEvents/GetPracticeEventsQuery.cs b/PracticeCalendar.Application/PracticeEvents/Queries/GetPracticeEvents/GetPracticeEventsQuery.cs index 21d45c0..f062e6b 100644 --- a/PracticeCalendar.Application/PracticeEvents/Queries/GetPracticeEvents/GetPracticeEventsQuery.cs +++ b/PracticeCalendar.Application/PracticeEvents/Queries/GetPracticeEvents/GetPracticeEventsQuery.cs @@ -1,4 +1,6 @@ -using MediatR; +using Mapster; +using MapsterMapper; +using MediatR; using Microsoft.Extensions.Logging; using PracticeCalendar.Domain.Common.Interfaces; using PracticeCalendar.Domain.Entities; @@ -6,27 +8,31 @@ using PracticeCalendar.Domain.Entities.Specifications; namespace PracticeCalendar.Application.PracticeEvents.Queries.GetPracticeEvents { - public record class GetPracticeEventsQuery : IRequest> + public record class GetPracticeEventsQuery : IRequest> { } - public class GetPracticeEventsQueryHandler : IRequestHandler> + public class GetPracticeEventsQueryHandler : IRequestHandler> { private readonly ILogger logger; private readonly IRepository eventsRepo; + private readonly IMapper mapper; - public GetPracticeEventsQueryHandler(IRepository eventsRepo, - ILogger logger) + public GetPracticeEventsQueryHandler(IRepository eventsRepo, + ILogger logger, + IMapper mapper) { this.eventsRepo = eventsRepo; this.logger = logger; + this.mapper = mapper; } - public async Task> Handle(GetPracticeEventsQuery request, CancellationToken cancellationToken) + public async Task> Handle(GetPracticeEventsQuery request, CancellationToken cancellationToken) { var spec = new PracticeEventsWithAttendees(); - var evList = await eventsRepo.ListAsync(spec); - return evList; + var evList = await eventsRepo.ListAsync(spec, cancellationToken); + var lst = evList.Adapt>(mapper.Config); + return lst; } } } diff --git a/PracticeCalendar.Application/PracticeEvents/Queries/PracticeEventDto.cs b/PracticeCalendar.Application/PracticeEvents/Queries/PracticeEventDto.cs new file mode 100644 index 0000000..434b63e --- /dev/null +++ b/PracticeCalendar.Application/PracticeEvents/Queries/PracticeEventDto.cs @@ -0,0 +1,14 @@ +namespace PracticeCalendar.Application.PracticeEvents.Queries +{ + public class PracticeEventDto + { + public int Id { get; set; } + public string Title { get; set; } = string.Empty; + public string Description { get; set; } = string.Empty; + + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + + public IList Attendees { get; set; } = new List(); + } +} diff --git a/PracticeCalendar.Domain/Entities/PracticeEvent.cs b/PracticeCalendar.Domain/Entities/PracticeEvent.cs index 5da71ad..80c3b3d 100644 --- a/PracticeCalendar.Domain/Entities/PracticeEvent.cs +++ b/PracticeCalendar.Domain/Entities/PracticeEvent.cs @@ -22,8 +22,7 @@ namespace PracticeCalendar.Domain.Entities public string Title { get; private set; } = string.Empty; public string Description{ get; private set; } = string.Empty; - private List attendees = new List(); - public IEnumerable Attendees => attendees.AsReadOnly(); + public IList Attendees { get; private set; } = new List(); public DateTime StartTime { get; private set; } public DateTime EndTime { get; private set; } @@ -32,7 +31,7 @@ namespace PracticeCalendar.Domain.Entities { Guard.Against.Null(attendee, nameof(attendee)); attendee.AssignToEvent(this.Id); - attendees.Add(attendee); + Attendees.Add(attendee); var attendeeAddedEvent = new AttendeeAddedEvent(this, attendee); base.RegisterDomainEvent(attendeeAddedEvent); diff --git a/PracticeCalendar.Domain/Entities/Specifications/PracticeEventsWithAttendees.cs b/PracticeCalendar.Domain/Entities/Specifications/PracticeEventsWithAttendees.cs index 11ded62..8490ab5 100644 --- a/PracticeCalendar.Domain/Entities/Specifications/PracticeEventsWithAttendees.cs +++ b/PracticeCalendar.Domain/Entities/Specifications/PracticeEventsWithAttendees.cs @@ -6,7 +6,8 @@ namespace PracticeCalendar.Domain.Entities.Specifications { public PracticeEventsWithAttendees() { - Query.Include(x => x.Attendees); + Query.AsNoTracking() + .Include(x => x.Attendees); } } } diff --git a/PracticeCalendar.Domain/Exceptions/PracticeEventNotFound.cs b/PracticeCalendar.Domain/Exceptions/PracticeEventNotFound.cs new file mode 100644 index 0000000..323d961 --- /dev/null +++ b/PracticeCalendar.Domain/Exceptions/PracticeEventNotFound.cs @@ -0,0 +1,8 @@ +using PracticeCalendar.Domain.Common; + +namespace PracticeCalendar.Domain.Exceptions +{ + public class PracticeEventNotFoundException : DomainException + { + } +} diff --git a/PracticeCalendar.Infrastructure/InfrastructureDI.cs b/PracticeCalendar.Infrastructure/ConfigureServices.cs similarity index 83% rename from PracticeCalendar.Infrastructure/InfrastructureDI.cs rename to PracticeCalendar.Infrastructure/ConfigureServices.cs index 15cf5f3..5a67acc 100644 --- a/PracticeCalendar.Infrastructure/InfrastructureDI.cs +++ b/PracticeCalendar.Infrastructure/ConfigureServices.cs @@ -5,13 +5,13 @@ using PracticeCalendar.Domain.Common.Interfaces; using PracticeCalendar.Domain.Interfaces; using PracticeCalendar.Infrastructure.Notification; using PracticeCalendar.Infrastructure.Persistence; -using System; namespace PracticeCalendar.Infrastructure { - public static class InfrastructureDI + public static class ConfigureServices { - public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfigurationRoot configuration) + public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, + IConfigurationRoot configuration) { string connectionString = configuration.GetConnectionString("SqliteConnection"); diff --git a/PracticeCalendar.UnitTests/PracticeEventTest.cs b/PracticeCalendar.UnitTests/Domain/PracticeEventTest.cs similarity index 72% rename from PracticeCalendar.UnitTests/PracticeEventTest.cs rename to PracticeCalendar.UnitTests/Domain/PracticeEventTest.cs index 5c064de..15bc8a3 100644 --- a/PracticeCalendar.UnitTests/PracticeEventTest.cs +++ b/PracticeCalendar.UnitTests/Domain/PracticeEventTest.cs @@ -1,15 +1,14 @@ using FluentAssertions; using PracticeCalendar.Domain.Entities; -namespace PracticeCalendar.UnitTests +namespace PracticeCalendar.UnitTests.Domain { public class PracticeEventTest { - string _eventTitle = "Event1"; - string _eventDescription = "Description"; - - string _attendeeName = "Claudiu Farcas"; - string _atendeeEmail = "claudiu.farcas@testingbee.com"; + readonly string _eventTitle = "Event1"; + readonly string _eventDescription = "Description"; + readonly string _attendeeName = "Claudiu Farcas"; + readonly string _atendeeEmail = "claudiu.farcas@testingbee.com"; [Fact] public void InitializeProperties() @@ -23,22 +22,26 @@ namespace PracticeCalendar.UnitTests [Fact] public void InitializeWithNullShouldThrowException() { - Action act = () => { - var practiceEvent = new PracticeEvent(null, _eventDescription); + Action act = () => + { + var practiceEvent = new PracticeEvent(null!, _eventDescription); }; act.Should().Throw(); - act = () => { - var practiceEvent = new PracticeEvent(_eventTitle, null); + act = () => + { + var practiceEvent = new PracticeEvent(_eventTitle, null!); }; act.Should().Throw(); - act = () => { + act = () => + { var practiceEvent = new PracticeEvent(string.Empty, _eventDescription); }; act.Should().Throw(); - act = () => { + act = () => + { var practiceEvent = new PracticeEvent(_eventTitle, string.Empty); }; act.Should().Throw(); diff --git a/PracticeCalendar.UnitTests/Integration/BaseTest.cs b/PracticeCalendar.UnitTests/Integration/BaseTest.cs new file mode 100644 index 0000000..2419a06 --- /dev/null +++ b/PracticeCalendar.UnitTests/Integration/BaseTest.cs @@ -0,0 +1,12 @@ +using static PracticeCalendar.UnitTests.Integration.Testing; + +namespace PracticeCalendar.UnitTests.Integration +{ + public class BaseTest + { + public BaseTest() + { + ResetState(); + } + } +} diff --git a/PracticeCalendar.UnitTests/Integration/CustomWebApplicationFactory.cs b/PracticeCalendar.UnitTests/Integration/CustomWebApplicationFactory.cs new file mode 100644 index 0000000..5130742 --- /dev/null +++ b/PracticeCalendar.UnitTests/Integration/CustomWebApplicationFactory.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using PracticeCalendar.Infrastructure.Persistence; + +namespace PracticeCalendar.UnitTests.Integration +{ + public class CustomWebApplicationFactory : WebApplicationFactory + { + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + builder.ConfigureAppConfiguration(configurationBuilder => + { + var integrationConfig = new ConfigurationBuilder() + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables() + .Build(); + + configurationBuilder.AddConfiguration(integrationConfig); + }); + + builder.ConfigureServices((builder, services) => + { + services.Remove>(); + services.AddDbContext(options => + options.UseInMemoryDatabase("InMemoryDbForTesting") + ); + }); + } + } +} diff --git a/PracticeCalendar.UnitTests/Integration/PracticeEvents/CreatePracticeEventsTest.cs b/PracticeCalendar.UnitTests/Integration/PracticeEvents/CreatePracticeEventsTest.cs new file mode 100644 index 0000000..d8aaf2d --- /dev/null +++ b/PracticeCalendar.UnitTests/Integration/PracticeEvents/CreatePracticeEventsTest.cs @@ -0,0 +1,43 @@ +using FluentAssertions; +using PracticeCalendar.Application.PracticeEvents.Commands; +using PracticeCalendar.Application.PracticeEvents.Queries; + +using static PracticeCalendar.UnitTests.Integration.Testing; + +namespace PracticeCalendar.UnitTests.Integration.PracticeEvents +{ + public class CreatePracticeEventsTest : BaseTest + { + [Fact] + public async Task ShouldCreatePracticeEvent() + { + await RunBeforeAnyTests(); + + var query = new CreatePracticeEventCommand(new PracticeEventDto + { + Title = "Some title", + Description = "Some desc", + StartTime = DateTime.Now, + EndTime = DateTime.Now, + Attendees = { + new AttendeeDto + { + Name = "Claudiu F", + EmailAddress = "claudiuf@somewhere.com" + }, + new AttendeeDto + { + Name = "Claudiu F 2", + EmailAddress = "claudiuf2@somewhere.com" + } + } + }); + + var result = await SendAsync(query); + + result.Should().NotBeNull(); + result.Id.Should().NotBe(0); + result.Attendees.Count.Should().Be(2); + } + } +} diff --git a/PracticeCalendar.UnitTests/Integration/PracticeEvents/GetPracticeEventsTest.cs b/PracticeCalendar.UnitTests/Integration/PracticeEvents/GetPracticeEventsTest.cs new file mode 100644 index 0000000..586719a --- /dev/null +++ b/PracticeCalendar.UnitTests/Integration/PracticeEvents/GetPracticeEventsTest.cs @@ -0,0 +1,48 @@ +using FluentAssertions; +using PracticeCalendar.Application.PracticeEvents.Queries.GetPracticeEvents; +using PracticeCalendar.Domain.Entities; + +using static PracticeCalendar.UnitTests.Integration.Testing; + +namespace PracticeCalendar.UnitTests.Integration.PracticeEvents +{ + public class GetPracticeEventsTest : BaseTest + { + + [Fact] + public async Task ShouldReturnZeroResult() + { + await RunBeforeAnyTests(); + + var query = new GetPracticeEventsQuery(); + + var result = await SendAsync(query); + + result.Count.Should().Be(0); + } + + [Fact] + public async Task ShouldReturnAllListsAndItems() + { + await RunBeforeAnyTests(); + + await AddAsync(new PracticeEvent("Test Event", "Event description") + { + Id = 1, + Attendees = { + new Attendee("Claudiu F", "claudiuf@busybee.com") + { + Id = 1 + } + } + }); + + var query = new GetPracticeEventsQuery(); + + var result = await SendAsync(query); + + result.Should().HaveCount(1); + result.First().Attendees.Should().HaveCount(1); + } + } +} diff --git a/PracticeCalendar.UnitTests/Integration/ServiceCollectionExtensions.cs b/PracticeCalendar.UnitTests/Integration/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..aa4848c --- /dev/null +++ b/PracticeCalendar.UnitTests/Integration/ServiceCollectionExtensions.cs @@ -0,0 +1,20 @@ +using Microsoft.Extensions.DependencyInjection; + +namespace PracticeCalendar.UnitTests.Integration +{ + public static class ServiceCollectionExtensions + { + public static IServiceCollection Remove(this IServiceCollection services) + { + var serviceDescriptor = services.FirstOrDefault(d => + d.ServiceType == typeof(TService)); + + if (serviceDescriptor != null) + { + services.Remove(serviceDescriptor); + } + + return services; + } + } +} diff --git a/PracticeCalendar.UnitTests/Integration/Testing.cs b/PracticeCalendar.UnitTests/Integration/Testing.cs new file mode 100644 index 0000000..11c41c3 --- /dev/null +++ b/PracticeCalendar.UnitTests/Integration/Testing.cs @@ -0,0 +1,76 @@ +using Microsoft.Extensions.Configuration; +using MediatR; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; +using PracticeCalendar.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; +using PracticeCalendar.Domain.Entities; + +namespace PracticeCalendar.UnitTests.Integration +{ + public partial class Testing + { + private static WebApplicationFactory _factory = null!; + private static IConfiguration _configuration = null!; + private static IServiceScopeFactory _scopeFactory = null!; + + public static async Task RunBeforeAnyTests() + { + _factory = new CustomWebApplicationFactory(); + _scopeFactory = _factory.Services.GetRequiredService(); + _configuration = _factory.Services.GetRequiredService(); + } + + public static async Task SendAsync(IRequest request) + { + using var scope = _scopeFactory.CreateScope(); + + var mediator = scope.ServiceProvider.GetRequiredService(); + + return await mediator.Send(request); + } + + public static async Task FindAsync(params object[] keyValues) + where TEntity : class + { + using var scope = _scopeFactory.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + + return await context.FindAsync(keyValues); + } + + public static async Task AddAsync(TEntity entity) + where TEntity : class + { + using var scope = _scopeFactory.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + + context.Add(entity); + + await context.SaveChangesAsync(); + } + + public static async Task CountAsync() where TEntity : class + { + using var scope = _scopeFactory.CreateScope(); + + var context = scope.ServiceProvider.GetRequiredService(); + + return await context.Set().CountAsync(); + } + + public static void ResetState() + { + if (_scopeFactory != null) + { + using var scope = _scopeFactory.CreateScope(); + var context = scope.ServiceProvider.GetRequiredService(); + context.Set().RemoveRange(context.Set().ToList()); + context.Set().RemoveRange(context.Set().ToList()); + context.SaveChanges(); + } + } + } +} diff --git a/PracticeCalendar.UnitTests/PracticeCalendar.UnitTests.csproj b/PracticeCalendar.UnitTests/PracticeCalendar.UnitTests.csproj index ff139c1..00e6796 100644 --- a/PracticeCalendar.UnitTests/PracticeCalendar.UnitTests.csproj +++ b/PracticeCalendar.UnitTests/PracticeCalendar.UnitTests.csproj @@ -8,9 +8,24 @@ false + + + + + + + PreserveNewest + true + PreserveNewest + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -23,7 +38,17 @@ - + + + + + + + + + + PreserveNewest + diff --git a/PracticeCalendar.UnitTests/appsettings.json b/PracticeCalendar.UnitTests/appsettings.json new file mode 100644 index 0000000..4ba8810 --- /dev/null +++ b/PracticeCalendar.UnitTests/appsettings.json @@ -0,0 +1,13 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + }, + "UseInMemoryDatabase": true, //integration tests always with real database + "ConnectionStrings": { + "TestSqliteConnection": "Data Source=practicecalendar_test.sqlite" + } +} diff --git a/PracticeCalendar.UnitTests/xunit.runner.json b/PracticeCalendar.UnitTests/xunit.runner.json new file mode 100644 index 0000000..3ad9c00 --- /dev/null +++ b/PracticeCalendar.UnitTests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "parallelizeAssembly": false, + "parallelizeTestCollections": false +} \ No newline at end of file From ec54d2c2554e537eab50f65dc1e4868a5288325d Mon Sep 17 00:00:00 2001 From: farcasclaudiu Date: Mon, 10 Oct 2022 04:02:46 +0300 Subject: [PATCH 3/3] refactoring and integration tests --- PracticeCalendar.Api/Program.cs | 2 - .../appsettings.Development.json | 2 +- PracticeCalendar.Api/appsettings.json | 2 +- PracticeCalendar.Api/practicecalendar.sqlite | Bin 20480 -> 20480 bytes .../Events/AttendeeAddedEventNotification.cs | 24 ++++++++++++ .../Common/DomainEventBase.cs | 1 + .../Common/DomainEventNotification.cs | 14 +++++++ .../Common/Interfaces/IDomainEventService.cs | 7 ++++ .../Entities/PracticeEvent.cs | 9 +++++ .../Events/AttendeeAcceptEvent.cs | 17 ++++++++ .../Events/AttendeeDeclinedEvent.cs | 17 ++++++++ .../EventUpdateTitleAndDescriptionEvent.cs | 15 +++++++ .../ConfigureServices.cs | 2 + .../Persistence/ApplicationDbContext.cs | 37 ++++++++++++++++-- .../Services/DomainEventService.cs | 30 ++++++++++++++ 15 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 PracticeCalendar.Application/PracticeEvents/Events/AttendeeAddedEventNotification.cs create mode 100644 PracticeCalendar.Domain/Common/DomainEventNotification.cs create mode 100644 PracticeCalendar.Domain/Common/Interfaces/IDomainEventService.cs create mode 100644 PracticeCalendar.Domain/Events/AttendeeAcceptEvent.cs create mode 100644 PracticeCalendar.Domain/Events/AttendeeDeclinedEvent.cs create mode 100644 PracticeCalendar.Domain/Events/EventUpdateTitleAndDescriptionEvent.cs create mode 100644 PracticeCalendar.Infrastructure/Services/DomainEventService.cs diff --git a/PracticeCalendar.Api/Program.cs b/PracticeCalendar.Api/Program.cs index 9c4d85c..fae36e4 100644 --- a/PracticeCalendar.Api/Program.cs +++ b/PracticeCalendar.Api/Program.cs @@ -27,8 +27,6 @@ namespace PracticeCalendar //inject infrastructure builder.Services.AddInfrastructureServices(builder.Configuration); - //mapster - var app = builder.Build(); diff --git a/PracticeCalendar.Api/appsettings.Development.json b/PracticeCalendar.Api/appsettings.Development.json index 0c208ae..a6e86ac 100644 --- a/PracticeCalendar.Api/appsettings.Development.json +++ b/PracticeCalendar.Api/appsettings.Development.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.AspNetCore": "Warning" } } diff --git a/PracticeCalendar.Api/appsettings.json b/PracticeCalendar.Api/appsettings.json index 83a9a57..9464942 100644 --- a/PracticeCalendar.Api/appsettings.json +++ b/PracticeCalendar.Api/appsettings.json @@ -1,7 +1,7 @@ { "Logging": { "LogLevel": { - "Default": "Information", + "Default": "Debug", "Microsoft.AspNetCore": "Warning" } }, diff --git a/PracticeCalendar.Api/practicecalendar.sqlite b/PracticeCalendar.Api/practicecalendar.sqlite index 5e6e9144cf8b9e9f0d453bfb00c7976d597c02ae..951caee88851776e155a46fbdb974f305d5aeaad 100644 GIT binary patch delta 410 zcmY+8Ci&B!i_el*I(vm-nvO=FK)gad`GLx5~3O+vmtj(Py~D zG1fu!L@nAPTfWN+xhD&vA50m~Vq^)vF|o`kQ6`E+JyDAd-27Q)#Te(QS-x4{DSOp& zWo^Gw^BRzhF;K|xi5J}C5 zj}}GIane%7#lf)TP%f%mw1mn+?F(uYEPW^!a)WMaQIDRXt6k~4rR!;+!|;o5eBlE< qyx|pHJmLX&2oc~4o#-P+m>W%T4kMNjD=AUN3X!PC>YCr(nEwNX0BB$U delta 142 zcmV;90CE3-paFoO0gxL36_Ff60Tr=eqz@Db4jBLsybpp8NDmGU&JK+ZQw|xk5g-8$ z0yqkj;T<`%R34WREeR3<11SMea$#e1X=7zYc4cmKa|#Ur000ONQ~(d_57rOC52_E7 w4}K464^*=eAUqF~R6np32oKQ!5Bm@G5AP4@591Hr57!UTvk@S<50lYPFNDS^?*IS* diff --git a/PracticeCalendar.Application/PracticeEvents/Events/AttendeeAddedEventNotification.cs b/PracticeCalendar.Application/PracticeEvents/Events/AttendeeAddedEventNotification.cs new file mode 100644 index 0000000..e736f4e --- /dev/null +++ b/PracticeCalendar.Application/PracticeEvents/Events/AttendeeAddedEventNotification.cs @@ -0,0 +1,24 @@ +using MediatR; +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Events; +using PracticeCalendar.Domain.Interfaces; + +namespace PracticeCalendar.Application.PracticeEvents.Events +{ + public class AttendeeAddedEventNotification : INotificationHandler> + { + private readonly IEmailSender emailSender; + + public AttendeeAddedEventNotification(IEmailSender emailSender) + { + this.emailSender = emailSender; + } + + public async Task Handle(DomainEventNotification notification, CancellationToken cancellationToken) + { + var sendTo = notification.DomainEvent.AddedAtendee.EmailAddress; + await emailSender.SendEmailAsync(sendTo, "system", + "You have been added to the event", "Confirmed you have been added to the event."); + } + } +} diff --git a/PracticeCalendar.Domain/Common/DomainEventBase.cs b/PracticeCalendar.Domain/Common/DomainEventBase.cs index 3ee56bc..a6a6a07 100644 --- a/PracticeCalendar.Domain/Common/DomainEventBase.cs +++ b/PracticeCalendar.Domain/Common/DomainEventBase.cs @@ -3,5 +3,6 @@ public class DomainEventBase { public DateTime EventDate { get; protected set; } = DateTime.UtcNow; + public bool IsPublished { get; set; } } } diff --git a/PracticeCalendar.Domain/Common/DomainEventNotification.cs b/PracticeCalendar.Domain/Common/DomainEventNotification.cs new file mode 100644 index 0000000..1281805 --- /dev/null +++ b/PracticeCalendar.Domain/Common/DomainEventNotification.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace PracticeCalendar.Domain.Common +{ + public class DomainEventNotification : INotification where TDomainEvent : DomainEventBase + { + public TDomainEvent DomainEvent { get; } + + public DomainEventNotification(TDomainEvent domainEvent) + { + DomainEvent = domainEvent; + } + } +} diff --git a/PracticeCalendar.Domain/Common/Interfaces/IDomainEventService.cs b/PracticeCalendar.Domain/Common/Interfaces/IDomainEventService.cs new file mode 100644 index 0000000..e7884fc --- /dev/null +++ b/PracticeCalendar.Domain/Common/Interfaces/IDomainEventService.cs @@ -0,0 +1,7 @@ +namespace PracticeCalendar.Domain.Common.Interfaces +{ + public interface IDomainEventService + { + Task Publish(DomainEventBase domainEvent); + } +} diff --git a/PracticeCalendar.Domain/Entities/PracticeEvent.cs b/PracticeCalendar.Domain/Entities/PracticeEvent.cs index 80c3b3d..4f823dd 100644 --- a/PracticeCalendar.Domain/Entities/PracticeEvent.cs +++ b/PracticeCalendar.Domain/Entities/PracticeEvent.cs @@ -41,6 +41,9 @@ namespace PracticeCalendar.Domain.Entities { this.Title = title; this.Description = description; + + var titleDescUpdatedEvent = new EventUpdateTitleAndDescriptionEvent(this); + base.RegisterDomainEvent(titleDescUpdatedEvent); } public void AttendeeAcceptEvent(int attendeeId) @@ -49,6 +52,9 @@ namespace PracticeCalendar.Domain.Entities if (attendee == null) throw new InvalidAttendeeException(attendeeId); attendee.SetIsAttending(true); + + var attendeeAcceptedEvent = new AttendeeAcceptedEvent(this, attendee); + base.RegisterDomainEvent(attendeeAcceptedEvent); } public void AttendeeDeclineEvent(int attendeeId) @@ -57,6 +63,9 @@ namespace PracticeCalendar.Domain.Entities if (attendee == null) throw new InvalidAttendeeException(attendeeId); attendee.SetIsAttending(false); + + var attendeeDeclinedEvent = new AttendeeDeclinedEvent(this, attendee); + base.RegisterDomainEvent(attendeeDeclinedEvent); } } } diff --git a/PracticeCalendar.Domain/Events/AttendeeAcceptEvent.cs b/PracticeCalendar.Domain/Events/AttendeeAcceptEvent.cs new file mode 100644 index 0000000..a367c8d --- /dev/null +++ b/PracticeCalendar.Domain/Events/AttendeeAcceptEvent.cs @@ -0,0 +1,17 @@ +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Entities; + +namespace PracticeCalendar.Domain.Events +{ + public sealed class AttendeeAcceptedEvent : DomainEventBase + { + public AttendeeAcceptedEvent(PracticeEvent practiceEvent, Attendee attendee) + { + PracticeEvent = practiceEvent; + Attendee = attendee; + } + + public PracticeEvent PracticeEvent { get; } + public Attendee Attendee { get; } + } +} diff --git a/PracticeCalendar.Domain/Events/AttendeeDeclinedEvent.cs b/PracticeCalendar.Domain/Events/AttendeeDeclinedEvent.cs new file mode 100644 index 0000000..d66cec2 --- /dev/null +++ b/PracticeCalendar.Domain/Events/AttendeeDeclinedEvent.cs @@ -0,0 +1,17 @@ +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Entities; + +namespace PracticeCalendar.Domain.Events +{ + public sealed class AttendeeDeclinedEvent : DomainEventBase + { + public AttendeeDeclinedEvent(PracticeEvent practiceEvent, Attendee attendee) + { + PracticeEvent = practiceEvent; + Attendee = attendee; + } + + public PracticeEvent PracticeEvent { get; } + public Attendee Attendee { get; } + } +} diff --git a/PracticeCalendar.Domain/Events/EventUpdateTitleAndDescriptionEvent.cs b/PracticeCalendar.Domain/Events/EventUpdateTitleAndDescriptionEvent.cs new file mode 100644 index 0000000..c7339c7 --- /dev/null +++ b/PracticeCalendar.Domain/Events/EventUpdateTitleAndDescriptionEvent.cs @@ -0,0 +1,15 @@ +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Entities; + +namespace PracticeCalendar.Domain.Events +{ + public sealed class EventUpdateTitleAndDescriptionEvent : DomainEventBase + { + public EventUpdateTitleAndDescriptionEvent(PracticeEvent practiceEvent) + { + PracticeEvent = practiceEvent; + } + + public PracticeEvent PracticeEvent { get; } + } +} diff --git a/PracticeCalendar.Infrastructure/ConfigureServices.cs b/PracticeCalendar.Infrastructure/ConfigureServices.cs index 5a67acc..a01c34a 100644 --- a/PracticeCalendar.Infrastructure/ConfigureServices.cs +++ b/PracticeCalendar.Infrastructure/ConfigureServices.cs @@ -5,6 +5,7 @@ using PracticeCalendar.Domain.Common.Interfaces; using PracticeCalendar.Domain.Interfaces; using PracticeCalendar.Infrastructure.Notification; using PracticeCalendar.Infrastructure.Persistence; +using PracticeCalendar.Infrastructure.Services; namespace PracticeCalendar.Infrastructure { @@ -17,6 +18,7 @@ namespace PracticeCalendar.Infrastructure services.AddDbContext(connectionString); services.AddTransient(); + services.AddTransient(); services.AddScoped(typeof(IRepository<>), typeof(EfRepository<>)); diff --git a/PracticeCalendar.Infrastructure/Persistence/ApplicationDbContext.cs b/PracticeCalendar.Infrastructure/Persistence/ApplicationDbContext.cs index dca7d7f..8962c25 100644 --- a/PracticeCalendar.Infrastructure/Persistence/ApplicationDbContext.cs +++ b/PracticeCalendar.Infrastructure/Persistence/ApplicationDbContext.cs @@ -1,4 +1,6 @@ using Microsoft.EntityFrameworkCore; +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Common.Interfaces; using PracticeCalendar.Domain.Entities; using System.Reflection; @@ -6,9 +8,15 @@ namespace PracticeCalendar.Infrastructure.Persistence { public class ApplicationDbContext : DbContext { - public ApplicationDbContext(DbContextOptions options) + private readonly IDomainEventService domainEventService; + + public DbSet Atendees => Set(); + public DbSet PracticeEvents => Set(); + + public ApplicationDbContext(DbContextOptions options, IDomainEventService domainEventService) : base(options) { + this.domainEventService = domainEventService; } protected override void OnModelCreating(ModelBuilder modelBuilder) @@ -17,7 +25,30 @@ namespace PracticeCalendar.Infrastructure.Persistence modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); } - public DbSet Atendees => Set(); - public DbSet PracticeEvents => Set(); + + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = new CancellationToken()) + { + var events = ChangeTracker.Entries() + .Select(x => x.Entity.DomainEvents) + .SelectMany(x => x) + .Where(domainEvent => !domainEvent.IsPublished) + .ToArray(); + + var result = await base.SaveChangesAsync(cancellationToken); + + await DispatchEvents(events); + + return result; + } + + private async Task DispatchEvents(DomainEventBase[] events) + { + foreach (var @event in events) + { + @event.IsPublished = true; + await domainEventService.Publish(@event); + } + } } } diff --git a/PracticeCalendar.Infrastructure/Services/DomainEventService.cs b/PracticeCalendar.Infrastructure/Services/DomainEventService.cs new file mode 100644 index 0000000..126f000 --- /dev/null +++ b/PracticeCalendar.Infrastructure/Services/DomainEventService.cs @@ -0,0 +1,30 @@ +using MediatR; +using Microsoft.Extensions.Logging; +using PracticeCalendar.Domain.Common; +using PracticeCalendar.Domain.Common.Interfaces; + +namespace PracticeCalendar.Infrastructure.Services +{ + public class DomainEventService : IDomainEventService + { + private readonly ILogger logger; + private readonly IPublisher mediator; + + public DomainEventService(ILogger logger, IPublisher mediator) + { + this.logger = logger; + this.mediator = mediator; + } + public async Task Publish(DomainEventBase domainEvent) + { + logger.LogInformation("Publishing domain event. Event - {event}", domainEvent.GetType().Name); + await mediator.Publish(GetNotificationCorrespondingToDomainEvent(domainEvent)); + } + + private INotification GetNotificationCorrespondingToDomainEvent(DomainEventBase domainEvent) + { + return (INotification)Activator.CreateInstance( + typeof(DomainEventNotification<>).MakeGenericType(domainEvent.GetType()), domainEvent)!; + } + } +}