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 b7646ed..5e6e914 100644 Binary files a/PracticeCalendar.Api/practicecalendar.sqlite and b/PracticeCalendar.Api/practicecalendar.sqlite differ 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