diff --git a/EventFlow-API.Tests/Controllers/EventControllerTests.cs b/EventFlow-API.Tests/Controllers/EventControllerTests.cs index 45d1187..7dc936d 100644 --- a/EventFlow-API.Tests/Controllers/EventControllerTests.cs +++ b/EventFlow-API.Tests/Controllers/EventControllerTests.cs @@ -1,16 +1,23 @@ -using Microsoft.AspNetCore.Http; +using MediatR; +using Microsoft.AspNetCore.Http; +using EventFlow.Application.Features.Events.Commands.CreateEvent; +using EventFlow.Application.Features.Events.Commands.DeleteEvent; +using EventFlow.Application.Features.Events.Commands.UpdateEvent; +using EventFlow.Application.Features.Events.Queries.GetAllEvents; +using EventFlow.Application.Features.Events.Queries.GetEventById; +using EventFlow.Core.Primitives; namespace EventFlow_API.Tests.Controllers; public class EventControllerTests { - private readonly Mock _serviceMock; private readonly EventController _controller; + private readonly Mock _mockMediator; public EventControllerTests() { - _serviceMock = new Mock(); - _controller = new EventController(_serviceMock.Object); + _mockMediator = new Mock(); + _controller = new EventController(_mockMediator.Object); _controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() @@ -18,106 +25,114 @@ public EventControllerTests() } [Fact] - public async Task PostAsync_ReturnsOk_WhenEventCreated() + public async Task Create_ReturnsCreatedAtAction_WhenEventCreated() { - var command = new EventCommand { Title = "Test", Description = "Test Desc", Date = DateTime.Now, Location = "Test Location", OrganizerId = 1 }; - var createdEvent = new Event { Id = 1, Title = "Test" }; + var command = new CreateEventCommand("Test Event", "Description", DateTime.Now.AddDays(1), "Location", 1); + var result = Result.Success(1); - _serviceMock.Setup(s => s.CreateAsync(command)).ReturnsAsync(createdEvent); + _mockMediator.Setup(m => m.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.PostAsync(command); + var actionResult = await _controller.Create(command, CancellationToken.None); - var okResult = Assert.IsType(result); - var returnedEvent = Assert.IsType(okResult.Value); - Assert.Equal(1, returnedEvent.Id); + var createdResult = Assert.IsType(actionResult); + Assert.Equal(1, createdResult.RouteValues!["id"]); } [Fact] - public async Task PostAsync_ReturnsBadRequest_WhenCreationFails() + public async Task Create_ReturnsError_WhenCreationFails() { - var command = new EventCommand { Title = "Test", Description = "Test Desc", Date = DateTime.Now, Location = "Test Location", OrganizerId = 1 }; - _serviceMock.Setup(s => s.CreateAsync(command)).ReturnsAsync((Event)null!); + var command = new CreateEventCommand("Test Event", "Description", DateTime.Now.AddDays(1), "Location", 1); + var result = Result.Failure(Error.Validation("Event.CreateFailed", "Failed to create event")); - var result = await _controller.PostAsync(command); + _mockMediator.Setup(m => m.Send(command, It.IsAny())).ReturnsAsync(result); - Assert.IsType(result); + var actionResult = await _controller.Create(command, CancellationToken.None); + + Assert.IsType(actionResult); } [Fact] - public async Task UpdateAsync_ReturnsOk_WhenUpdated() + public async Task Update_ReturnsOk_WhenUpdated() { - var command = new EventCommand { Title = "Updated", Description = "Desc", Date = DateTime.Now, Location = "Loc", OrganizerId = 1 }; - var updatedEvent = new EventDTO { Id = 1, Title = "Updated" }; + var command = new UpdateEventCommand(1, "Updated Event", "Description", DateTime.Now.AddDays(1), "Location"); + var result = Result.Success(); - _serviceMock.Setup(s => s.UpdateAsync(1, command)).ReturnsAsync(updatedEvent); + _mockMediator.Setup(m => m.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.UpdateAsync(1, command); + var actionResult = await _controller.Update(1, command, CancellationToken.None); - var okResult = Assert.IsType(result); - var returnedEvent = Assert.IsType(okResult.Value); - Assert.Equal(1, returnedEvent.Id); + Assert.IsType(actionResult); } [Fact] - public async Task UpdateAsync_ReturnsNotFound_WhenUpdateFails() + public async Task Update_ReturnsNotFound_WhenEventNotFound() { - var command = new EventCommand { Title = "Updated" }; - _serviceMock.Setup(s => s.UpdateAsync(1, command)).ReturnsAsync((EventDTO)null!); + var command = new UpdateEventCommand(1, "Updated Event", "Description", DateTime.Now.AddDays(1), "Location"); + var result = Result.Failure(Error.NotFound("Event.NotFound", "Event not found")); + + _mockMediator.Setup(m => m.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.UpdateAsync(1, command); + var actionResult = await _controller.Update(1, command, CancellationToken.None); - Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] - public async Task DeleteAsync_ReturnsOk_WhenDeleted() + public async Task Delete_ReturnsOk_WhenDeleted() { - _serviceMock.Setup(s => s.DeleteAsync(1)).ReturnsAsync(true); + var command = new DeleteEventCommand(1); + var result = Result.Success(); + + _mockMediator.Setup(m => m.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.DeleteAsync(1); + var actionResult = await _controller.Delete(1, CancellationToken.None); - var okResult = Assert.IsType(result); - Assert.Equal(1, okResult.Value); + Assert.IsType(actionResult); } [Fact] - public async Task DeleteAsync_ReturnsNotFound_WhenDeleteFails() + public async Task Delete_ReturnsNotFound_WhenDeleteFails() { - _serviceMock.Setup(s => s.DeleteAsync(1)).ReturnsAsync(false); + var command = new DeleteEventCommand(1); + var result = Result.Failure(Error.NotFound("Event.NotFound", "Event not found")); - var result = await _controller.DeleteAsync(1); + _mockMediator.Setup(m => m.Send(command, It.IsAny())).ReturnsAsync(result); - Assert.IsType(result); + var actionResult = await _controller.Delete(1, CancellationToken.None); + + Assert.IsType(actionResult); } [Fact] public async Task GetById_ReturnsOk_WhenEventExists() { - var eventId = 1; - var eventDto = new EventDTO { Id = eventId, Title = "Test Event" }; - _serviceMock.Setup(s => s.GetByIdAsync(eventId)).ReturnsAsync(eventDto); + var query = new GetEventByIdQuery(1); + var evento = new EventDTO { Id = 1, Title = "Event 1" }; + var result = Result.Success(evento); + + _mockMediator.Setup(m => m.Send(query, It.IsAny())).ReturnsAsync(result); - var result = await _controller.GetEventByIdAsync(eventId); + var actionResult = await _controller.GetById(1, CancellationToken.None); - var okResult = Assert.IsType(result); + var okResult = Assert.IsType(actionResult); var returnValue = Assert.IsType(okResult.Value); - Assert.Equal(eventDto.Id, returnValue.Id); + returnValue.Id.Should().Be(1); } [Fact] - public async Task GetAllEventsAsync_ReturnsNotFound_WhenNoEventsExist() + public async Task GetAll_ReturnsOk_WithList() { var queryParameters = new QueryParameters(); - var emptyList = new List(); - - var pagedResult = new PagedResult(emptyList, 1, 10, 0); + var query = new GetAllEventsQuery(queryParameters); + var events = new List { new() { Id = 1, Title = "Event 1" } }; + var pagedResult = new PagedResult(events, 1, 10, 1); - _serviceMock - .Setup(s => s.GetAllPagedEventsAsync(It.IsAny())) - .ReturnsAsync(pagedResult); + _mockMediator.Setup(m => m.Send(query, It.IsAny())).ReturnsAsync(pagedResult); - var result = await _controller.GetAllEventsAsync(queryParameters); + var actionResult = await _controller.GetAll(queryParameters, CancellationToken.None); - Assert.IsType(result); + var okResult = Assert.IsType(actionResult); + var returnValue = Assert.IsType>(okResult.Value); + returnValue.Should().HaveCount(1); } } diff --git a/EventFlow-API.Tests/Controllers/OrganizerControllerTests.cs b/EventFlow-API.Tests/Controllers/OrganizerControllerTests.cs index e70a038..6b6447d 100644 --- a/EventFlow-API.Tests/Controllers/OrganizerControllerTests.cs +++ b/EventFlow-API.Tests/Controllers/OrganizerControllerTests.cs @@ -1,16 +1,23 @@ -using Microsoft.AspNetCore.Http; +using MediatR; +using Microsoft.AspNetCore.Http; +using EventFlow.Application.Features.Organizers.Commands.CreateOrganizer; +using EventFlow.Application.Features.Organizers.Commands.DeleteOrganizer; +using EventFlow.Application.Features.Organizers.Commands.UpdateOrganizer; +using EventFlow.Application.Features.Organizers.Queries.GetAllOrganizers; +using EventFlow.Application.Features.Organizers.Queries.GetOrganizerById; +using EventFlow.Core.Primitives; namespace EventFlow_API.Tests.Controllers; public class OrganizerControllerTests { private readonly OrganizerController _controller; - private readonly Mock _mockService; + private readonly Mock _mockSender; public OrganizerControllerTests() { - _mockService = new Mock(); - _controller = new OrganizerController(_mockService.Object); + _mockSender = new Mock(); + _controller = new OrganizerController(_mockSender.Object); _controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() @@ -20,107 +27,94 @@ public OrganizerControllerTests() [Fact] public async Task PostAsync_ReturnsOk_WhenOrganizerCreated() { - var command = new OrganizerCommand { Name = "Test", Email = "test@example.com" }; - var organizer = new Organizer { Id = 1, Name = "Test", Email = "test@example.com" }; + var command = new CreateOrganizerCommand("Test", "test@example.com"); + var result = Result.Success(1); - _mockService.Setup(s => s.CreateAsync(command)).ReturnsAsync(organizer); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.PostAsync(command); + var actionResult = await _controller.PostAsync(command); - var okResult = Assert.IsType(result); - var returnValue = Assert.IsType(okResult.Value); - returnValue.Id.Should().Be(1); + var okResult = Assert.IsType(actionResult); + Assert.Equal(1, okResult.Value); } [Fact] public async Task PostAsync_ReturnsBadRequest_WhenCreationFails() { - var command = new OrganizerCommand { Name = "Test", Email = "test@example.com" }; + var command = new CreateOrganizerCommand("Test", "test@example.com"); + var result = Result.Failure(Error.Validation("Organizer.CreateFailed", "Failed to create organizer")); - _mockService.Setup(s => s.CreateAsync(command)).ReturnsAsync((Organizer)null!); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.PostAsync(command); + var actionResult = await _controller.PostAsync(command); - Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] public async Task UpdateAsync_ReturnsOk_WhenUpdated() { - var command = new OrganizerCommand { Name = "Updated", Email = "updated@example.com" }; - var updated = new OrganizerDTO { Id = 1, Name = "Updated" }; + var command = new UpdateOrganizerCommand(1, "Updated", "updated@example.com"); + var result = Result.Success(); - _mockService.Setup(s => s.UpdateAsync(1, command)).ReturnsAsync(updated); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.UpdateAsync(1, command); + var actionResult = await _controller.UpdateAsync(1, command); - var okResult = Assert.IsType(result); - var returnValue = Assert.IsType(okResult.Value); - returnValue.Id.Should().Be(1); + Assert.IsType(actionResult); } [Fact] - public async Task UpdateAsync_ReturnsNotFound_WhenUpdateFails() + public async Task UpdateAsync_ReturnsNotFound_WhenOrganizerNotFound() { - var command = new OrganizerCommand { Name = "Updated", Email = "updated@example.com" }; + var command = new UpdateOrganizerCommand(1, "Updated", "updated@example.com"); + var result = Result.Failure(Error.NotFound("Organizer.NotFound", "Organizer not found")); - _mockService.Setup(s => s.UpdateAsync(1, command)).ReturnsAsync((OrganizerDTO)null!); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.UpdateAsync(1, command); + var actionResult = await _controller.UpdateAsync(1, command); - Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] public async Task DeleteAsync_ReturnsOk_WhenDeleted() { - _mockService.Setup(s => s.DeleteAsync(1)).ReturnsAsync(true); - - var result = await _controller.DeleteAsync(1); + var command = new DeleteOrganizerCommand(1); + var result = Result.Success(); - var okResult = Assert.IsType(result); - Assert.Equal(1, okResult.Value); - } + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - [Fact] - public async Task DeleteAsync_ReturnsNotFound_WhenDeleteFails() - { - _mockService.Setup(s => s.DeleteAsync(1)).ReturnsAsync(false); + var actionResult = await _controller.DeleteAsync(1); - var result = await _controller.DeleteAsync(1); - - Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] - public async Task RegisterParticipantAsync_ReturnsOk_WhenSuccess() + public async Task DeleteAsync_ReturnsNotFound_WhenDeleteFails() { - _mockService.Setup(s => s.RegisterToEventAsync(1, 1)).ReturnsAsync(true); + var command = new DeleteOrganizerCommand(1); + var result = Result.Failure(Error.NotFound("Organizer.NotFound", "Organizer not found")); - var result = await _controller.RegisterParticipantAsync(1, 1); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var okResult = Assert.IsType(result); - } + var actionResult = await _controller.DeleteAsync(1); - [Fact] - public async Task RegisterParticipantAsync_ReturnsNotFound_WhenFailure() - { - _mockService.Setup(s => s.RegisterToEventAsync(1, 1)).ReturnsAsync(false); - - var result = await _controller.RegisterParticipantAsync(1, 1); - - var notFoundResult = Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] public async Task GetById_ReturnsOk_WhenOrganizerExists() { + var query = new GetOrganizerByIdQuery(1); var organizer = new OrganizerDTO { Id = 1, Name = "Organizer 1" }; - _mockService.Setup(s => s.GetByIdAsync(1)).ReturnsAsync(organizer); + var result = Result.Success(organizer); - var result = await _controller.GetOrganizerByIdAsync(1); + _mockSender.Setup(s => s.Send(query, It.IsAny())).ReturnsAsync(result); - var okResult = Assert.IsType(result); + var actionResult = await _controller.GetOrganizerByIdAsync(1); + + var okResult = Assert.IsType(actionResult); var returnValue = Assert.IsType(okResult.Value); returnValue.Id.Should().Be(1); } @@ -129,18 +123,16 @@ public async Task GetById_ReturnsOk_WhenOrganizerExists() public async Task GetAll_ReturnsOk_WithList() { var queryParameters = new QueryParameters(); + var query = new GetAllOrganizersQuery(queryParameters); var organizers = new List { new() { Id = 1, Name = "Organizer 1" } }; var pagedResult = new PagedResult(organizers, 1, 10, 1); - _mockService - .Setup(s => s.GetAllPagedOrganizersAsync(It.IsAny())) - .ReturnsAsync(pagedResult); + _mockSender.Setup(s => s.Send(query, It.IsAny())).ReturnsAsync(pagedResult); - var result = await _controller.GetAllOrganizersAsync(queryParameters); + var actionResult = await _controller.GetAllOrganizersAsync(queryParameters); - var okResult = Assert.IsType(result); + var okResult = Assert.IsType(actionResult); var returnValue = Assert.IsType>(okResult.Value); returnValue.Should().HaveCount(1); } - } diff --git a/EventFlow-API.Tests/Controllers/ParticipantControllerTests.cs b/EventFlow-API.Tests/Controllers/ParticipantControllerTests.cs index f3fc3d6..d628614 100644 --- a/EventFlow-API.Tests/Controllers/ParticipantControllerTests.cs +++ b/EventFlow-API.Tests/Controllers/ParticipantControllerTests.cs @@ -1,16 +1,23 @@ -using Microsoft.AspNetCore.Http; +using MediatR; +using Microsoft.AspNetCore.Http; +using EventFlow.Application.Features.Participants.Commands.CreateParticipant; +using EventFlow.Application.Features.Participants.Commands.DeleteParticipant; +using EventFlow.Application.Features.Participants.Commands.UpdateParticipant; +using EventFlow.Application.Features.Participants.Queries.GetParticipantById; +using EventFlow.Application.Features.Participants.Queries.GetParticipantsByEventId; +using EventFlow.Core.Primitives; namespace EventFlow_API.Tests.Controllers; public class ParticipantControllerTests { private readonly ParticipantController _controller; - private readonly Mock _mockService; + private readonly Mock _mockSender; public ParticipantControllerTests() { - _mockService = new Mock(); - _controller = new ParticipantController(_mockService.Object); + _mockSender = new Mock(); + _controller = new ParticipantController(_mockSender.Object); _controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() @@ -20,107 +27,95 @@ public ParticipantControllerTests() [Fact] public async Task PostAsync_ReturnsOk_WhenParticipantCreated() { - var command = new ParticipantCommand { Name = "Test", Email = "test@example.com" }; - var participant = new Participant { Id = 1, Name = "Test", Email = "test@example.com" }; + var command = new CreateParticipantCommand("Test", "test@example.com", "Interests"); + var result = Result.Success(1); - _mockService.Setup(s => s.CreateAsync(command)).ReturnsAsync(participant); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.PostAsync(command); + var actionResult = await _controller.PostAsync(command); - var okResult = Assert.IsType(result); - var returnValue = Assert.IsType(okResult.Value); - returnValue.Id.Should().Be(1); + var okResult = Assert.IsType(actionResult); + Assert.Equal(1, okResult.Value); } [Fact] - public async Task PostAsync_ReturnsBadRequest_WhenCreationFails() + public async Task PostAsync_ReturnsInternalServerError_WhenCreationFails() { - var command = new ParticipantCommand { Name = "Test", Email = "test@example.com" }; + var command = new CreateParticipantCommand("Test", "test@example.com", "Interests"); + var result = Result.Failure(Error.Failure("Participant.CreateFailed", "Failed to create participant")); - _mockService.Setup(s => s.CreateAsync(command)).ReturnsAsync((Participant)null!); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.PostAsync(command); + var actionResult = await _controller.PostAsync(command); - Assert.IsType(result); + var objectResult = Assert.IsType(actionResult); + Assert.Equal(500, objectResult.StatusCode); } [Fact] public async Task UpdateAsync_ReturnsOk_WhenUpdated() { - var command = new ParticipantCommand { Name = "Updated", Email = "updated@example.com" }; - var updated = new ParticipantDTO { Id = 1, Name = "Updated" }; + var command = new UpdateParticipantCommand(1, "Updated", "updated@example.com", "Interests"); + var result = Result.Success(); - _mockService.Setup(s => s.UpdateAsync(1, command)).ReturnsAsync(updated); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.UpdateAsync(1, command); + var actionResult = await _controller.UpdateAsync(1, command); - var okResult = Assert.IsType(result); - var returnValue = Assert.IsType(okResult.Value); - returnValue.Id.Should().Be(1); + Assert.IsType(actionResult); } [Fact] public async Task UpdateAsync_ReturnsNotFound_WhenUpdateFails() { - var command = new ParticipantCommand { Name = "Updated", Email = "updated@example.com" }; + var command = new UpdateParticipantCommand(1, "Updated", "updated@example.com", "Interests"); + var result = Result.Failure(Error.NotFound("Participant.NotFound", "Participant not found")); - _mockService.Setup(s => s.UpdateAsync(1, command)).ReturnsAsync((ParticipantDTO)null!); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.UpdateAsync(1, command); + var actionResult = await _controller.UpdateAsync(1, command); - Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] public async Task DeleteAsync_ReturnsOk_WhenDeleted() { - _mockService.Setup(s => s.DeleteAsync(1)).ReturnsAsync(true); - - var result = await _controller.DeleteAsync(1); + var command = new DeleteParticipantCommand(1); + var result = Result.Success(); - var okResult = Assert.IsType(result); - Assert.Equal(1, okResult.Value); - } + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - [Fact] - public async Task DeleteAsync_ReturnsNotFound_WhenDeleteFails() - { - _mockService.Setup(s => s.DeleteAsync(1)).ReturnsAsync(false); + var actionResult = await _controller.DeleteAsync(1); - var result = await _controller.DeleteAsync(1); - - Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] - public async Task RegisterParticipantAsync_ReturnsOk_WhenSuccess() + public async Task DeleteAsync_ReturnsNotFound_WhenDeleteFails() { - _mockService.Setup(s => s.RegisterToEventAsync(1, 1)).ReturnsAsync(true); + var command = new DeleteParticipantCommand(1); + var result = Result.Failure(Error.NotFound("Participant.NotFound", "Participant not found")); - var result = await _controller.RegisterParticipantAsync(1, 1); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var okResult = Assert.IsType(result); - } + var actionResult = await _controller.DeleteAsync(1); - [Fact] - public async Task RegisterParticipantAsync_ReturnsNotFound_WhenFailure() - { - _mockService.Setup(s => s.RegisterToEventAsync(1, 1)).ReturnsAsync(false); - - var result = await _controller.RegisterParticipantAsync(1, 1); - - var notFoundResult = Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] public async Task GetById_ReturnsOk_WhenParticipantExists() { + var query = new GetParticipantByIdQuery(1); var participant = new ParticipantDTO { Id = 1, Name = "Participant 1" }; - _mockService.Setup(s => s.GetByIdAsync(1)).ReturnsAsync(participant); + var result = Result.Success(participant); - var result = await _controller.GetParticipantByIdAsync(1); + _mockSender.Setup(s => s.Send(query, It.IsAny())).ReturnsAsync(result); - var okResult = Assert.IsType(result); + var actionResult = await _controller.GetParticipantByIdAsync(1); + + var okResult = Assert.IsType(actionResult); var returnValue = Assert.IsType(okResult.Value); returnValue.Id.Should().Be(1); } @@ -128,19 +123,17 @@ public async Task GetById_ReturnsOk_WhenParticipantExists() [Fact] public async Task GetAll_ReturnsOk_WithList() { - var eventId = 1; var queryParameters = new QueryParameters(); + var query = new GetParticipantsByEventIdQuery(1, queryParameters); var participants = new List { new() { Id = 1, Name = "Participant 1" } }; var pagedResult = new PagedResult(participants, 1, 10, 1); - _mockService - .Setup(s => s.GetAllPagedParticipantsByEventIdAsync(eventId, It.IsAny())) - .ReturnsAsync(pagedResult); + _mockSender.Setup(s => s.Send(query, It.IsAny())).ReturnsAsync(pagedResult); - var result = await _controller.GetAllParticipantsAsync(eventId, queryParameters); + var actionResult = await _controller.GetAllParticipantsAsync(1, queryParameters); - var okResult = Assert.IsType(result); + var okResult = Assert.IsType(actionResult); var returnValue = Assert.IsType>(okResult.Value); + returnValue.Should().HaveCount(1); } - } diff --git a/EventFlow-API.Tests/Controllers/SpeakerControllerTests.cs b/EventFlow-API.Tests/Controllers/SpeakerControllerTests.cs index 310cc1b..96485a4 100644 --- a/EventFlow-API.Tests/Controllers/SpeakerControllerTests.cs +++ b/EventFlow-API.Tests/Controllers/SpeakerControllerTests.cs @@ -1,16 +1,23 @@ -using Microsoft.AspNetCore.Http; +using MediatR; +using Microsoft.AspNetCore.Http; +using EventFlow.Application.Features.Speakers.Commands.CreateSpeaker; +using EventFlow.Application.Features.Speakers.Commands.DeleteSpeaker; +using EventFlow.Application.Features.Speakers.Commands.UpdateSpeaker; +using EventFlow.Application.Features.Speakers.Queries.GetAllSpeakers; +using EventFlow.Application.Features.Speakers.Queries.GetSpeakerById; +using EventFlow.Core.Primitives; namespace EventFlow_API.Tests.Controllers; public class SpeakerControllerTests { private readonly SpeakerController _controller; - private readonly Mock _mockService; + private readonly Mock _mockSender; public SpeakerControllerTests() { - _mockService = new Mock(); - _controller = new SpeakerController(_mockService.Object); + _mockSender = new Mock(); + _controller = new SpeakerController(_mockSender.Object); _controller.ControllerContext = new ControllerContext { HttpContext = new DefaultHttpContext() @@ -20,107 +27,94 @@ public SpeakerControllerTests() [Fact] public async Task PostAsync_ReturnsOk_WhenSpeakerCreated() { - var command = new SpeakerCommand { Name = "Test", Email = "test@example.com", Biography = "Bio" }; - var speaker = new Speaker { Id = 1, Name = "Test", Email = "test@example.com", Biography = "Bio" }; + var command = new CreateSpeakerCommand("Test", "test@example.com", "Bio", "Expertise"); + var result = Result.Success(1); - _mockService.Setup(s => s.CreateAsync(command)).ReturnsAsync(speaker); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.PostAsync(command); + var actionResult = await _controller.PostAsync(command); - var okResult = Assert.IsType(result); - var returnValue = Assert.IsType(okResult.Value); - returnValue.Id.Should().Be(1); + var okResult = Assert.IsType(actionResult); + Assert.Equal(1, okResult.Value); } [Fact] public async Task PostAsync_ReturnsBadRequest_WhenCreationFails() { - var command = new SpeakerCommand { Name = "Test", Email = "test@example.com", Biography = "Bio" }; + var command = new CreateSpeakerCommand("Test", "test@example.com", "Bio", "Expertise"); + var result = Result.Failure(Error.Validation("Speaker.CreateFailed", "Failed to create speaker")); - _mockService.Setup(s => s.CreateAsync(command)).ReturnsAsync((Speaker)null!); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.PostAsync(command); + var actionResult = await _controller.PostAsync(command); - Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] public async Task UpdateAsync_ReturnsOk_WhenUpdated() { - var command = new SpeakerCommand { Name = "Updated", Email = "updated@example.com", Biography = "Bio" }; - var updated = new SpeakerDTO { Id = 1, Name = "Updated" }; + var command = new UpdateSpeakerCommand(1, "Updated", "updated@example.com", "Bio", "Expertise"); + var result = Result.Success(); - _mockService.Setup(s => s.UpdateAsync(1, command)).ReturnsAsync(updated); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.UpdateAsync(1, command); + var actionResult = await _controller.UpdateAsync(1, command); - var okResult = Assert.IsType(result); - var returnValue = Assert.IsType(okResult.Value); - returnValue.Id.Should().Be(1); + Assert.IsType(actionResult); } [Fact] public async Task UpdateAsync_ReturnsNotFound_WhenUpdateFails() { - var command = new SpeakerCommand { Name = "Updated", Email = "updated@example.com", Biography = "Bio" }; + var command = new UpdateSpeakerCommand(1, "Updated", "updated@example.com", "Bio", "Expertise"); + var result = Result.Failure(Error.NotFound("Speaker.NotFound", "Speaker not found")); - _mockService.Setup(s => s.UpdateAsync(1, command)).ReturnsAsync((SpeakerDTO)null!); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.UpdateAsync(1, command); + var actionResult = await _controller.UpdateAsync(1, command); - Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] public async Task DeleteAsync_ReturnsOk_WhenDeleted() { - _mockService.Setup(s => s.DeleteAsync(1)).ReturnsAsync(true); - - var result = await _controller.DeleteAsync(1); + var command = new DeleteSpeakerCommand(1); + var result = Result.Success(); - var okResult = Assert.IsType(result); - Assert.Equal(1, okResult.Value); - } - - [Fact] - public async Task DeleteAsync_ReturnsNotFound_WhenDeleteFails() - { - _mockService.Setup(s => s.DeleteAsync(1)).ReturnsAsync(false); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var result = await _controller.DeleteAsync(1); + var actionResult = await _controller.DeleteAsync(1); - Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] - public async Task RegisterToEventAsync_ReturnsOk_WhenSuccess() + public async Task DeleteAsync_ReturnsNotFound_WhenDeleteFails() { - _mockService.Setup(s => s.RegisterToEventAsync(1, 1)).ReturnsAsync(true); + var command = new DeleteSpeakerCommand(1); + var result = Result.Failure(Error.NotFound("Speaker.NotFound", "Speaker not found")); - var result = await _controller.RegisterToEventAsync(1, 1); + _mockSender.Setup(s => s.Send(command, It.IsAny())).ReturnsAsync(result); - var okResult = Assert.IsType(result); - } + var actionResult = await _controller.DeleteAsync(1); - [Fact] - public async Task RegisterToEventAsync_ReturnsNotFound_WhenFailure() - { - _mockService.Setup(s => s.RegisterToEventAsync(1, 1)).ReturnsAsync(false); - - var result = await _controller.RegisterToEventAsync(1, 1); - - var notFoundResult = Assert.IsType(result); + Assert.IsType(actionResult); } [Fact] public async Task GetById_ReturnsOk_WhenSpeakerExists() { + var query = new GetSpeakerByIdQuery(1); var speaker = new SpeakerDTO { Id = 1, Name = "Speaker 1" }; - _mockService.Setup(s => s.GetByIdAsync(1)).ReturnsAsync(speaker); + var result = Result.Success(speaker); + + _mockSender.Setup(s => s.Send(query, It.IsAny())).ReturnsAsync(result); - var result = await _controller.GetSpeakerByIdAsync(1); + var actionResult = await _controller.GetSpeakerByIdAsync(1); - var okResult = Assert.IsType(result); + var okResult = Assert.IsType(actionResult); var returnValue = Assert.IsType(okResult.Value); returnValue.Id.Should().Be(1); } @@ -129,16 +123,15 @@ public async Task GetById_ReturnsOk_WhenSpeakerExists() public async Task GetAll_ReturnsOk_WithList() { var queryParameters = new QueryParameters(); + var query = new GetAllSpeakersQuery(queryParameters); var speakers = new List { new() { Id = 1, Name = "Speaker 1" } }; var pagedResult = new PagedResult(speakers, 1, 10, 1); - _mockService - .Setup(s => s.GetAllPagedSpeakersAsync(It.IsAny())) - .ReturnsAsync(pagedResult); + _mockSender.Setup(s => s.Send(query, It.IsAny())).ReturnsAsync(pagedResult); - var result = await _controller.GetAllSpeakersAsync(queryParameters); + var actionResult = await _controller.GetAllSpeakersAsync(queryParameters); - var okResult = Assert.IsType(result); + var okResult = Assert.IsType(actionResult); var returnValue = Assert.IsType>(okResult.Value); returnValue.Should().HaveCount(1); } diff --git a/EventFlow-API.Tests/Data/IntegrationTestBase.cs b/EventFlow-API.Tests/Data/IntegrationTestBase.cs index f62159f..e8239c6 100644 --- a/EventFlow-API.Tests/Data/IntegrationTestBase.cs +++ b/EventFlow-API.Tests/Data/IntegrationTestBase.cs @@ -25,7 +25,9 @@ protected IntegrationTestBase() private void SeedData() { - _context.Organizer.Add(new Organizer { Id = 1, Name = "Organizer Test", Email = "test@test.com" }); + var organizer = Organizer.Create("Organizer Test", "test@test.com"); + typeof(Organizer).GetProperty("Id")!.SetValue(organizer, 1); + _context.Organizer.Add(organizer); _context.SaveChanges(); } diff --git a/EventFlow-API.Tests/EventFlow.Tests.csproj b/EventFlow-API.Tests/EventFlow.Tests.csproj index be6e413..e1d27da 100644 --- a/EventFlow-API.Tests/EventFlow.Tests.csproj +++ b/EventFlow-API.Tests/EventFlow.Tests.csproj @@ -9,7 +9,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/EventFlow-API.Tests/GlobalUsing.cs b/EventFlow-API.Tests/GlobalUsing.cs index 44ae789..46b2ea2 100644 --- a/EventFlow-API.Tests/GlobalUsing.cs +++ b/EventFlow-API.Tests/GlobalUsing.cs @@ -5,10 +5,11 @@ global using Microsoft.EntityFrameworkCore; global using Microsoft.AspNetCore.Mvc; -global using EventFlow.Core.Commands; -global using EventFlow.Core.Models; -global using EventFlow.Core.Models.DTOs; -global using EventFlow.Core.Services.Interfaces; +global using EventFlow.Application.Commands; +global using EventFlow.Application.DTOs; global using EventFlow.Application.Services; global using EventFlow.Application.Validators; +global using EventFlow.Core.Models; +global using EventFlow.Core.Repository; +global using EventFlow.Core.ValueObjects; global using EventFlow.Presentation.Controllers; \ No newline at end of file diff --git a/EventFlow-API.Tests/Services/EventServiceTests.cs b/EventFlow-API.Tests/Services/EventServiceTests.cs deleted file mode 100644 index 47e9577..0000000 --- a/EventFlow-API.Tests/Services/EventServiceTests.cs +++ /dev/null @@ -1,235 +0,0 @@ -using AutoMapper; -using EventFlow.Core.Models; -using EventFlow.Core.Repository.Interfaces; -using Microsoft.Extensions.Caching.Distributed; - -namespace EventFlow_API.Tests.Services; - -public class EventServiceTests -{ - private readonly Mock _eventRepoMock; - private readonly Mock _mapperMock; - private readonly Mock _cacheMock; - private readonly EventService _eventService; - - public EventServiceTests() - { - _eventRepoMock = new Mock(); - _mapperMock = new Mock(); - _cacheMock = new Mock(); - _eventService = new EventService(_eventRepoMock.Object, _mapperMock.Object, _cacheMock.Object); - } - - [Fact] - public async Task CreateAsync_ShouldReturnEvent_WhenCreated() - { - var command = new EventCommand - { - Title = "Test", - Date = DateTime.Now, - Location = "Loc", - OrganizerId = 1 - }; - - var newEvent = new Event - { - Id = 1, - Title = "Test" - }; - - _eventRepoMock.Setup(r => r.PostAsync(It.IsAny())).ReturnsAsync(newEvent); - _eventRepoMock.Setup(r => r.GetEventWithDetailsByIdAsync(newEvent.Id)).ReturnsAsync(newEvent); - - var result = await _eventService.CreateAsync(command); - - result.Should().NotBeNull(); - result!.Id.Should().Be(1); - } - - [Fact] - public async Task CreateAsync_ShouldReturnNull_WhenPostFails() - { - var command = new EventCommand - { - Title = "Test" - }; - - _eventRepoMock.Setup(r => r.PostAsync(It.IsAny())).ReturnsAsync((Event)null!); - - var result = await _eventService.CreateAsync(command); - - result.Should().BeNull(); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnUpdatedEvent_WhenUpdateSuccessful() - { - var command = new EventCommand - { - Title = "Updated", - Date = DateTime.Now, - Location = "Loc", - OrganizerId = 1 - }; - var existing = new Event - { - Id = 1 - }; - - var updated = new Event - { - Id = 1, - Title = "Updated" - }; - - var updatedDto = new EventDTO - { - Id = 1, - Title = "Updated" - }; - - _eventRepoMock.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync(existing); - _eventRepoMock.Setup(r => r.UpdateAsync(existing)).ReturnsAsync(updated); - _mapperMock.Setup(m => m.Map(updated)).Returns(updatedDto); - - var result = await _eventService.UpdateAsync(1, command); - - result.Should().NotBeNull(); - result!.Id.Should().Be(1); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnNull_WhenEventNotFound() - { - _eventRepoMock.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync((Event)null!); - - var result = await _eventService.UpdateAsync(1, new EventCommand()); - - result.Should().BeNull(); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnNull_WhenUpdateFails() - { - var command = new EventCommand - { - Title = "Updated" - }; - - var existing = new Event - { - Id = 1 - }; - - _eventRepoMock.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync(existing); - _eventRepoMock.Setup(r => r.UpdateAsync(existing)).ReturnsAsync((Event)null!); - - var result = await _eventService.UpdateAsync(1, command); - - result.Should().BeNull(); - } - - [Fact] - public async Task DeleteAsync_ShouldReturnTrue_WhenDeleted() - { - _eventRepoMock.Setup(r => r.DeleteAsync(1)).ReturnsAsync(1); - - var result = await _eventService.DeleteAsync(1); - - result.Should().BeTrue(); - } - - [Fact] - public async Task DeleteAsync_ShouldReturnFalse_WhenDeleteFails() - { - _eventRepoMock.Setup(r => r.DeleteAsync(1)).ReturnsAsync(0); - - var result = await _eventService.DeleteAsync(1); - - result.Should().BeFalse(); - } - - [Fact] - public async Task GetByIdAsync_ShouldReturnEventDTO_WhenEventExists() - { - var eventId = 1; - var eventEntity = new Event - { - Id = eventId, - Title = "Event Title" - }; - - var eventDto = new EventDTO - { - Id = eventId, - Title = "Event Title" - }; - - _eventRepoMock.Setup(r => r.GetEventWithDetailsByIdAsync(eventId)).ReturnsAsync(eventEntity); - _mapperMock.Setup(m => m.Map(eventEntity)).Returns(eventDto); - - var result = await _eventService.GetByIdAsync(eventId); - - result.Should().NotBeNull(); - result!.Id.Should().Be(eventId); - result.Title.Should().Be("Event Title"); - } - - [Fact] - public async Task GetByIdAsync_ShouldReturnNull_WhenEventDoesNotExist() - { - var eventId = 1; - - _eventRepoMock.Setup(r => r.GetEventWithDetailsByIdAsync(eventId)).ReturnsAsync((Event)null!); - - var result = await _eventService.GetByIdAsync(eventId); - - result.Should().BeNull(); - } - - [Fact] - public async Task GetAllAsync_ShouldReturnMappedEvents() - { - var queryParameters = new QueryParameters - { - PageNumber = 1, - PageSize = 10, - Filter = null, - SortBy = null - }; - - var events = new List - { - new Event - { - Id = 1, - Title = "Test" - } - }; - - var pagedResult = new PagedResult(events, queryParameters.PageNumber, queryParameters.PageSize, totalCount: 1); - - var eventsDto = new List - { - new EventDTO - { - Id = 1, - Title = "Test" - } - }; - - var pagedResultDto = new PagedResult(eventsDto, queryParameters.PageNumber, queryParameters.PageSize, totalCount: 1); - - _eventRepoMock.Setup(r => r.GetAllPagedEventsAsync(queryParameters)).ReturnsAsync(pagedResult); - _mapperMock.Setup(m => m.Map>(events)).Returns(eventsDto); - - var result = await _eventService.GetAllPagedEventsAsync(queryParameters); - - result.Should().NotBeNull(); - result.Items.Should().HaveCount(1); - result.Items.First().Title.Should().Be("Test"); - result.TotalCount.Should().Be(1); - result.PageNumber.Should().Be(1); - result.PageSize.Should().Be(10); - } -} diff --git a/EventFlow-API.Tests/Services/OrganizerServiceTests.cs b/EventFlow-API.Tests/Services/OrganizerServiceTests.cs deleted file mode 100644 index 57287f4..0000000 --- a/EventFlow-API.Tests/Services/OrganizerServiceTests.cs +++ /dev/null @@ -1,191 +0,0 @@ -using AutoMapper; -using EventFlow.Core.Repository.Interfaces; -using Microsoft.Extensions.Caching.Distributed; - -namespace EventFlow_API.Tests.Services; - -public class OrganizerServiceTests -{ - private readonly OrganizerService _service; - private readonly Mock _mockOrganizerRepository; - private readonly Mock _mockEventRepository; - private readonly Mock _mockMapper; - private readonly Mock _mockCache; - - public OrganizerServiceTests() - { - _mockOrganizerRepository = new Mock(); - _mockEventRepository = new Mock(); - _mockMapper = new Mock(); - _mockCache = new Mock(); - _service = new OrganizerService(_mockOrganizerRepository.Object, _mockEventRepository.Object, _mockMapper.Object, _mockCache.Object); - } - - [Fact] - public async Task CreateAsync_ShouldReturnOrganizer_WhenSuccessful() - { - var command = new OrganizerCommand { Name = "Test", Email = "test@example.com" }; - var organizer = new Organizer { Id = 1, Name = command.Name, Email = command.Email }; - - _mockOrganizerRepository.Setup(r => r.PostAsync(It.IsAny())).ReturnsAsync(organizer); - - var result = await _service.CreateAsync(command); - - result.Should().NotBeNull(); - result.Id.Should().Be(1); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnUpdatedOrganizer_WhenSuccessful() - { - var command = new OrganizerCommand { Name = "Updated", Email = "updated@example.com" }; - var existing = new Organizer { Id = 1, Name = "Old", Email = "old@example.com" }; - var updated = new Organizer { Id = 1, Name = command.Name, Email = command.Email }; - var updatedDTO = new OrganizerDTO { Id = 1, Name = command.Name }; - - _mockOrganizerRepository.Setup(r => r.GetOrganizerByIdAsync(1)).ReturnsAsync(existing); - _mockOrganizerRepository.Setup(r => r.UpdateAsync(existing)).ReturnsAsync(updated); - _mockMapper.Setup(m => m.Map(updated)).Returns(updatedDTO); - - var result = await _service.UpdateAsync(1, command); - - result.Should().NotBeNull(); - result!.Id.Should().Be(1); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnNull_WhenOrganizerNotFound() - { - _mockOrganizerRepository.Setup(r => r.GetOrganizerByIdAsync(1)).ReturnsAsync((Organizer)null!); - - var result = await _service.UpdateAsync(1, new OrganizerCommand { Name = "Name", Email = "Email" }); - - result.Should().BeNull(); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnNull_WhenUpdateFails() - { - var command = new OrganizerCommand { Name = "Test", Email = "Email" }; - var existing = new Organizer { Id = 1 }; - - _mockOrganizerRepository.Setup(r => r.GetOrganizerByIdAsync(1)).ReturnsAsync(existing); - _mockOrganizerRepository.Setup(r => r.UpdateAsync(existing)).ReturnsAsync((Organizer)null!); - - var result = await _service.UpdateAsync(1, command); - - result.Should().BeNull(); - } - - [Fact] - public async Task DeleteAsync_ShouldReturnTrue_WhenSuccessful() - { - _mockOrganizerRepository.Setup(r => r.DeleteAsync(1)).ReturnsAsync(1); - - var result = await _service.DeleteAsync(1); - - result.Should().BeTrue(); - } - - [Fact] - public async Task DeleteAsync_ShouldReturnFalse_WhenUnsuccessful() - { - _mockOrganizerRepository.Setup(r => r.DeleteAsync(1)).ReturnsAsync(0); - - var result = await _service.DeleteAsync(1); - - result.Should().BeFalse(); - } - - [Fact] - public async Task RegisterToEventAsync_ShouldReturnTrue_WhenSuccessful() - { - var organizer = new Organizer { Id = 1 }; - var evento = new Event { Id = 1, OrganizerId = 0 }; - - _mockOrganizerRepository.Setup(r => r.GetOrganizerByIdAsync(1)).ReturnsAsync(organizer); - _mockEventRepository.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync(evento); - _mockEventRepository.Setup(r => r.UpdateAsync(evento)).ReturnsAsync(evento); - - var result = await _service.RegisterToEventAsync(1, 1); - - result.Should().BeTrue(); - } - - [Fact] - public async Task RegisterToEventAsync_ShouldReturnFalse_WhenOrganizerOrEventNotFound() - { - _mockOrganizerRepository.Setup(r => r.GetOrganizerByIdAsync(1)).ReturnsAsync((Organizer)null!); - _mockEventRepository.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync(new Event { Id = 1 }); - - var result1 = await _service.RegisterToEventAsync(1, 1); - result1.Should().BeFalse(); - - _mockOrganizerRepository.Setup(r => r.GetOrganizerByIdAsync(1)).ReturnsAsync(new Organizer { Id = 1 }); - _mockEventRepository.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync((Event)null!); - - var result2 = await _service.RegisterToEventAsync(1, 1); - result2.Should().BeFalse(); - } - - [Fact] - public async Task GetByIdAsync_ShouldReturnOrganizer() - { - var organizer = new Organizer { Id = 1, Name = "Organizer 1" }; - var organizerDTO = new OrganizerDTO { Id = 1, Name = "Organizer 1" }; - - _mockOrganizerRepository.Setup(r => r.GetOrganizerByIdAsync(1)).ReturnsAsync(organizer); - _mockMapper.Setup(m => m.Map(organizer)).Returns(organizerDTO); - - var result = await _service.GetByIdAsync(1); - - result.Should().NotBeNull(); - result.Id.Should().Be(1); - } - - [Fact] - public async Task GetAllAsync_ShouldReturnPagedResultOrganizers() - { - var queryParameters = new QueryParameters - { - PageNumber = 1, - PageSize = 10, - Filter = null, - SortBy = null - }; - - var organizers = new List - { - new Organizer { Id = 1, Name = "Organizer 1" } - }; - - var pagedResult = new PagedResult( - organizers, - queryParameters.PageNumber, - queryParameters.PageSize, - totalCount: 1 - ); - - var organizerDTOs = new List - { - new OrganizerDTO { Id = 1, Name = "Organizer 1" } - }; - - _mockOrganizerRepository - .Setup(r => r.GetAllPagedOrganizersAsync(queryParameters)) - .ReturnsAsync(pagedResult); - - _mockMapper - .Setup(m => m.Map>(organizers)) - .Returns(organizerDTOs); - - var result = await _service.GetAllPagedOrganizersAsync(queryParameters); - - result.Should().NotBeNull(); - result.Items.Should().HaveCount(1); - result.Items.First().Id.Should().Be(1); - result.TotalCount.Should().Be(1); - result.PageNumber.Should().Be(1); - result.PageSize.Should().Be(10); - } -} diff --git a/EventFlow-API.Tests/Services/ParticipantServiceTests.cs b/EventFlow-API.Tests/Services/ParticipantServiceTests.cs deleted file mode 100644 index bf39062..0000000 --- a/EventFlow-API.Tests/Services/ParticipantServiceTests.cs +++ /dev/null @@ -1,197 +0,0 @@ -using AutoMapper; -using EventFlow.Core.Repository.Interfaces; -using EventFlow.Infrastructure.Data; -using Microsoft.Extensions.Caching.Distributed; - -namespace EventFlow_API.Tests.Services; - -public class ParticipantServiceTests -{ - private readonly ParticipantService _service; - private readonly Mock _mockParticipantRepository; - private readonly Mock _mockEventRepository; - private readonly Mock _mockMapper; - private readonly Mock _mockCache; - - public ParticipantServiceTests() - { - _mockParticipantRepository = new Mock(); - _mockEventRepository = new Mock(); - _mockMapper = new Mock(); - _mockCache = new Mock(); - - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: "TestDatabase") - .Options; - - _service = new ParticipantService(_mockParticipantRepository.Object, _mockEventRepository.Object, _mockMapper.Object, _mockCache.Object); - } - - [Fact] - public async Task CreateAsync_ShouldReturnParticipant_WhenSuccessful() - { - var command = new ParticipantCommand { Name = "Test", Email = "test@example.com" }; - var participant = new Participant { Id = 1, Name = command.Name, Email = command.Email }; - - _mockParticipantRepository.Setup(r => r.PostAsync(It.IsAny())).ReturnsAsync(participant); - - var result = await _service.CreateAsync(command); - - result.Should().NotBeNull(); - result.Id.Should().Be(1); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnUpdatedParticipant_WhenSuccessful() - { - var command = new ParticipantCommand { Name = "Updated", Email = "updated@example.com" }; - var existing = new Participant { Id = 1, Name = "Old", Email = "old@example.com" }; - var updated = new Participant { Id = 1, Name = command.Name, Email = command.Email }; - var updatedDTO = new ParticipantDTO { Id = 1, Name = command.Name }; - - _mockParticipantRepository.Setup(r => r.GetParticipantByIdAsync(1)).ReturnsAsync(existing); - _mockParticipantRepository.Setup(r => r.UpdateAsync(existing)).ReturnsAsync(updated); - _mockMapper.Setup(m => m.Map(updated)).Returns(updatedDTO); - - var result = await _service.UpdateAsync(1, command); - - result.Should().NotBeNull(); - result!.Id.Should().Be(1); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnNull_WhenParticipantNotFound() - { - _mockParticipantRepository.Setup(r => r.GetParticipantByIdAsync(1)).ReturnsAsync((Participant)null!); - - var result = await _service.UpdateAsync(1, new ParticipantCommand { Name = "Name", Email = "Email" }); - - result.Should().BeNull(); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnNull_WhenUpdateFails() - { - var command = new ParticipantCommand { Name = "Test", Email = "Email" }; - var existing = new Participant { Id = 1 }; - - _mockParticipantRepository.Setup(r => r.GetParticipantByIdAsync(1)).ReturnsAsync(existing); - _mockParticipantRepository.Setup(r => r.UpdateAsync(existing)).ReturnsAsync((Participant)null!); - - var result = await _service.UpdateAsync(1, command); - - result.Should().BeNull(); - } - - [Fact] - public async Task DeleteAsync_ShouldReturnTrue_WhenSuccessful() - { - _mockParticipantRepository.Setup(r => r.DeleteAsync(1)).ReturnsAsync(1); - - var result = await _service.DeleteAsync(1); - - result.Should().BeTrue(); - } - - [Fact] - public async Task DeleteAsync_ShouldReturnFalse_WhenUnsuccessful() - { - _mockParticipantRepository.Setup(r => r.DeleteAsync(1)).ReturnsAsync(0); - - var result = await _service.DeleteAsync(1); - - result.Should().BeFalse(); - } - - [Fact] - public async Task RegisterToEventAsync_ShouldReturnTrue_WhenSuccessful() - { - var participant = new Participant { Id = 1, Events = new List() }; - var evento = new Event { Id = 1 }; - - _mockParticipantRepository.Setup(r => r.GetParticipantByIdAsync(1)).ReturnsAsync(participant); - _mockEventRepository.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync(evento); - _mockParticipantRepository.Setup(r => r.UpdateAsync(participant)).ReturnsAsync(participant); - - var result = await _service.RegisterToEventAsync(1, 1); - - result.Should().BeTrue(); - } - - [Fact] - public async Task RegisterToEventAsync_ShouldReturnFalse_WhenParticipantOrEventNotFound() - { - _mockParticipantRepository.Setup(r => r.GetParticipantByIdAsync(1)).ReturnsAsync((Participant)null!); - _mockEventRepository.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync(new Event { Id = 1 }); - - var result1 = await _service.RegisterToEventAsync(1, 1); - result1.Should().BeFalse(); - - _mockParticipantRepository.Setup(r => r.GetParticipantByIdAsync(1)).ReturnsAsync(new Participant { Id = 1, Events = new List() }); - _mockEventRepository.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync((Event)null!); - - var result2 = await _service.RegisterToEventAsync(1, 1); - result2.Should().BeFalse(); - } - - [Fact] - public async Task GetByIdAsync_ShouldReturnParticipant() - { - var participant = new Participant { Id = 1, Name = "Participant 1" }; - var participantDTO = new ParticipantDTO { Id = 1, Name = "Participant 1" }; - - _mockParticipantRepository.Setup(r => r.GetParticipantByIdAsync(1)).ReturnsAsync(participant); - _mockMapper.Setup(m => m.Map(participant)).Returns(participantDTO); - - var result = await _service.GetByIdAsync(1); - - result.Should().NotBeNull(); - result.Id.Should().Be(1); - } - - [Fact] - public async Task GetAllAsync_ShouldReturnPagedResultParticipants() - { - var queryParameters = new QueryParameters - { - PageNumber = 1, - PageSize = 10, - Filter = null, - SortBy = null - }; - - var participants = new List - { - new Participant { Id = 1, Name = "Participant 1" } - }; - - var pagedResult = new PagedResult( - participants, - queryParameters.PageNumber, - queryParameters.PageSize, - totalCount: 1 - ); - - var participantDTOs = new List - { - new ParticipantDTO { Id = 1, Name = "Participant 1" } - }; - - _mockParticipantRepository - .Setup(r => r.GetAllPagedParticipantsByEventIdAsync(1, queryParameters)) - .ReturnsAsync(pagedResult); - - _mockMapper - .Setup(m => m.Map>(participants)) - .Returns(participantDTOs); - - var result = await _service.GetAllPagedParticipantsByEventIdAsync(1, queryParameters); - - result.Should().NotBeNull(); - result.Items.Should().HaveCount(1); - result.Items.First().Id.Should().Be(1); - result.TotalCount.Should().Be(1); - result.PageNumber.Should().Be(1); - result.PageSize.Should().Be(10); - } -} diff --git a/EventFlow-API.Tests/Services/SpeakerServiceTests.cs b/EventFlow-API.Tests/Services/SpeakerServiceTests.cs deleted file mode 100644 index b940261..0000000 --- a/EventFlow-API.Tests/Services/SpeakerServiceTests.cs +++ /dev/null @@ -1,210 +0,0 @@ -using AutoMapper; -using EventFlow.Core.Repository.Interfaces; -using EventFlow.Infrastructure.Data; -using Microsoft.Extensions.Caching.Distributed; - -namespace EventFlow_API.Tests.Services; - -public class SpeakerServiceTests -{ - private readonly SpeakerService _service; - private readonly Mock _mockRepository; - private readonly Mock _mockEventRepository; - private readonly Mock _mockMapper; - private readonly Mock _mockCache; - - public SpeakerServiceTests() - { - _mockRepository = new Mock(); - _mockEventRepository = new Mock(); - _mockMapper = new Mock(); - _mockCache = new Mock(); - - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: "TestDatabase") - .Options; - - _service = new SpeakerService(_mockRepository.Object, _mockEventRepository.Object, _mockMapper.Object, _mockCache.Object); - } - - [Fact] - public async Task CreateAsync_ShouldReturnSpeaker_WhenSuccessful() - { - var command = new SpeakerCommand { Name = "Test", Email = "test@example.com", Biography = "Bio" }; - var speaker = new Speaker { Id = 1, Name = command.Name, Email = command.Email, Biography = command.Biography }; - - _mockRepository.Setup(r => r.PostAsync(It.IsAny())).ReturnsAsync(speaker); - - var result = await _service.CreateAsync(command); - - result.Should().NotBeNull(); - result.Id.Should().Be(1); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnUpdatedSpeaker_WhenSuccessful() - { - var command = new SpeakerCommand { Name = "Updated", Email = "updated@example.com", Biography = "New Bio" }; - var existing = new Speaker { Id = 1, Name = "Old", Email = "old@example.com", Biography = "Old Bio" }; - var updated = new Speaker { Id = 1, Name = command.Name, Email = command.Email, Biography = command.Biography }; - var updatedDTO = new SpeakerDTO { Id = 1, Name = command.Name }; - - _mockRepository.Setup(r => r.GetSpeakerByIdAsync(1)).ReturnsAsync(existing); - _mockRepository.Setup(r => r.UpdateAsync(existing)).ReturnsAsync(updated); - _mockMapper.Setup(m => m.Map(updated)).Returns(updatedDTO); - - var result = await _service.UpdateAsync(1, command); - - result.Should().NotBeNull(); - result!.Id.Should().Be(1); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnNull_WhenSpeakerNotFound() - { - _mockRepository.Setup(r => r.GetSpeakerByIdAsync(1)).ReturnsAsync((Speaker)null!); - - var result = await _service.UpdateAsync(1, new SpeakerCommand { Name = "Name", Email = "Email", Biography = "Bio" }); - - result.Should().BeNull(); - } - - [Fact] - public async Task UpdateAsync_ShouldReturnNull_WhenUpdateFails() - { - var command = new SpeakerCommand { Name = "Test", Email = "Email", Biography = "Bio" }; - var existing = new Speaker { Id = 1 }; - - _mockRepository.Setup(r => r.GetSpeakerByIdAsync(1)).ReturnsAsync(existing); - _mockRepository.Setup(r => r.UpdateAsync(existing)).ReturnsAsync((Speaker)null!); - - var result = await _service.UpdateAsync(1, command); - - result.Should().BeNull(); - } - - [Fact] - public async Task DeleteAsync_ShouldReturnTrue_WhenSuccessful() - { - _mockRepository.Setup(r => r.DeleteAsync(1)).ReturnsAsync(1); - - var result = await _service.DeleteAsync(1); - - result.Should().BeTrue(); - } - - [Fact] - public async Task DeleteAsync_ShouldReturnFalse_WhenUnsuccessful() - { - _mockRepository.Setup(r => r.DeleteAsync(1)).ReturnsAsync(0); - - var result = await _service.DeleteAsync(1); - - result.Should().BeFalse(); - } - - [Fact] - public async Task RegisterToEventAsync_ShouldReturnTrue_WhenSuccessful() - { - var speaker = new Speaker { Id = 1, SpeakerEvents = new List() }; - var evento = new Event { Id = 1 }; - - _mockRepository.Setup(r => r.GetSpeakerByIdAsync(1)).ReturnsAsync(speaker); - _mockEventRepository.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync(evento); - - var result = await _service.RegisterToEventAsync(1, 1); - - result.Should().BeTrue(); - } - - [Fact] - public async Task RegisterToEventAsync_ShouldReturnFalse_WhenSpeakerOrEventNotFound() - { - _mockRepository.Setup(r => r.GetSpeakerByIdAsync(1)).ReturnsAsync((Speaker)null!); - _mockEventRepository.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync(new Event { Id = 1 }); - - var result1 = await _service.RegisterToEventAsync(1, 1); - result1.Should().BeFalse(); - - _mockRepository.Setup(r => r.GetSpeakerByIdAsync(1)).ReturnsAsync(new Speaker { Id = 1, SpeakerEvents = new List() }); - _mockEventRepository.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync((Event)null!); - - var result2 = await _service.RegisterToEventAsync(1, 1); - result2.Should().BeFalse(); - } - - [Fact] - public async Task RegisterToEventAsync_ShouldReturnTrue_WhenAlreadyLinked() - { - var speaker = new Speaker { Id = 1, SpeakerEvents = new List { new SpeakerEvent { EventId = 1 } } }; - var evento = new Event { Id = 1 }; - - _mockRepository.Setup(r => r.GetSpeakerByIdAsync(1)).ReturnsAsync(speaker); - _mockEventRepository.Setup(r => r.GetEventByIdAsync(1)).ReturnsAsync(evento); - - var result = await _service.RegisterToEventAsync(1, 1); - - result.Should().BeTrue(); - } - - [Fact] - public async Task GetByIdAsync_ShouldReturnSpeaker() - { - var speaker = new Speaker { Id = 1, Name = "Speaker 1" }; - var speakerDTO = new SpeakerDTO { Id = 1, Name = "Speaker 1" }; - - _mockRepository.Setup(r => r.GetSpeakerByIdAsync(1)).ReturnsAsync(speaker); - _mockMapper.Setup(m => m.Map(speaker)).Returns(speakerDTO); - - var result = await _service.GetByIdAsync(1); - - result.Should().NotBeNull(); - result.Id.Should().Be(1); - } - - [Fact] - public async Task GetAllAsync_ShouldReturnPagedResultSpeakers() - { - var queryParameters = new QueryParameters - { - PageNumber = 1, - PageSize = 10, - Filter = null, - SortBy = null - }; - - var speakers = new List - { - new Speaker { Id = 1, Name = "Speaker 1" } - }; - - var pagedResult = new PagedResult( - speakers, - queryParameters.PageNumber, - queryParameters.PageSize, - totalCount: 1 - ); - - var speakerDTOs = new List - { - new SpeakerDTO { Id = 1, Name = "Speaker 1" } - }; - - _mockRepository - .Setup(r => r.GetAllPagedSpeakersAsync(queryParameters)) - .ReturnsAsync(pagedResult); - - _mockMapper - .Setup(m => m.Map>(speakers)) - .Returns(speakerDTOs); - - var result = await _service.GetAllPagedSpeakersAsync(queryParameters); - - result.Should().NotBeNull(); - result.Items.Should().HaveCount(1); - result.Items.First().Id.Should().Be(1); - result.TotalCount.Should().Be(1); - result.PageNumber.Should().Be(1); - result.PageSize.Should().Be(10); - } -} diff --git a/EventFlow-API.Tests/ValueObjects/EmailTests.cs b/EventFlow-API.Tests/ValueObjects/EmailTests.cs new file mode 100644 index 0000000..7ac3c8d --- /dev/null +++ b/EventFlow-API.Tests/ValueObjects/EmailTests.cs @@ -0,0 +1,116 @@ +namespace EventFlow_API.Tests.ValueObjects; + +public class EmailTests +{ + [Fact] + public void Create_ValidEmail_ReturnsEmail() + { + var result = Email.Create("test@example.com"); + + result.Should().NotBeNull(); + result.Value.Should().Be("test@example.com"); + } + + [Theory] + [InlineData("test@example.com")] + [InlineData("user.name@domain.co")] + [InlineData("user+tag@example.org")] + public void Create_ValidEmails_AcceptAll(string email) + { + var result = Email.Create(email); + + result.Value.Should().Be(email.ToLowerInvariant()); + } + + [Fact] + public void Create_EmptyEmail_ThrowsArgumentException() + { + Action act = () => Email.Create(""); + + act.Should().Throw() + .WithMessage("*cannot be empty*"); + } + + [Fact] + public void Create_InvalidEmail_ThrowsArgumentException() + { + Action act = () => Email.Create("invalid-email"); + + act.Should().Throw() + .WithMessage("*Invalid email format*"); + } + + [Fact] + public void Create_NullEmail_ThrowsArgumentException() + { + Action act = () => Email.Create(null!); + + act.Should().Throw(); + } + + [Fact] + public void TryCreate_ValidEmail_ReturnsTrue() + { + var success = Email.TryCreate("test@example.com", out var result); + + success.Should().BeTrue(); + result.Should().NotBeNull(); + result!.Value.Should().Be("test@example.com"); + } + + [Fact] + public void TryCreate_InvalidEmail_ReturnsFalse() + { + var success = Email.TryCreate("invalid", out var result); + + success.Should().BeFalse(); + result.Should().BeNull(); + } + + [Fact] + public void Equals_SameValue_ReturnsTrue() + { + var email1 = Email.Create("test@example.com"); + var email2 = Email.Create("test@example.com"); + + email1.Should().Be(email2); + (email1 == email2).Should().BeTrue(); + } + + [Fact] + public void Equals_DifferentValue_ReturnsFalse() + { + var email1 = Email.Create("test1@example.com"); + var email2 = Email.Create("test2@example.com"); + + email1.Should().NotBe(email2); + (email1 != email2).Should().BeTrue(); + } + + [Fact] + public void Equals_CaseInsensitive_ReturnsTrue() + { + var email1 = Email.Create("Test@Example.COM"); + var email2 = Email.Create("test@example.com"); + + email1.Should().Be(email2); + } + + [Fact] + public void ImplicitConversion_ToString_ReturnsValue() + { + var email = Email.Create("test@example.com"); + + string result = email; + + result.Should().Be("test@example.com"); + } + + [Fact] + public void ToString_ReturnsValue() + { + var email = Email.Create("test@example.com"); + + email.ToString().Should().Be("test@example.com"); + } +} diff --git a/EventFlow-API.Tests/ValueObjects/EventTitleTests.cs b/EventFlow-API.Tests/ValueObjects/EventTitleTests.cs new file mode 100644 index 0000000..557dc94 --- /dev/null +++ b/EventFlow-API.Tests/ValueObjects/EventTitleTests.cs @@ -0,0 +1,76 @@ +namespace EventFlow_API.Tests.ValueObjects; + +public class EventTitleTests +{ + [Fact] + public void Create_ValidTitle_ReturnsEventTitle() + { + var result = EventTitle.Create("Tech Conference 2024"); + + result.Should().NotBeNull(); + result.Value.Should().Be("Tech Conference 2024"); + } + + [Fact] + public void Create_ShortTitle_ThrowsArgumentException() + { + Action act = () => EventTitle.Create("AB"); + + act.Should().Throw() + .WithMessage("*at least 3 characters*"); + } + + [Fact] + public void Create_LongTitle_ThrowsArgumentException() + { + var longTitle = new string('A', 201); + Action act = () => EventTitle.Create(longTitle); + + act.Should().Throw() + .WithMessage("*cannot exceed 200 characters*"); + } + + [Fact] + public void Create_EmptyTitle_ThrowsArgumentException() + { + Action act = () => EventTitle.Create(""); + + act.Should().Throw(); + } + + [Fact] + public void Create_TrimmedTitle_ReturnsTrimmedValue() + { + var result = EventTitle.Create(" Tech Conference 2024 "); + + result.Value.Should().Be("Tech Conference 2024"); + } + + [Fact] + public void TryCreate_ValidTitle_ReturnsTrue() + { + var success = EventTitle.TryCreate("Valid Title", out var result); + + success.Should().BeTrue(); + result.Should().NotBeNull(); + } + + [Fact] + public void Equals_SameValue_ReturnsTrue() + { + var title1 = EventTitle.Create("Conference"); + var title2 = EventTitle.Create("Conference"); + + title1.Should().Be(title2); + } + + [Fact] + public void ImplicitConversion_ToString_ReturnsValue() + { + var title = EventTitle.Create("Conference"); + + string result = title; + + result.Should().Be("Conference"); + } +} diff --git a/EventFlow.Application/Abstractions/ICommand.cs b/EventFlow.Application/Abstractions/ICommand.cs new file mode 100644 index 0000000..e75f327 --- /dev/null +++ b/EventFlow.Application/Abstractions/ICommand.cs @@ -0,0 +1,11 @@ +using MediatR; + +namespace EventFlow.Application.Abstractions; + +public interface ICommand : IRequest +{ +} + +public interface ICommand : IRequest +{ +} diff --git a/EventFlow.Application/Abstractions/IJwtTokenService.cs b/EventFlow.Application/Abstractions/IJwtTokenService.cs new file mode 100644 index 0000000..422105f --- /dev/null +++ b/EventFlow.Application/Abstractions/IJwtTokenService.cs @@ -0,0 +1,9 @@ +using System.Security.Claims; + +namespace EventFlow.Application.Abstractions; + +public interface IJwtTokenService +{ + string GenerateToken(int userId, string username, string email); + ClaimsPrincipal? ValidateToken(string token); +} diff --git a/EventFlow.Application/Abstractions/IPasswordHasher.cs b/EventFlow.Application/Abstractions/IPasswordHasher.cs new file mode 100644 index 0000000..51b9007 --- /dev/null +++ b/EventFlow.Application/Abstractions/IPasswordHasher.cs @@ -0,0 +1,7 @@ +namespace EventFlow.Application.Abstractions; + +public interface IPasswordHasher +{ + string HashPassword(string password); + bool VerifyPassword(string password, string hash); +} diff --git a/EventFlow.Application/Abstractions/IQuery.cs b/EventFlow.Application/Abstractions/IQuery.cs new file mode 100644 index 0000000..f727710 --- /dev/null +++ b/EventFlow.Application/Abstractions/IQuery.cs @@ -0,0 +1,7 @@ +using MediatR; + +namespace EventFlow.Application.Abstractions; + +public interface IQuery : IRequest +{ +} diff --git a/EventFlow.Application/Abstractions/IUnitOfWork.cs b/EventFlow.Application/Abstractions/IUnitOfWork.cs new file mode 100644 index 0000000..e151314 --- /dev/null +++ b/EventFlow.Application/Abstractions/IUnitOfWork.cs @@ -0,0 +1,6 @@ +namespace EventFlow.Application.Abstractions; + +public interface IUnitOfWork +{ + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/EventFlow.Application/Behaviors/LoggingBehavior.cs b/EventFlow.Application/Behaviors/LoggingBehavior.cs new file mode 100644 index 0000000..3c1ce9e --- /dev/null +++ b/EventFlow.Application/Behaviors/LoggingBehavior.cs @@ -0,0 +1,38 @@ +using MediatR; +using Microsoft.Extensions.Logging; + +namespace EventFlow.Application.Behaviors; + +public class LoggingBehavior : IPipelineBehavior + where TRequest : notnull +{ + private readonly ILogger> _logger; + + public LoggingBehavior(ILogger> logger) + { + _logger = logger; + } + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + var requestName = typeof(TRequest).Name; + + _logger.LogInformation( + "Handling {RequestName}", + requestName); + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + var response = await next(); + stopwatch.Stop(); + + _logger.LogInformation( + "Handled {RequestName} in {ElapsedMilliseconds}ms", + requestName, + stopwatch.ElapsedMilliseconds); + + return response; + } +} diff --git a/EventFlow.Application/Behaviors/ValidationBehavior.cs b/EventFlow.Application/Behaviors/ValidationBehavior.cs new file mode 100644 index 0000000..232cd28 --- /dev/null +++ b/EventFlow.Application/Behaviors/ValidationBehavior.cs @@ -0,0 +1,44 @@ +using FluentValidation; +using MediatR; + +namespace EventFlow.Application.Behaviors; + +public class ValidationBehavior : IPipelineBehavior + where TRequest : notnull +{ + private readonly IEnumerable> _validators; + + public ValidationBehavior(IEnumerable> validators) + { + _validators = validators; + } + + public async Task Handle( + TRequest request, + RequestHandlerDelegate next, + CancellationToken cancellationToken) + { + if (_validators is ICollection> validatorCollection + ? validatorCollection.Count == 0 + : !_validators.Any()) + { + return await next(); + } + + var context = new ValidationContext(request); + var validationResults = await Task.WhenAll( + _validators.Select(v => v.ValidateAsync(context, cancellationToken))); + + var failures = validationResults + .SelectMany(r => r.Errors) + .Where(f => f != null) + .ToList(); + + if (failures.Count > 0) + { + throw new ValidationException(failures); + } + + return await next(); + } +} diff --git a/EventFlow.Core/Commands/EventCommand.cs b/EventFlow.Application/Commands/EventCommand.cs similarity index 58% rename from EventFlow.Core/Commands/EventCommand.cs rename to EventFlow.Application/Commands/EventCommand.cs index fa03044..95bc47b 100644 --- a/EventFlow.Core/Commands/EventCommand.cs +++ b/EventFlow.Application/Commands/EventCommand.cs @@ -1,11 +1,11 @@ -namespace EventFlow.Core.Commands; +namespace EventFlow.Application.Commands; public class EventCommand { public int Id { get; private set; } - public string Title { get; set; } + public required string Title { get; set; } public string? Description { get; set; } public DateTime Date { get; set; } - public string Location { get; set; } + public required string Location { get; set; } public int OrganizerId { get; set; } } diff --git a/EventFlow.Core/Commands/LoginUserCommand.cs b/EventFlow.Application/Commands/LoginUserCommand.cs similarity index 77% rename from EventFlow.Core/Commands/LoginUserCommand.cs rename to EventFlow.Application/Commands/LoginUserCommand.cs index 1c78176..30caeeb 100644 --- a/EventFlow.Core/Commands/LoginUserCommand.cs +++ b/EventFlow.Application/Commands/LoginUserCommand.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Commands; +namespace EventFlow.Application.Commands; public class LoginUserCommand { diff --git a/EventFlow.Application/Commands/OrganizerCommand.cs b/EventFlow.Application/Commands/OrganizerCommand.cs new file mode 100644 index 0000000..e0f23f6 --- /dev/null +++ b/EventFlow.Application/Commands/OrganizerCommand.cs @@ -0,0 +1,7 @@ +namespace EventFlow.Application.Commands; + +public class OrganizerCommand +{ + public required string Name { get; set; } + public required string Email { get; set; } +} diff --git a/EventFlow.Application/Commands/ParticipantCommand.cs b/EventFlow.Application/Commands/ParticipantCommand.cs new file mode 100644 index 0000000..d16d669 --- /dev/null +++ b/EventFlow.Application/Commands/ParticipantCommand.cs @@ -0,0 +1,8 @@ +namespace EventFlow.Application.Commands; + +public class ParticipantCommand +{ + public string Name { get; set; } = string.Empty; + public string Email { get; set; } = string.Empty; + public string? Interests { get; set; } +} diff --git a/EventFlow.Core/Commands/RegisterUserCommand.cs b/EventFlow.Application/Commands/RegisterUserCommand.cs similarity index 83% rename from EventFlow.Core/Commands/RegisterUserCommand.cs rename to EventFlow.Application/Commands/RegisterUserCommand.cs index 3c36e52..d70727e 100644 --- a/EventFlow.Core/Commands/RegisterUserCommand.cs +++ b/EventFlow.Application/Commands/RegisterUserCommand.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Commands; +namespace EventFlow.Application.Commands; public class RegisterUserCommand { diff --git a/EventFlow.Application/Commands/SpeakerCommand.cs b/EventFlow.Application/Commands/SpeakerCommand.cs new file mode 100644 index 0000000..e90e414 --- /dev/null +++ b/EventFlow.Application/Commands/SpeakerCommand.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace EventFlow.Application.Commands; + +public class SpeakerCommand +{ + public required string Name { get; set; } + public required string Email { get; set; } + public string? Biography { get; set; } + + [JsonIgnore] + public int EventId { get; set; } +} diff --git a/EventFlow.Application/DTOs/DashboardStatsDTO.cs b/EventFlow.Application/DTOs/DashboardStatsDTO.cs new file mode 100644 index 0000000..bc4ed71 --- /dev/null +++ b/EventFlow.Application/DTOs/DashboardStatsDTO.cs @@ -0,0 +1,9 @@ +namespace EventFlow.Application.DTOs; + +public class DashboardStatsDTO +{ + public int EventCount { get; set; } + public int OrganizerCount { get; set; } + public int SpeakerCount { get; set; } + public int ParticipantCount { get; set; } +} diff --git a/EventFlow.Core/Models/DTOs/EventDTO.cs b/EventFlow.Application/DTOs/EventDTO.cs similarity index 91% rename from EventFlow.Core/Models/DTOs/EventDTO.cs rename to EventFlow.Application/DTOs/EventDTO.cs index 11b9e97..dd69eb1 100644 --- a/EventFlow.Core/Models/DTOs/EventDTO.cs +++ b/EventFlow.Application/DTOs/EventDTO.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Models.DTOs; +namespace EventFlow.Application.DTOs; public class EventDTO { @@ -12,4 +12,3 @@ public class EventDTO public List Speakers { get; set; } = []; public List Participants { get; set; } = []; } - diff --git a/EventFlow.Core/Models/DTOs/EventSummaryDTO.cs b/EventFlow.Application/DTOs/EventSummaryDTO.cs similarity index 87% rename from EventFlow.Core/Models/DTOs/EventSummaryDTO.cs rename to EventFlow.Application/DTOs/EventSummaryDTO.cs index 167a667..e7d28f0 100644 --- a/EventFlow.Core/Models/DTOs/EventSummaryDTO.cs +++ b/EventFlow.Application/DTOs/EventSummaryDTO.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Models.DTOs; +namespace EventFlow.Application.DTOs; public class EventSummaryDTO { diff --git a/EventFlow.Core/Models/DTOs/OrganizerDTO.cs b/EventFlow.Application/DTOs/OrganizerDTO.cs similarity index 84% rename from EventFlow.Core/Models/DTOs/OrganizerDTO.cs rename to EventFlow.Application/DTOs/OrganizerDTO.cs index 4302a69..70c5014 100644 --- a/EventFlow.Core/Models/DTOs/OrganizerDTO.cs +++ b/EventFlow.Application/DTOs/OrganizerDTO.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Models.DTOs; +namespace EventFlow.Application.DTOs; public class OrganizerDTO { @@ -7,4 +7,3 @@ public class OrganizerDTO public string Email { get; set; } = string.Empty; public List Events { get; set; } = []; } - diff --git a/EventFlow.Core/Models/DTOs/ParticipantDTO.cs b/EventFlow.Application/DTOs/ParticipantDTO.cs similarity index 83% rename from EventFlow.Core/Models/DTOs/ParticipantDTO.cs rename to EventFlow.Application/DTOs/ParticipantDTO.cs index 301d1c8..6aed3e9 100644 --- a/EventFlow.Core/Models/DTOs/ParticipantDTO.cs +++ b/EventFlow.Application/DTOs/ParticipantDTO.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Models.DTOs; +namespace EventFlow.Application.DTOs; public class ParticipantDTO { @@ -7,4 +7,3 @@ public class ParticipantDTO public string Email { get; set; } = string.Empty; public string? Interests { get; set; } } - diff --git a/EventFlow.Core/Models/DTOs/SpeakerDTO.cs b/EventFlow.Application/DTOs/SpeakerDTO.cs similarity index 88% rename from EventFlow.Core/Models/DTOs/SpeakerDTO.cs rename to EventFlow.Application/DTOs/SpeakerDTO.cs index d0680a4..8bc1770 100644 --- a/EventFlow.Core/Models/DTOs/SpeakerDTO.cs +++ b/EventFlow.Application/DTOs/SpeakerDTO.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Models.DTOs; +namespace EventFlow.Application.DTOs; public class SpeakerDTO { diff --git a/EventFlow.Core/Models/DTOs/UserDTO.cs b/EventFlow.Application/DTOs/UserDTO.cs similarity index 80% rename from EventFlow.Core/Models/DTOs/UserDTO.cs rename to EventFlow.Application/DTOs/UserDTO.cs index ad8a70b..28c6902 100644 --- a/EventFlow.Core/Models/DTOs/UserDTO.cs +++ b/EventFlow.Application/DTOs/UserDTO.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Models.DTOs; +namespace EventFlow.Application.DTOs; public class UserDTO { diff --git a/EventFlow.Core/Models/DTOs/UserPasswordDTO.cs b/EventFlow.Application/DTOs/UserPasswordUpdateDTO.cs similarity index 62% rename from EventFlow.Core/Models/DTOs/UserPasswordDTO.cs rename to EventFlow.Application/DTOs/UserPasswordUpdateDTO.cs index 138d0d0..7e9dbc7 100644 --- a/EventFlow.Core/Models/DTOs/UserPasswordDTO.cs +++ b/EventFlow.Application/DTOs/UserPasswordUpdateDTO.cs @@ -1,6 +1,6 @@ -namespace EventFlow.Core.Models.DTOs; +namespace EventFlow.Application.DTOs; -public class UserPasswordUpdateDTO +public class UserPasswordUpdateDto { public string CurrentPassword { get; set; } = string.Empty; public string NewPassword { get; set; } = string.Empty; diff --git a/EventFlow.Application/EventFlow.Application.csproj b/EventFlow.Application/EventFlow.Application.csproj index f3820f5..d3427c4 100644 --- a/EventFlow.Application/EventFlow.Application.csproj +++ b/EventFlow.Application/EventFlow.Application.csproj @@ -7,12 +7,11 @@ - - + + - - + diff --git a/EventFlow.Application/Features/Events/Commands/CreateEvent/CreateEventCommand.cs b/EventFlow.Application/Features/Events/Commands/CreateEvent/CreateEventCommand.cs new file mode 100644 index 0000000..5f599c8 --- /dev/null +++ b/EventFlow.Application/Features/Events/Commands/CreateEvent/CreateEventCommand.cs @@ -0,0 +1,10 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Events.Commands.CreateEvent; + +public record CreateEventCommand( + string Title, + string? Description, + DateTime Date, + string Location, + int OrganizerId) : ICommand>; diff --git a/EventFlow.Application/Features/Events/Commands/CreateEvent/CreateEventCommandHandler.cs b/EventFlow.Application/Features/Events/Commands/CreateEvent/CreateEventCommandHandler.cs new file mode 100644 index 0000000..5030dc7 --- /dev/null +++ b/EventFlow.Application/Features/Events/Commands/CreateEvent/CreateEventCommandHandler.cs @@ -0,0 +1,32 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Events.Commands.CreateEvent; + +public class CreateEventCommandHandler : IRequestHandler> +{ + private readonly IEventRepository _repository; + private readonly IUnitOfWork _unitOfWork; + + public CreateEventCommandHandler( + IEventRepository repository, + IUnitOfWork unitOfWork) + { + _repository = repository; + _unitOfWork = unitOfWork; + } + + public async Task> Handle(CreateEventCommand request, CancellationToken cancellationToken) + { + var @event = Event.Create( + request.Title, + request.Description, + request.Date, + request.Location, + request.OrganizerId); + + await _repository.PostAsync(@event); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(@event.Id); + } +} diff --git a/EventFlow.Application/Features/Events/Commands/DeleteEvent/DeleteEventCommand.cs b/EventFlow.Application/Features/Events/Commands/DeleteEvent/DeleteEventCommand.cs new file mode 100644 index 0000000..da0505f --- /dev/null +++ b/EventFlow.Application/Features/Events/Commands/DeleteEvent/DeleteEventCommand.cs @@ -0,0 +1,5 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Events.Commands.DeleteEvent; + +public record DeleteEventCommand(int Id) : ICommand; diff --git a/EventFlow.Application/Features/Events/Commands/DeleteEvent/DeleteEventCommandHandler.cs b/EventFlow.Application/Features/Events/Commands/DeleteEvent/DeleteEventCommandHandler.cs new file mode 100644 index 0000000..3302909 --- /dev/null +++ b/EventFlow.Application/Features/Events/Commands/DeleteEvent/DeleteEventCommandHandler.cs @@ -0,0 +1,41 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Events.Commands.DeleteEvent; + +public class DeleteEventCommandHandler : IRequestHandler +{ + private readonly IEventRepository _repository; + private readonly IUnitOfWork _unitOfWork; + private readonly ICacheService _cache; + + public DeleteEventCommandHandler( + IEventRepository repository, + IUnitOfWork unitOfWork, + ICacheService cache) + { + _repository = repository; + _unitOfWork = unitOfWork; + _cache = cache; + } + + public async Task Handle(DeleteEventCommand request, CancellationToken cancellationToken) + { + var @event = await _repository.GetEventByIdAsync(request.Id); + + if (@event == null) + return Result.Failure(Error.NotFound("Event.NotFound", $"Event with id {request.Id} not found")); + + @event.MarkAsDeleted(); + + var deleted = await _repository.DeleteAsync(request.Id); + + if (deleted > 0) + { + await _unitOfWork.SaveChangesAsync(cancellationToken); + await _cache.RemoveAsync($"event-{request.Id}"); + return Result.Success(); + } + + return Result.Failure(Error.Failure("Event.DeleteFailed", "Failed to delete event")); + } +} diff --git a/EventFlow.Application/Features/Events/Commands/UpdateEvent/UpdateEventCommand.cs b/EventFlow.Application/Features/Events/Commands/UpdateEvent/UpdateEventCommand.cs new file mode 100644 index 0000000..75b12e6 --- /dev/null +++ b/EventFlow.Application/Features/Events/Commands/UpdateEvent/UpdateEventCommand.cs @@ -0,0 +1,10 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Events.Commands.UpdateEvent; + +public record UpdateEventCommand( + int Id, + string Title, + string? Description, + DateTime Date, + string Location) : ICommand; diff --git a/EventFlow.Application/Features/Events/Commands/UpdateEvent/UpdateEventCommandHandler.cs b/EventFlow.Application/Features/Events/Commands/UpdateEvent/UpdateEventCommandHandler.cs new file mode 100644 index 0000000..a75810b --- /dev/null +++ b/EventFlow.Application/Features/Events/Commands/UpdateEvent/UpdateEventCommandHandler.cs @@ -0,0 +1,41 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Events.Commands.UpdateEvent; + +public class UpdateEventCommandHandler : IRequestHandler +{ + private readonly IEventRepository _repository; + private readonly IUnitOfWork _unitOfWork; + private readonly ICacheService _cache; + + public UpdateEventCommandHandler( + IEventRepository repository, + IUnitOfWork unitOfWork, + ICacheService cache) + { + _repository = repository; + _unitOfWork = unitOfWork; + _cache = cache; + } + + public async Task Handle(UpdateEventCommand request, CancellationToken cancellationToken) + { + var @event = await _repository.GetEventByIdAsync(request.Id); + + if (@event == null) + return Result.Failure(Error.NotFound("Event.NotFound", $"Event with id {request.Id} not found")); + + @event.UpdateDetails( + request.Title, + request.Description, + request.Date, + request.Location); + + await _repository.UpdateAsync(@event); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + await _cache.RemoveAsync($"event-{request.Id}"); + + return Result.Success(); + } +} diff --git a/EventFlow.Application/Features/Events/Queries/GetAllEvents/GetAllEventsQuery.cs b/EventFlow.Application/Features/Events/Queries/GetAllEvents/GetAllEventsQuery.cs new file mode 100644 index 0000000..7404eb9 --- /dev/null +++ b/EventFlow.Application/Features/Events/Queries/GetAllEvents/GetAllEventsQuery.cs @@ -0,0 +1,3 @@ +namespace EventFlow.Application.Features.Events.Queries.GetAllEvents; + +public record GetAllEventsQuery(QueryParameters Parameters) : IQuery>; diff --git a/EventFlow.Application/Features/Events/Queries/GetAllEvents/GetAllEventsQueryHandler.cs b/EventFlow.Application/Features/Events/Queries/GetAllEvents/GetAllEventsQueryHandler.cs new file mode 100644 index 0000000..51a4dd9 --- /dev/null +++ b/EventFlow.Application/Features/Events/Queries/GetAllEvents/GetAllEventsQueryHandler.cs @@ -0,0 +1,27 @@ +namespace EventFlow.Application.Features.Events.Queries.GetAllEvents; + +public class GetAllEventsQueryHandler : IRequestHandler> +{ + private readonly IEventRepository _repository; + private readonly IMapper _mapper; + + public GetAllEventsQueryHandler(IEventRepository repository, IMapper mapper) + { + _repository = repository; + _mapper = mapper; + } + + public async Task> Handle(GetAllEventsQuery request, CancellationToken cancellationToken) + { + var pagedEvents = await _repository.GetAllPagedEventsAsync(request.Parameters); + + var eventDtos = _mapper.Map>(pagedEvents.Items); + + return new PagedResult( + eventDtos, + pagedEvents.PageNumber, + pagedEvents.PageSize, + pagedEvents.TotalCount + ); + } +} diff --git a/EventFlow.Application/Features/Events/Queries/GetEventById/GetEventByIdQuery.cs b/EventFlow.Application/Features/Events/Queries/GetEventById/GetEventByIdQuery.cs new file mode 100644 index 0000000..a992606 --- /dev/null +++ b/EventFlow.Application/Features/Events/Queries/GetEventById/GetEventByIdQuery.cs @@ -0,0 +1,5 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Events.Queries.GetEventById; + +public record GetEventByIdQuery(int Id) : IQuery>; diff --git a/EventFlow.Application/Features/Events/Queries/GetEventById/GetEventByIdQueryHandler.cs b/EventFlow.Application/Features/Events/Queries/GetEventById/GetEventByIdQueryHandler.cs new file mode 100644 index 0000000..8615dbc --- /dev/null +++ b/EventFlow.Application/Features/Events/Queries/GetEventById/GetEventByIdQueryHandler.cs @@ -0,0 +1,40 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Events.Queries.GetEventById; + +public class GetEventByIdQueryHandler : IRequestHandler> +{ + private readonly IEventRepository _repository; + private readonly IMapper _mapper; + private readonly ICacheService _cache; + + public GetEventByIdQueryHandler( + IEventRepository repository, + IMapper mapper, + ICacheService cache) + { + _repository = repository; + _mapper = mapper; + _cache = cache; + } + + public async Task> Handle(GetEventByIdQuery request, CancellationToken cancellationToken) + { + var cacheKey = $"event-{request.Id}"; + + var cached = await _cache.GetAsync(cacheKey); + if (cached != null) + return Result.Success(cached); + + var @event = await _repository.GetEventWithDetailsByIdAsync(request.Id); + + if (@event == null) + return Result.Failure(Error.NotFound("Event.NotFound", $"Event with id {request.Id} not found")); + + var dto = _mapper.Map(@event); + + await _cache.SetAsync(cacheKey, dto, TimeSpan.FromMinutes(10)); + + return Result.Success(dto); + } +} diff --git a/EventFlow.Application/Features/Organizers/Commands/CreateOrganizer/CreateOrganizerCommand.cs b/EventFlow.Application/Features/Organizers/Commands/CreateOrganizer/CreateOrganizerCommand.cs new file mode 100644 index 0000000..c2ba3ca --- /dev/null +++ b/EventFlow.Application/Features/Organizers/Commands/CreateOrganizer/CreateOrganizerCommand.cs @@ -0,0 +1,7 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Organizers.Commands.CreateOrganizer; + +public record CreateOrganizerCommand( + string Name, + string? Email) : ICommand>; diff --git a/EventFlow.Application/Features/Organizers/Commands/CreateOrganizer/CreateOrganizerCommandHandler.cs b/EventFlow.Application/Features/Organizers/Commands/CreateOrganizer/CreateOrganizerCommandHandler.cs new file mode 100644 index 0000000..deafae2 --- /dev/null +++ b/EventFlow.Application/Features/Organizers/Commands/CreateOrganizer/CreateOrganizerCommandHandler.cs @@ -0,0 +1,30 @@ +using EventFlow.Core.Models; +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Organizers.Commands.CreateOrganizer; + +public class CreateOrganizerCommandHandler : IRequestHandler> +{ + private readonly IOrganizerRepository _repository; + private readonly IUnitOfWork _unitOfWork; + + public CreateOrganizerCommandHandler( + IOrganizerRepository repository, + IUnitOfWork unitOfWork) + { + _repository = repository; + _unitOfWork = unitOfWork; + } + + public async Task> Handle(CreateOrganizerCommand request, CancellationToken cancellationToken) + { + var organizer = Organizer.Create(request.Name, request.Email ?? ""); + + await _repository.PostAsync(organizer); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(organizer.Id); + } +} diff --git a/EventFlow.Application/Features/Organizers/Commands/DeleteOrganizer/DeleteOrganizerCommand.cs b/EventFlow.Application/Features/Organizers/Commands/DeleteOrganizer/DeleteOrganizerCommand.cs new file mode 100644 index 0000000..b33d01d --- /dev/null +++ b/EventFlow.Application/Features/Organizers/Commands/DeleteOrganizer/DeleteOrganizerCommand.cs @@ -0,0 +1,5 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Organizers.Commands.DeleteOrganizer; + +public record DeleteOrganizerCommand(int Id) : ICommand; diff --git a/EventFlow.Application/Features/Organizers/Commands/DeleteOrganizer/DeleteOrganizerCommandHandler.cs b/EventFlow.Application/Features/Organizers/Commands/DeleteOrganizer/DeleteOrganizerCommandHandler.cs new file mode 100644 index 0000000..879e08d --- /dev/null +++ b/EventFlow.Application/Features/Organizers/Commands/DeleteOrganizer/DeleteOrganizerCommandHandler.cs @@ -0,0 +1,35 @@ +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Organizers.Commands.DeleteOrganizer; + +public class DeleteOrganizerCommandHandler : IRequestHandler +{ + private readonly IOrganizerRepository _repository; + private readonly IUnitOfWork _unitOfWork; + private readonly ICacheService _cache; + + public DeleteOrganizerCommandHandler( + IOrganizerRepository repository, + IUnitOfWork unitOfWork, + ICacheService cache) + { + _repository = repository; + _unitOfWork = unitOfWork; + _cache = cache; + } + + public async Task Handle(DeleteOrganizerCommand request, CancellationToken cancellationToken) + { + var deleted = await _repository.DeleteAsync(request.Id); + + if (deleted == 0) + return Result.Failure(Error.NotFound("Organizer.NotFound", $"Organizer with id {request.Id} not found")); + + await _unitOfWork.SaveChangesAsync(cancellationToken); + await _cache.RemoveAsync($"organizer-{request.Id}"); + + return Result.Success(); + } +} diff --git a/EventFlow.Application/Features/Organizers/Commands/UpdateOrganizer/UpdateOrganizerCommand.cs b/EventFlow.Application/Features/Organizers/Commands/UpdateOrganizer/UpdateOrganizerCommand.cs new file mode 100644 index 0000000..c1fada0 --- /dev/null +++ b/EventFlow.Application/Features/Organizers/Commands/UpdateOrganizer/UpdateOrganizerCommand.cs @@ -0,0 +1,8 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Organizers.Commands.UpdateOrganizer; + +public record UpdateOrganizerCommand( + int Id, + string Name, + string? Email) : ICommand; diff --git a/EventFlow.Application/Features/Organizers/Commands/UpdateOrganizer/UpdateOrganizerCommandHandler.cs b/EventFlow.Application/Features/Organizers/Commands/UpdateOrganizer/UpdateOrganizerCommandHandler.cs new file mode 100644 index 0000000..10781e5 --- /dev/null +++ b/EventFlow.Application/Features/Organizers/Commands/UpdateOrganizer/UpdateOrganizerCommandHandler.cs @@ -0,0 +1,39 @@ +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Organizers.Commands.UpdateOrganizer; + +public class UpdateOrganizerCommandHandler : IRequestHandler +{ + private readonly IOrganizerRepository _repository; + private readonly IUnitOfWork _unitOfWork; + private readonly ICacheService _cache; + + public UpdateOrganizerCommandHandler( + IOrganizerRepository repository, + IUnitOfWork unitOfWork, + ICacheService cache) + { + _repository = repository; + _unitOfWork = unitOfWork; + _cache = cache; + } + + public async Task Handle(UpdateOrganizerCommand request, CancellationToken cancellationToken) + { + var organizer = await _repository.GetOrganizerByIdAsync(request.Id); + + if (organizer == null) + return Result.Failure(Error.NotFound("Organizer.NotFound", $"Organizer with id {request.Id} not found")); + + organizer.UpdateDetails(request.Name, request.Email ?? ""); + + await _repository.UpdateAsync(organizer); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + await _cache.RemoveAsync($"organizer-{request.Id}"); + + return Result.Success(); + } +} diff --git a/EventFlow.Application/Features/Organizers/Queries/GetAllOrganizers/GetAllOrganizersQuery.cs b/EventFlow.Application/Features/Organizers/Queries/GetAllOrganizers/GetAllOrganizersQuery.cs new file mode 100644 index 0000000..ff86cce --- /dev/null +++ b/EventFlow.Application/Features/Organizers/Queries/GetAllOrganizers/GetAllOrganizersQuery.cs @@ -0,0 +1,6 @@ +using EventFlow.Application.DTOs; +using EventFlow.Core.Models; + +namespace EventFlow.Application.Features.Organizers.Queries.GetAllOrganizers; + +public record GetAllOrganizersQuery(QueryParameters QueryParameters) : IQuery>; diff --git a/EventFlow.Application/Features/Organizers/Queries/GetAllOrganizers/GetAllOrganizersQueryHandler.cs b/EventFlow.Application/Features/Organizers/Queries/GetAllOrganizers/GetAllOrganizersQueryHandler.cs new file mode 100644 index 0000000..7e18665 --- /dev/null +++ b/EventFlow.Application/Features/Organizers/Queries/GetAllOrganizers/GetAllOrganizersQueryHandler.cs @@ -0,0 +1,57 @@ +using AutoMapper; +using EventFlow.Application.Abstractions; +using EventFlow.Application.DTOs; +using EventFlow.Core.Models; +using EventFlow.Core.Repository; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace EventFlow.Application.Features.Organizers.Queries.GetAllOrganizers; + +public class GetAllOrganizersQueryHandler : IRequestHandler> +{ + private readonly IOrganizerRepository _repository; + private readonly IMapper _mapper; + private readonly ICacheService _cache; + private readonly ILogger _logger; + + public GetAllOrganizersQueryHandler( + IOrganizerRepository repository, + IMapper mapper, + ICacheService cache, + ILogger logger) + { + _repository = repository; + _mapper = mapper; + _cache = cache; + _logger = logger; + } + + public async Task> Handle(GetAllOrganizersQuery request, CancellationToken cancellationToken) + { + var cacheKey = $"organizers-page-{request.QueryParameters.PageNumber}-size-{request.QueryParameters.PageSize}"; + + var cached = await _cache.GetAsync>(cacheKey); + if (cached != null) + { + _logger.LogInformation("Cache hit for organizers page {PageNumber}", request.QueryParameters.PageNumber); + return cached; + } + + _logger.LogInformation("Cache miss for organizers page {PageNumber}. Fetching from database...", request.QueryParameters.PageNumber); + + var pagedResult = await _repository.GetAllPagedOrganizersAsync(request.QueryParameters); + var dtos = _mapper.Map>(pagedResult.Items); + + var result = new PagedResult( + dtos, + request.QueryParameters.PageNumber, + request.QueryParameters.PageSize, + pagedResult.TotalCount); + + await _cache.SetAsync(cacheKey, result, TimeSpan.FromMinutes(5)); + _logger.LogInformation("Cached organizers page {PageNumber} for 5 minutes", request.QueryParameters.PageNumber); + + return result; + } +} diff --git a/EventFlow.Application/Features/Organizers/Queries/GetOrganizerById/GetOrganizerByIdQuery.cs b/EventFlow.Application/Features/Organizers/Queries/GetOrganizerById/GetOrganizerByIdQuery.cs new file mode 100644 index 0000000..6439217 --- /dev/null +++ b/EventFlow.Application/Features/Organizers/Queries/GetOrganizerById/GetOrganizerByIdQuery.cs @@ -0,0 +1,6 @@ +using EventFlow.Application.DTOs; +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Organizers.Queries.GetOrganizerById; + +public record GetOrganizerByIdQuery(int Id) : IQuery>; diff --git a/EventFlow.Application/Features/Organizers/Queries/GetOrganizerById/GetOrganizerByIdQueryHandler.cs b/EventFlow.Application/Features/Organizers/Queries/GetOrganizerById/GetOrganizerByIdQueryHandler.cs new file mode 100644 index 0000000..a5120c8 --- /dev/null +++ b/EventFlow.Application/Features/Organizers/Queries/GetOrganizerById/GetOrganizerByIdQueryHandler.cs @@ -0,0 +1,44 @@ +using AutoMapper; +using EventFlow.Application.DTOs; +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Organizers.Queries.GetOrganizerById; + +public class GetOrganizerByIdQueryHandler : IRequestHandler> +{ + private readonly IOrganizerRepository _repository; + private readonly IMapper _mapper; + private readonly ICacheService _cache; + + public GetOrganizerByIdQueryHandler( + IOrganizerRepository repository, + IMapper mapper, + ICacheService cache) + { + _repository = repository; + _mapper = mapper; + _cache = cache; + } + + public async Task> Handle(GetOrganizerByIdQuery request, CancellationToken cancellationToken) + { + string cacheKey = $"organizer-{request.Id}"; + + var cachedData = await _cache.GetAsync(cacheKey); + if (cachedData != null) + return Result.Success(cachedData); + + var organizer = await _repository.GetOrganizerByIdAsync(request.Id); + + if (organizer == null) + return Result.Failure(Error.NotFound("Organizer.NotFound", $"Organizer with id {request.Id} not found")); + + var dto = _mapper.Map(organizer); + + await _cache.SetAsync(cacheKey, dto, TimeSpan.FromMinutes(10)); + + return Result.Success(dto); + } +} diff --git a/EventFlow.Application/Features/Participants/Commands/CreateParticipant/CreateParticipantCommand.cs b/EventFlow.Application/Features/Participants/Commands/CreateParticipant/CreateParticipantCommand.cs new file mode 100644 index 0000000..0b26bb4 --- /dev/null +++ b/EventFlow.Application/Features/Participants/Commands/CreateParticipant/CreateParticipantCommand.cs @@ -0,0 +1,8 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Participants.Commands.CreateParticipant; + +public record CreateParticipantCommand( + string Name, + string? Email, + string? Interests) : ICommand>; diff --git a/EventFlow.Application/Features/Participants/Commands/CreateParticipant/CreateParticipantCommandHandler.cs b/EventFlow.Application/Features/Participants/Commands/CreateParticipant/CreateParticipantCommandHandler.cs new file mode 100644 index 0000000..b4dd39a --- /dev/null +++ b/EventFlow.Application/Features/Participants/Commands/CreateParticipant/CreateParticipantCommandHandler.cs @@ -0,0 +1,33 @@ +using EventFlow.Core.Models; +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Participants.Commands.CreateParticipant; + +public class CreateParticipantCommandHandler : IRequestHandler> +{ + private readonly IParticipantRepository _repository; + private readonly IUnitOfWork _unitOfWork; + + public CreateParticipantCommandHandler( + IParticipantRepository repository, + IUnitOfWork unitOfWork) + { + _repository = repository; + _unitOfWork = unitOfWork; + } + + public async Task> Handle(CreateParticipantCommand request, CancellationToken cancellationToken) + { + var participant = Participant.Create( + request.Name, + request.Email ?? "", + request.Interests ?? ""); + + await _repository.PostAsync(participant); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(participant.Id); + } +} diff --git a/EventFlow.Application/Features/Participants/Commands/DeleteParticipant/DeleteParticipantCommand.cs b/EventFlow.Application/Features/Participants/Commands/DeleteParticipant/DeleteParticipantCommand.cs new file mode 100644 index 0000000..9e079ab --- /dev/null +++ b/EventFlow.Application/Features/Participants/Commands/DeleteParticipant/DeleteParticipantCommand.cs @@ -0,0 +1,5 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Participants.Commands.DeleteParticipant; + +public record DeleteParticipantCommand(int Id) : ICommand; diff --git a/EventFlow.Application/Features/Participants/Commands/DeleteParticipant/DeleteParticipantCommandHandler.cs b/EventFlow.Application/Features/Participants/Commands/DeleteParticipant/DeleteParticipantCommandHandler.cs new file mode 100644 index 0000000..eeef638 --- /dev/null +++ b/EventFlow.Application/Features/Participants/Commands/DeleteParticipant/DeleteParticipantCommandHandler.cs @@ -0,0 +1,35 @@ +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Participants.Commands.DeleteParticipant; + +public class DeleteParticipantCommandHandler : IRequestHandler +{ + private readonly IParticipantRepository _repository; + private readonly IUnitOfWork _unitOfWork; + private readonly ICacheService _cache; + + public DeleteParticipantCommandHandler( + IParticipantRepository repository, + IUnitOfWork unitOfWork, + ICacheService cache) + { + _repository = repository; + _unitOfWork = unitOfWork; + _cache = cache; + } + + public async Task Handle(DeleteParticipantCommand request, CancellationToken cancellationToken) + { + var deleted = await _repository.DeleteAsync(request.Id); + + if (deleted == 0) + return Result.Failure(Error.NotFound("Participant.NotFound", $"Participant with id {request.Id} not found")); + + await _unitOfWork.SaveChangesAsync(cancellationToken); + await _cache.RemoveAsync($"participant-{request.Id}"); + + return Result.Success(); + } +} diff --git a/EventFlow.Application/Features/Participants/Commands/UpdateParticipant/UpdateParticipantCommand.cs b/EventFlow.Application/Features/Participants/Commands/UpdateParticipant/UpdateParticipantCommand.cs new file mode 100644 index 0000000..b34247b --- /dev/null +++ b/EventFlow.Application/Features/Participants/Commands/UpdateParticipant/UpdateParticipantCommand.cs @@ -0,0 +1,9 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Participants.Commands.UpdateParticipant; + +public record UpdateParticipantCommand( + int Id, + string Name, + string? Email, + string? Interests) : ICommand; diff --git a/EventFlow.Application/Features/Participants/Commands/UpdateParticipant/UpdateParticipantCommandHandler.cs b/EventFlow.Application/Features/Participants/Commands/UpdateParticipant/UpdateParticipantCommandHandler.cs new file mode 100644 index 0000000..6238217 --- /dev/null +++ b/EventFlow.Application/Features/Participants/Commands/UpdateParticipant/UpdateParticipantCommandHandler.cs @@ -0,0 +1,42 @@ +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Participants.Commands.UpdateParticipant; + +public class UpdateParticipantCommandHandler : IRequestHandler +{ + private readonly IParticipantRepository _repository; + private readonly IUnitOfWork _unitOfWork; + private readonly ICacheService _cache; + + public UpdateParticipantCommandHandler( + IParticipantRepository repository, + IUnitOfWork unitOfWork, + ICacheService cache) + { + _repository = repository; + _unitOfWork = unitOfWork; + _cache = cache; + } + + public async Task Handle(UpdateParticipantCommand request, CancellationToken cancellationToken) + { + var participant = await _repository.GetParticipantByIdAsync(request.Id); + + if (participant == null) + return Result.Failure(Error.NotFound("Participant.NotFound", $"Participant with id {request.Id} not found")); + + participant.UpdateDetails( + request.Name, + request.Email ?? "", + request.Interests ?? ""); + + await _repository.UpdateAsync(participant); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + await _cache.RemoveAsync($"participant-{request.Id}"); + + return Result.Success(); + } +} diff --git a/EventFlow.Application/Features/Participants/Queries/GetParticipantById/GetParticipantByIdQuery.cs b/EventFlow.Application/Features/Participants/Queries/GetParticipantById/GetParticipantByIdQuery.cs new file mode 100644 index 0000000..673eb6a --- /dev/null +++ b/EventFlow.Application/Features/Participants/Queries/GetParticipantById/GetParticipantByIdQuery.cs @@ -0,0 +1,6 @@ +using EventFlow.Application.DTOs; +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Participants.Queries.GetParticipantById; + +public record GetParticipantByIdQuery(int Id) : IQuery>; diff --git a/EventFlow.Application/Features/Participants/Queries/GetParticipantById/GetParticipantByIdQueryHandler.cs b/EventFlow.Application/Features/Participants/Queries/GetParticipantById/GetParticipantByIdQueryHandler.cs new file mode 100644 index 0000000..843f796 --- /dev/null +++ b/EventFlow.Application/Features/Participants/Queries/GetParticipantById/GetParticipantByIdQueryHandler.cs @@ -0,0 +1,44 @@ +using AutoMapper; +using EventFlow.Application.DTOs; +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Participants.Queries.GetParticipantById; + +public class GetParticipantByIdQueryHandler : IRequestHandler> +{ + private readonly IParticipantRepository _repository; + private readonly IMapper _mapper; + private readonly ICacheService _cache; + + public GetParticipantByIdQueryHandler( + IParticipantRepository repository, + IMapper mapper, + ICacheService cache) + { + _repository = repository; + _mapper = mapper; + _cache = cache; + } + + public async Task> Handle(GetParticipantByIdQuery request, CancellationToken cancellationToken) + { + string cacheKey = $"participant-{request.Id}"; + + var cachedData = await _cache.GetAsync(cacheKey); + if (cachedData != null) + return Result.Success(cachedData); + + var participant = await _repository.GetParticipantByIdAsync(request.Id); + + if (participant == null) + return Result.Failure(Error.NotFound("Participant.NotFound", $"Participant with id {request.Id} not found")); + + var dto = _mapper.Map(participant); + + await _cache.SetAsync(cacheKey, dto, TimeSpan.FromMinutes(10)); + + return Result.Success(dto); + } +} diff --git a/EventFlow.Application/Features/Participants/Queries/GetParticipantsByEventId/GetParticipantsByEventIdQuery.cs b/EventFlow.Application/Features/Participants/Queries/GetParticipantsByEventId/GetParticipantsByEventIdQuery.cs new file mode 100644 index 0000000..f6b1f2f --- /dev/null +++ b/EventFlow.Application/Features/Participants/Queries/GetParticipantsByEventId/GetParticipantsByEventIdQuery.cs @@ -0,0 +1,6 @@ +using EventFlow.Application.DTOs; +using EventFlow.Core.Models; + +namespace EventFlow.Application.Features.Participants.Queries.GetParticipantsByEventId; + +public record GetParticipantsByEventIdQuery(int EventId, QueryParameters QueryParameters) : IQuery>; diff --git a/EventFlow.Application/Features/Participants/Queries/GetParticipantsByEventId/GetParticipantsByEventIdQueryHandler.cs b/EventFlow.Application/Features/Participants/Queries/GetParticipantsByEventId/GetParticipantsByEventIdQueryHandler.cs new file mode 100644 index 0000000..9cb0947 --- /dev/null +++ b/EventFlow.Application/Features/Participants/Queries/GetParticipantsByEventId/GetParticipantsByEventIdQueryHandler.cs @@ -0,0 +1,60 @@ +using AutoMapper; +using EventFlow.Application.Abstractions; +using EventFlow.Application.DTOs; +using EventFlow.Core.Models; +using EventFlow.Core.Repository; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace EventFlow.Application.Features.Participants.Queries.GetParticipantsByEventId; + +public class GetParticipantsByEventIdQueryHandler : IRequestHandler> +{ + private readonly IParticipantRepository _repository; + private readonly IMapper _mapper; + private readonly ICacheService _cache; + private readonly ILogger _logger; + + public GetParticipantsByEventIdQueryHandler( + IParticipantRepository repository, + IMapper mapper, + ICacheService cache, + ILogger logger) + { + _repository = repository; + _mapper = mapper; + _cache = cache; + _logger = logger; + } + + public async Task> Handle(GetParticipantsByEventIdQuery request, CancellationToken cancellationToken) + { + var cacheKey = $"participants-event-{request.EventId}-page-{request.QueryParameters.PageNumber}-size-{request.QueryParameters.PageSize}"; + + var cached = await _cache.GetAsync>(cacheKey); + if (cached != null) + { + _logger.LogInformation("Cache hit for participants of event {EventId} page {PageNumber}", request.EventId, request.QueryParameters.PageNumber); + return cached; + } + + _logger.LogInformation("Cache miss for participants of event {EventId} page {PageNumber}. Fetching from database...", request.EventId, request.QueryParameters.PageNumber); + + var pagedResult = await _repository.GetAllPagedParticipantsByEventIdAsync( + request.EventId, + request.QueryParameters); + + var dtos = _mapper.Map>(pagedResult.Items); + + var result = new PagedResult( + dtos, + request.QueryParameters.PageNumber, + request.QueryParameters.PageSize, + pagedResult.TotalCount); + + await _cache.SetAsync(cacheKey, result, TimeSpan.FromMinutes(3)); + _logger.LogInformation("Cached participants of event {EventId} page {PageNumber} for 3 minutes", request.EventId, request.QueryParameters.PageNumber); + + return result; + } +} diff --git a/EventFlow.Application/Features/Speakers/Commands/CreateSpeaker/CreateSpeakerCommand.cs b/EventFlow.Application/Features/Speakers/Commands/CreateSpeaker/CreateSpeakerCommand.cs new file mode 100644 index 0000000..6554f68 --- /dev/null +++ b/EventFlow.Application/Features/Speakers/Commands/CreateSpeaker/CreateSpeakerCommand.cs @@ -0,0 +1,9 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Speakers.Commands.CreateSpeaker; + +public record CreateSpeakerCommand( + string Name, + string? Email, + string? Biography, + string? ExpertiseArea) : ICommand>; diff --git a/EventFlow.Application/Features/Speakers/Commands/CreateSpeaker/CreateSpeakerCommandHandler.cs b/EventFlow.Application/Features/Speakers/Commands/CreateSpeaker/CreateSpeakerCommandHandler.cs new file mode 100644 index 0000000..6ec4c48 --- /dev/null +++ b/EventFlow.Application/Features/Speakers/Commands/CreateSpeaker/CreateSpeakerCommandHandler.cs @@ -0,0 +1,34 @@ +using EventFlow.Core.Models; +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Speakers.Commands.CreateSpeaker; + +public class CreateSpeakerCommandHandler : IRequestHandler> +{ + private readonly ISpeakerRepository _repository; + private readonly IUnitOfWork _unitOfWork; + + public CreateSpeakerCommandHandler( + ISpeakerRepository repository, + IUnitOfWork unitOfWork) + { + _repository = repository; + _unitOfWork = unitOfWork; + } + + public async Task> Handle(CreateSpeakerCommand request, CancellationToken cancellationToken) + { + var speaker = Speaker.Create( + request.Name, + request.Email ?? "", + request.Biography ?? "", + request.ExpertiseArea ?? ""); + + await _repository.PostAsync(speaker); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + return Result.Success(speaker.Id); + } +} diff --git a/EventFlow.Application/Features/Speakers/Commands/DeleteSpeaker/DeleteSpeakerCommand.cs b/EventFlow.Application/Features/Speakers/Commands/DeleteSpeaker/DeleteSpeakerCommand.cs new file mode 100644 index 0000000..1efe8e1 --- /dev/null +++ b/EventFlow.Application/Features/Speakers/Commands/DeleteSpeaker/DeleteSpeakerCommand.cs @@ -0,0 +1,5 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Speakers.Commands.DeleteSpeaker; + +public record DeleteSpeakerCommand(int Id) : ICommand; diff --git a/EventFlow.Application/Features/Speakers/Commands/DeleteSpeaker/DeleteSpeakerCommandHandler.cs b/EventFlow.Application/Features/Speakers/Commands/DeleteSpeaker/DeleteSpeakerCommandHandler.cs new file mode 100644 index 0000000..1af8292 --- /dev/null +++ b/EventFlow.Application/Features/Speakers/Commands/DeleteSpeaker/DeleteSpeakerCommandHandler.cs @@ -0,0 +1,35 @@ +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Speakers.Commands.DeleteSpeaker; + +public class DeleteSpeakerCommandHandler : IRequestHandler +{ + private readonly ISpeakerRepository _repository; + private readonly IUnitOfWork _unitOfWork; + private readonly ICacheService _cache; + + public DeleteSpeakerCommandHandler( + ISpeakerRepository repository, + IUnitOfWork unitOfWork, + ICacheService cache) + { + _repository = repository; + _unitOfWork = unitOfWork; + _cache = cache; + } + + public async Task Handle(DeleteSpeakerCommand request, CancellationToken cancellationToken) + { + var deleted = await _repository.DeleteAsync(request.Id); + + if (deleted == 0) + return Result.Failure(Error.NotFound("Speaker.NotFound", $"Speaker with id {request.Id} not found")); + + await _unitOfWork.SaveChangesAsync(cancellationToken); + await _cache.RemoveAsync($"speaker-{request.Id}"); + + return Result.Success(); + } +} diff --git a/EventFlow.Application/Features/Speakers/Commands/UpdateSpeaker/UpdateSpeakerCommand.cs b/EventFlow.Application/Features/Speakers/Commands/UpdateSpeaker/UpdateSpeakerCommand.cs new file mode 100644 index 0000000..98e474a --- /dev/null +++ b/EventFlow.Application/Features/Speakers/Commands/UpdateSpeaker/UpdateSpeakerCommand.cs @@ -0,0 +1,10 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Speakers.Commands.UpdateSpeaker; + +public record UpdateSpeakerCommand( + int Id, + string Name, + string? Email, + string? Biography, + string? ExpertiseArea) : ICommand; diff --git a/EventFlow.Application/Features/Speakers/Commands/UpdateSpeaker/UpdateSpeakerCommandHandler.cs b/EventFlow.Application/Features/Speakers/Commands/UpdateSpeaker/UpdateSpeakerCommandHandler.cs new file mode 100644 index 0000000..42c50a0 --- /dev/null +++ b/EventFlow.Application/Features/Speakers/Commands/UpdateSpeaker/UpdateSpeakerCommandHandler.cs @@ -0,0 +1,43 @@ +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Speakers.Commands.UpdateSpeaker; + +public class UpdateSpeakerCommandHandler : IRequestHandler +{ + private readonly ISpeakerRepository _repository; + private readonly IUnitOfWork _unitOfWork; + private readonly ICacheService _cache; + + public UpdateSpeakerCommandHandler( + ISpeakerRepository repository, + IUnitOfWork unitOfWork, + ICacheService cache) + { + _repository = repository; + _unitOfWork = unitOfWork; + _cache = cache; + } + + public async Task Handle(UpdateSpeakerCommand request, CancellationToken cancellationToken) + { + var speaker = await _repository.GetSpeakerByIdAsync(request.Id); + + if (speaker == null) + return Result.Failure(Error.NotFound("Speaker.NotFound", $"Speaker with id {request.Id} not found")); + + speaker.UpdateDetails( + request.Name, + request.Email ?? "", + request.Biography ?? "", + request.ExpertiseArea ?? ""); + + await _repository.UpdateAsync(speaker); + await _unitOfWork.SaveChangesAsync(cancellationToken); + + await _cache.RemoveAsync($"speaker-{request.Id}"); + + return Result.Success(); + } +} diff --git a/EventFlow.Application/Features/Speakers/Queries/GetAllSpeakers/GetAllSpeakersQuery.cs b/EventFlow.Application/Features/Speakers/Queries/GetAllSpeakers/GetAllSpeakersQuery.cs new file mode 100644 index 0000000..7f58d05 --- /dev/null +++ b/EventFlow.Application/Features/Speakers/Queries/GetAllSpeakers/GetAllSpeakersQuery.cs @@ -0,0 +1,6 @@ +using EventFlow.Application.DTOs; +using EventFlow.Core.Models; + +namespace EventFlow.Application.Features.Speakers.Queries.GetAllSpeakers; + +public record GetAllSpeakersQuery(QueryParameters QueryParameters) : IQuery>; diff --git a/EventFlow.Application/Features/Speakers/Queries/GetAllSpeakers/GetAllSpeakersQueryHandler.cs b/EventFlow.Application/Features/Speakers/Queries/GetAllSpeakers/GetAllSpeakersQueryHandler.cs new file mode 100644 index 0000000..eaeae6c --- /dev/null +++ b/EventFlow.Application/Features/Speakers/Queries/GetAllSpeakers/GetAllSpeakersQueryHandler.cs @@ -0,0 +1,57 @@ +using AutoMapper; +using EventFlow.Application.Abstractions; +using EventFlow.Application.DTOs; +using EventFlow.Core.Models; +using EventFlow.Core.Repository; +using MediatR; +using Microsoft.Extensions.Logging; + +namespace EventFlow.Application.Features.Speakers.Queries.GetAllSpeakers; + +public class GetAllSpeakersQueryHandler : IRequestHandler> +{ + private readonly ISpeakerRepository _repository; + private readonly IMapper _mapper; + private readonly ICacheService _cache; + private readonly ILogger _logger; + + public GetAllSpeakersQueryHandler( + ISpeakerRepository repository, + IMapper mapper, + ICacheService cache, + ILogger logger) + { + _repository = repository; + _mapper = mapper; + _cache = cache; + _logger = logger; + } + + public async Task> Handle(GetAllSpeakersQuery request, CancellationToken cancellationToken) + { + var cacheKey = $"speakers-page-{request.QueryParameters.PageNumber}-size-{request.QueryParameters.PageSize}"; + + var cached = await _cache.GetAsync>(cacheKey); + if (cached != null) + { + _logger.LogInformation("Cache hit for speakers page {PageNumber}", request.QueryParameters.PageNumber); + return cached; + } + + _logger.LogInformation("Cache miss for speakers page {PageNumber}. Fetching from database...", request.QueryParameters.PageNumber); + + var pagedResult = await _repository.GetAllPagedSpeakersAsync(request.QueryParameters); + var dtos = _mapper.Map>(pagedResult.Items); + + var result = new PagedResult( + dtos, + request.QueryParameters.PageNumber, + request.QueryParameters.PageSize, + pagedResult.TotalCount); + + await _cache.SetAsync(cacheKey, result, TimeSpan.FromMinutes(5)); + _logger.LogInformation("Cached speakers page {PageNumber} for 5 minutes", request.QueryParameters.PageNumber); + + return result; + } +} diff --git a/EventFlow.Application/Features/Speakers/Queries/GetSpeakerById/GetSpeakerByIdQuery.cs b/EventFlow.Application/Features/Speakers/Queries/GetSpeakerById/GetSpeakerByIdQuery.cs new file mode 100644 index 0000000..5e48d0b --- /dev/null +++ b/EventFlow.Application/Features/Speakers/Queries/GetSpeakerById/GetSpeakerByIdQuery.cs @@ -0,0 +1,6 @@ +using EventFlow.Application.DTOs; +using EventFlow.Core.Primitives; + +namespace EventFlow.Application.Features.Speakers.Queries.GetSpeakerById; + +public record GetSpeakerByIdQuery(int Id) : IQuery>; diff --git a/EventFlow.Application/Features/Speakers/Queries/GetSpeakerById/GetSpeakerByIdQueryHandler.cs b/EventFlow.Application/Features/Speakers/Queries/GetSpeakerById/GetSpeakerByIdQueryHandler.cs new file mode 100644 index 0000000..0e3fbae --- /dev/null +++ b/EventFlow.Application/Features/Speakers/Queries/GetSpeakerById/GetSpeakerByIdQueryHandler.cs @@ -0,0 +1,44 @@ +using AutoMapper; +using EventFlow.Application.DTOs; +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; +using MediatR; + +namespace EventFlow.Application.Features.Speakers.Queries.GetSpeakerById; + +public class GetSpeakerByIdQueryHandler : IRequestHandler> +{ + private readonly ISpeakerRepository _repository; + private readonly IMapper _mapper; + private readonly ICacheService _cache; + + public GetSpeakerByIdQueryHandler( + ISpeakerRepository repository, + IMapper mapper, + ICacheService cache) + { + _repository = repository; + _mapper = mapper; + _cache = cache; + } + + public async Task> Handle(GetSpeakerByIdQuery request, CancellationToken cancellationToken) + { + string cacheKey = $"speaker-{request.Id}"; + + var cachedData = await _cache.GetAsync(cacheKey); + if (cachedData != null) + return Result.Success(cachedData); + + var speaker = await _repository.GetSpeakerByIdAsync(request.Id); + + if (speaker == null) + return Result.Failure(Error.NotFound("Speaker.NotFound", $"Speaker with id {request.Id} not found")); + + var dto = _mapper.Map(speaker); + + await _cache.SetAsync(cacheKey, dto, TimeSpan.FromMinutes(10)); + + return Result.Success(dto); + } +} diff --git a/EventFlow.Application/GlobalUsing.cs b/EventFlow.Application/GlobalUsing.cs index 7e8b7d2..601b85e 100644 --- a/EventFlow.Application/GlobalUsing.cs +++ b/EventFlow.Application/GlobalUsing.cs @@ -1,6 +1,10 @@ global using AutoMapper; -global using EventFlow.Core.Commands; +global using EventFlow.Application.Abstractions; +global using EventFlow.Application.Behaviors; +global using EventFlow.Application.Commands; +global using EventFlow.Application.DTOs; +global using EventFlow.Application.Services; global using EventFlow.Core.Models; -global using EventFlow.Core.Repository.Interfaces; -global using EventFlow.Core.Services.Interfaces; +global using EventFlow.Core.Repository; global using FluentValidation; +global using MediatR; diff --git a/EventFlow.Application/Services/AuthService.cs b/EventFlow.Application/Services/AuthService.cs index d41de19..f179fd4 100644 --- a/EventFlow.Application/Services/AuthService.cs +++ b/EventFlow.Application/Services/AuthService.cs @@ -1,26 +1,21 @@ -using EventFlow.Core.Models.DTOs; -using Microsoft.Extensions.Configuration; -using Microsoft.IdentityModel.Tokens; -using System.IdentityModel.Tokens.Jwt; -using System.Security.Claims; -using System.Text; - +using System.Security.Claims; namespace EventFlow.Application.Services; -public class AuthService(IUserRepository userRepository, IConfiguration configuration) : IAuthService +public class AuthService( + IUserRepository userRepository, + IPasswordHasher passwordHasher, + IJwtTokenService jwtTokenService) : IAuthService { public async Task RegisterAsync(RegisterUserCommand command) { if (await userRepository.ExistsByEmailAsync(command.Email)) return null; - var user = new User - { - Username = command.Username, - Email = command.Email, - PasswordHash = BCrypt.Net.BCrypt.HashPassword(command.Password) - }; + var user = User.Create( + command.Username, + command.Email, + passwordHasher.HashPassword(command.Password)); await userRepository.AddAsync(user); @@ -36,31 +31,10 @@ public class AuthService(IUserRepository userRepository, IConfiguration configur { var user = await userRepository.GetByEmailAsync(command.Email); - if (user == null || !BCrypt.Net.BCrypt.Verify(command.Password, user!.PasswordHash)) + if (user == null || !passwordHasher.VerifyPassword(command.Password, user!.PasswordHash)) return null; - var tokenHandler = new JwtSecurityTokenHandler(); - var key = Encoding.ASCII.GetBytes(configuration["Jwt:Key"]!); - - var tokenDescriptor = new SecurityTokenDescriptor - { - Subject = new ClaimsIdentity(new List - { - new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()), - new Claim(ClaimTypes.Name, user.Username), - new Claim(ClaimTypes.Email, user.Email) - }), - Expires = DateTime.UtcNow.AddHours(4), - Issuer = configuration["Jwt:Issuer"], - Audience = configuration["Jwt:Audience"], - SigningCredentials = new SigningCredentials( - new SymmetricSecurityKey(key), - SecurityAlgorithms.HmacSha256Signature - ) - }; - - var token = tokenHandler.CreateToken(tokenDescriptor); - return tokenHandler.WriteToken(token); + return jwtTokenService.GenerateToken(user.Id, user.Username, user.Email); } public async Task GetAuthenticatedUserAsync(ClaimsPrincipal userClaims) @@ -83,18 +57,18 @@ public class AuthService(IUserRepository userRepository, IConfiguration configur }; } - public async Task UpdatePasswordAsync(ClaimsPrincipal userClaims, UserPasswordUpdateDTO dto) + public async Task UpdatePasswordAsync(ClaimsPrincipal user, UserPasswordUpdateDto dto) { - var email = userClaims.FindFirstValue(ClaimTypes.Email); + var email = user.FindFirstValue(ClaimTypes.Email); if (string.IsNullOrEmpty(email)) return false; var entity = await userRepository.GetByEmailAsync(email); if (entity == null) return false; - if (!BCrypt.Net.BCrypt.Verify(dto.CurrentPassword, entity.PasswordHash)) + if (!passwordHasher.VerifyPassword(dto.CurrentPassword, entity.PasswordHash)) return false; - entity.PasswordHash = BCrypt.Net.BCrypt.HashPassword(dto.NewPassword); + entity.UpdatePassword(passwordHasher.HashPassword(dto.NewPassword)); await userRepository.UpdateAsync(entity); return true; diff --git a/EventFlow.Application/Services/CacheService.cs b/EventFlow.Application/Services/CacheService.cs new file mode 100644 index 0000000..42ec341 --- /dev/null +++ b/EventFlow.Application/Services/CacheService.cs @@ -0,0 +1,40 @@ +using Microsoft.Extensions.Caching.Distributed; +using System.Text.Json; + +namespace EventFlow.Application.Services; + +public class CacheService(IDistributedCache distributedCache) : ICacheService +{ + private readonly DistributedCacheEntryOptions _defaultOptions = new() + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) + }; + + public async Task GetAsync(string key) + { + var cachedValue = await distributedCache.GetStringAsync(key); + + if (string.IsNullOrEmpty(cachedValue)) + { + return default; + } + + return JsonSerializer.Deserialize(cachedValue); + } + + public async Task SetAsync(string key, T value, TimeSpan? expirationTime = null) + { + var options = expirationTime.HasValue + ? new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = expirationTime.Value } + : _defaultOptions; + + var jsonValue = JsonSerializer.Serialize(value); + + await distributedCache.SetStringAsync(key, jsonValue, options); + } + + public async Task RemoveAsync(string key) + { + await distributedCache.RemoveAsync(key); + } +} diff --git a/EventFlow.Application/Services/EventService.cs b/EventFlow.Application/Services/EventService.cs deleted file mode 100644 index 2bba832..0000000 --- a/EventFlow.Application/Services/EventService.cs +++ /dev/null @@ -1,105 +0,0 @@ -using EventFlow.Core.Models.DTOs; -using Microsoft.Extensions.Caching.Distributed; -using System.Reflection.Metadata.Ecma335; -using System.Text.Json; - -namespace EventFlow.Application.Services; - -public class EventService(IEventRepository repository, IMapper mapper, IDistributedCache cache) : IEventService -{ - public async Task GetByIdAsync(int id) - { - string cacheKey = $"event-{id}"; - - var cachedData = await cache.GetStringAsync(cacheKey); - - if (!string.IsNullOrEmpty(cachedData)) - { - return JsonSerializer.Deserialize(cachedData)!; - } - - var entity = await repository.GetEventWithDetailsByIdAsync(id); - - if (entity == null) - return null; - - var dto = mapper.Map(entity); - - await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto), - new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) - }); - - return dto; - } - - public async Task> GetAllEventsAsync() - { - var events = await repository.GetAllEventsAsync(); - return mapper.Map>(events); - } - - public async Task> GetAllPagedEventsAsync(QueryParameters queryParameters) - { - var pagedEvents = await repository.GetAllPagedEventsAsync(queryParameters); - - var eventDtos = mapper.Map>(pagedEvents.Items); - - return new PagedResult( - eventDtos, - pagedEvents.PageNumber, - pagedEvents.PageSize, - pagedEvents.TotalCount - ); - } - - public async Task CreateAsync(EventCommand command) - { - var newEvent = new Event - { - Title = command.Title, - Description = command.Description, - Date = command.Date, - Location = command.Location, - OrganizerId = command.OrganizerId - }; - - var createdEvent = await repository.PostAsync(newEvent); - if (createdEvent == null) - return null; - - var eventWithDetails = await repository.GetEventWithDetailsByIdAsync(createdEvent.Id); - return eventWithDetails; - } - - public async Task UpdateAsync(int id, EventCommand command) - { - var existing = await repository.GetEventByIdAsync(id); - if (existing == null) return null; - - existing.Title = command.Title; - existing.Description = command.Description; - existing.Date = command.Date; - existing.Location = command.Location; - existing.OrganizerId = command.OrganizerId; - - var updated = await repository.UpdateAsync(existing); - - await cache.RemoveAsync($"event-{id}"); - - return updated is null ? null : mapper.Map(updated); - } - - public async Task DeleteAsync(int id) - { - var deleted = await repository.DeleteAsync(id); - - if (deleted > 0) - { - await cache.RemoveAsync($"event-{id}"); - } - - return deleted > 0; - } -} diff --git a/EventFlow.Core/Services/Interfaces/IAuthService.cs b/EventFlow.Application/Services/IAuthService.cs similarity index 74% rename from EventFlow.Core/Services/Interfaces/IAuthService.cs rename to EventFlow.Application/Services/IAuthService.cs index 33ff604..11f088a 100644 --- a/EventFlow.Core/Services/Interfaces/IAuthService.cs +++ b/EventFlow.Application/Services/IAuthService.cs @@ -1,11 +1,11 @@ -using System.Security.Claims; +using System.Security.Claims; -namespace EventFlow.Core.Services.Interfaces; +namespace EventFlow.Application.Services; public interface IAuthService { Task RegisterAsync(RegisterUserCommand command); Task LoginAsync(LoginUserCommand command); Task GetAuthenticatedUserAsync(ClaimsPrincipal user); - Task UpdatePasswordAsync(ClaimsPrincipal user, UserPasswordUpdateDTO dto); + Task UpdatePasswordAsync(ClaimsPrincipal user, UserPasswordUpdateDto dto); } diff --git a/EventFlow.Application/Services/ICacheService.cs b/EventFlow.Application/Services/ICacheService.cs new file mode 100644 index 0000000..eb31e97 --- /dev/null +++ b/EventFlow.Application/Services/ICacheService.cs @@ -0,0 +1,8 @@ +namespace EventFlow.Application.Services; + +public interface ICacheService +{ + Task GetAsync(string key); + Task SetAsync(string key, T value, TimeSpan? expirationTime = null); + Task RemoveAsync(string key); +} diff --git a/EventFlow.Application/Services/IRecommendationService.cs b/EventFlow.Application/Services/IRecommendationService.cs new file mode 100644 index 0000000..1318e0d --- /dev/null +++ b/EventFlow.Application/Services/IRecommendationService.cs @@ -0,0 +1,7 @@ +namespace EventFlow.Application.Services; + +public interface IRecommendationService +{ + Task> GetRecommendedEventsAsync(int participantId); + Task> GetRecommendedConnectionsAsync(int participantId); +} diff --git a/EventFlow.Application/Services/OrganizerService.cs b/EventFlow.Application/Services/OrganizerService.cs deleted file mode 100644 index e11cc83..0000000 --- a/EventFlow.Application/Services/OrganizerService.cs +++ /dev/null @@ -1,110 +0,0 @@ -using EventFlow.Core.Models.DTOs; -using Microsoft.Extensions.Caching.Distributed; -using System.Text.Json; - -namespace EventFlow.Application.Services; - -public class OrganizerService(IOrganizerRepository repository, IEventRepository eventRepository, - IMapper mapper, IDistributedCache cache) : IOrganizerService -{ - public async Task GetByIdAsync(int id) - { - string cacheKey = $"organizer-{id}"; - - var cachedData = await cache.GetStringAsync(cacheKey); - - if (!string.IsNullOrEmpty(cachedData)) - { - return JsonSerializer.Deserialize(cachedData)!; - } - - var entity = await repository.GetOrganizerByIdAsync(id); - - if (entity == null) - return null; - - var dto = mapper.Map(entity); - - await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto), - new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) - }); - - return dto; - } - - public async Task> GetAllOrganizersAsync() - { - var organizers = await repository.GetAllOrganizersAsync(); - return mapper.Map>(organizers); - } - - public async Task> GetAllPagedOrganizersAsync(QueryParameters queryParameters) - { - var pagedOrganizers = await repository.GetAllPagedOrganizersAsync(queryParameters); - - var organizerDtos = mapper.Map>(pagedOrganizers.Items); - - return new PagedResult( - organizerDtos, - pagedOrganizers.PageNumber, - pagedOrganizers.PageSize, - pagedOrganizers.TotalCount - ); - } - - public async Task CreateAsync(OrganizerCommand command) - { - var organizer = new Organizer - { - Name = command.Name, - Email = command.Email - }; - - return await repository.PostAsync(organizer); - } - - public async Task RegisterToEventAsync(int eventId, int organizerId) - { - var organizer = await repository.GetOrganizerByIdAsync(organizerId); - var evento = await eventRepository.GetEventByIdAsync(eventId); - - if (organizer == null || evento == null) - return false; - - evento.OrganizerId = organizerId; - evento.Organizer = organizer; - - await eventRepository.UpdateAsync(evento); - return true; - } - - public async Task UpdateAsync(int id, OrganizerCommand command) - { - var existing = await repository.GetOrganizerByIdAsync(id); - if (existing == null) - return null; - - existing.Name = command.Name; - existing.Email = command.Email; - - var updated = await repository.UpdateAsync(existing); - - await cache.RemoveAsync($"organizer-{id}"); - - return updated is null ? null : mapper.Map(updated); - } - - public async Task DeleteAsync(int id) - { - var deleted = await repository.DeleteAsync(id); - - if (deleted > 0) - { - await cache.RemoveAsync($"organizer-{id}"); - } - - return deleted > 0; - } -} diff --git a/EventFlow.Application/Services/ParticipantService.cs b/EventFlow.Application/Services/ParticipantService.cs deleted file mode 100644 index 68a5aba..0000000 --- a/EventFlow.Application/Services/ParticipantService.cs +++ /dev/null @@ -1,112 +0,0 @@ -using EventFlow.Core.Models.DTOs; -using Microsoft.Extensions.Caching.Distributed; -using System.Text.Json; - -namespace EventFlow.Application.Services; - -public class ParticipantService(IParticipantRepository repository, IEventRepository eventRepository, - IMapper mapper, IDistributedCache cache) : IParticipantService -{ - public async Task GetByIdAsync(int id) - { - string cacheKey = $"participant-{id}"; - - var cachedData = await cache.GetStringAsync(cacheKey); - - if (!string.IsNullOrEmpty(cachedData)) - { - return JsonSerializer.Deserialize(cachedData)!; - } - - var entity = await repository.GetParticipantByIdAsync(id); - - if (entity == null) - return null; - - var dto = mapper.Map(entity); - - await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto), - new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) - }); - - return dto; - } - - public async Task> GetAllPagedParticipantsByEventIdAsync(int eventId, QueryParameters queryParameters) - { - var pagedParticipants = await repository.GetAllPagedParticipantsByEventIdAsync(eventId, queryParameters); - - var participantDtos = mapper.Map>(pagedParticipants.Items); - - return new PagedResult( - participantDtos, - pagedParticipants.PageNumber, - pagedParticipants.PageSize, - pagedParticipants.TotalCount - ); - } - - public async Task CreateAsync(ParticipantCommand command) - { - var participant = new Participant - { - Name = command.Name, - Email = command.Email - }; - - return await repository.PostAsync(participant); - } - - public async Task RegisterToEventAsync(int eventId, int participantId) - { - var participant = await repository.GetParticipantByIdAsync(participantId); - var evento = await eventRepository.GetEventByIdAsync(eventId); - - if (evento == null || participant == null) - return false; - - participant.Events!.Add(evento); - await repository.UpdateAsync(participant); - - await cache.RemoveAsync($"participant-{participantId}"); - - return true; - } - - public async Task UpdateAsync(int id, ParticipantCommand command) - { - var existing = await repository.GetParticipantByIdAsync(id); - - if (existing == null) - return null; - - existing.Name = command.Name; - existing.Email = command.Email; - - var updated = await repository.UpdateAsync(existing); - - await cache.RemoveAsync($"participant-{id}"); - - return updated is null ? null : mapper.Map(updated); - } - - public async Task DeleteAsync(int id) - { - var deleted = await repository.DeleteAsync(id); - - if (deleted > 0) - { - await cache.RemoveAsync($"participant-{id}"); - } - - return deleted > 0; - } - - public async Task> GetAllParticipantsWithEventsAsync() - { - var allParticipants = await repository.GetAllParticipantsWithEventsAsync(); - return mapper.Map>(allParticipants); - } -} diff --git a/EventFlow.Application/Services/RecommendationService.cs b/EventFlow.Application/Services/RecommendationService.cs new file mode 100644 index 0000000..9a20902 --- /dev/null +++ b/EventFlow.Application/Services/RecommendationService.cs @@ -0,0 +1,65 @@ +using EventFlow.Application.DTOs; + +namespace EventFlow.Application.Services; + +public class RecommendationService(IParticipantRepository participantRepository, IMapper mapper) : IRecommendationService +{ + private readonly IParticipantRepository _participantRepository = participantRepository; + private readonly IMapper _mapper = mapper; + + public async Task> GetRecommendedEventsAsync(int participantId) + { + var participantWithEvents = await _participantRepository.GetParticipantByIdAsync(participantId); + var participantEventsIds = (participantWithEvents?.Events != null) + ? participantWithEvents.Events.Select(e => e.Id).ToHashSet() + : new HashSet(); + + if (participantEventsIds.Count == 0) + { + return Enumerable.Empty(); + } + + var allParticipants = await _participantRepository.GetAllParticipantsWithEventsAsync(); + var similarParticipants = allParticipants + .Where(p => p.Id != participantId && p.Events != null && p.Events.Any(e => participantEventsIds.Contains(e.Id))) + .ToList(); + + var recommendedEvents = similarParticipants + .SelectMany(p => p.Events ?? Enumerable.Empty()) + .Where(e => !participantEventsIds.Contains(e.Id)) + .GroupBy(e => e.Id) + .OrderByDescending(g => g.Count()) + .Select(g => g.First()) + .Take(10); + + return _mapper.Map>(recommendedEvents); + } + + public async Task> GetRecommendedConnectionsAsync(int participantId) + { + var participantWithEvents = await _participantRepository.GetParticipantByIdAsync(participantId); + var participantEventsIds = (participantWithEvents?.Events != null) + ? participantWithEvents.Events.Select(e => e.Id).ToHashSet() + : new HashSet(); + + if (participantEventsIds.Count == 0) + { + return Enumerable.Empty(); + } + + var allParticipants = await _participantRepository.GetAllParticipantsWithEventsAsync(); + var recommendedParticipants = allParticipants + .Where(p => p.Id != participantId) + .Select(p => new + { + Participant = p, + SharedEventsCount = (p.Events ?? Enumerable.Empty()).Count(e => participantEventsIds.Contains(e.Id)) + }) + .Where(x => x.SharedEventsCount > 0) + .OrderByDescending(x => x.SharedEventsCount) + .Select(x => x.Participant) + .Take(10); + + return _mapper.Map>(recommendedParticipants); + } +} diff --git a/EventFlow.Application/Services/SpeakerService.cs b/EventFlow.Application/Services/SpeakerService.cs deleted file mode 100644 index 604944b..0000000 --- a/EventFlow.Application/Services/SpeakerService.cs +++ /dev/null @@ -1,120 +0,0 @@ -using EventFlow.Core.Models.DTOs; -using Microsoft.Extensions.Caching.Distributed; -using System.Text.Json; - -namespace EventFlow.Application.Services; - -public class SpeakerService(ISpeakerRepository repository, IEventRepository eventRepository, - IMapper mapper, IDistributedCache cache) : ISpeakerService -{ - public async Task GetByIdAsync(int id) - { - string cacheKey = $"speaker-{id}"; - - var cachedData = await cache.GetStringAsync(cacheKey); - - if (!string.IsNullOrEmpty(cachedData)) - { - return JsonSerializer.Deserialize(cachedData)!; - } - - var entity = await repository.GetSpeakerByIdAsync(id); - - if (entity == null) - return null; - - var dto = mapper.Map(entity); - - await cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(dto), - new DistributedCacheEntryOptions - { - AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(10) - }); - - return dto; - } - - public async Task> GetAllSpeakersAsync() - { - var speakers = await repository.GetAllSpeakersAsync(); - return mapper.Map>(speakers); - } - - public async Task> GetAllPagedSpeakersAsync(QueryParameters queryParameters) - { - var pagedSpeakers = await repository.GetAllPagedSpeakersAsync(queryParameters); - - var speakerDtos = mapper.Map>(pagedSpeakers.Items); - - return new PagedResult( - speakerDtos, - pagedSpeakers.PageNumber, - pagedSpeakers.PageSize, - pagedSpeakers.TotalCount - ); - } - - public async Task CreateAsync(SpeakerCommand command) - { - var speaker = new Speaker - { - Name = command.Name, - Email = command.Email, - Biography = command.Biography - }; - - return await repository.PostAsync(speaker); - } - public async Task RegisterToEventAsync(int eventId, int speakerId) - { - var speaker = await repository.GetSpeakerByIdAsync(speakerId); - var evento = await eventRepository.GetEventByIdAsync(eventId); - - if (speaker == null || evento == null) - return false; - - var alreadyLinked = speaker.SpeakerEvents.Any(se => se.EventId == eventId); - if (alreadyLinked) - return true; - var speakerEvent = new SpeakerEvent - { - SpeakerId = speakerId, - EventId = eventId, - RegisteredAt = DateTime.UtcNow - }; - - await repository.AddSpeakerEventAsync(speakerEvent); - - await cache.RemoveAsync($"speaker-{speakerId}"); - - return true; - } - - public async Task UpdateAsync(int id, SpeakerCommand command) - { - var existing = await repository.GetSpeakerByIdAsync(id); - if (existing == null) return null; - - existing.Name = command.Name; - existing.Email = command.Email; - existing.Biography = command.Biography; - - var updated = await repository.UpdateAsync(existing); - - await cache.RemoveAsync($"speaker-{id}"); - - return updated is null ? null : mapper.Map(updated); - } - - public async Task DeleteAsync(int id) - { - var deleted = await repository.DeleteAsync(id); - - if (deleted > 0) - { - await cache.RemoveAsync($"speaker-{id}"); - } - - return deleted > 0; - } -} diff --git a/EventFlow.Core/Commands/OrganizerCommand.cs b/EventFlow.Core/Commands/OrganizerCommand.cs deleted file mode 100644 index 2f84272..0000000 --- a/EventFlow.Core/Commands/OrganizerCommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace EventFlow.Core.Commands; - -public class OrganizerCommand -{ - public string Name { get; set; } - public string Email { get; set; } -} diff --git a/EventFlow.Core/Commands/ParticipantCommand.cs b/EventFlow.Core/Commands/ParticipantCommand.cs deleted file mode 100644 index 6785fc3..0000000 --- a/EventFlow.Core/Commands/ParticipantCommand.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace EventFlow.Core.Commands; - -public class ParticipantCommand -{ - public string Name { get; set; } - public string Email { get; set; } -} diff --git a/EventFlow.Core/Commands/SpeakerCommand.cs b/EventFlow.Core/Commands/SpeakerCommand.cs deleted file mode 100644 index 7006d77..0000000 --- a/EventFlow.Core/Commands/SpeakerCommand.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Text.Json.Serialization; - -namespace EventFlow.Core.Commands; - -public class SpeakerCommand -{ - public string Name { get; set; } - public string Email { get; set; } - public string? Biography { get; set; } - - [JsonIgnore] - public int EventId { get; set; } -} diff --git a/EventFlow.Core/Events/EventCreatedDomainEvent.cs b/EventFlow.Core/Events/EventCreatedDomainEvent.cs new file mode 100644 index 0000000..168d5dd --- /dev/null +++ b/EventFlow.Core/Events/EventCreatedDomainEvent.cs @@ -0,0 +1,17 @@ +namespace EventFlow.Core.Events; + +public class EventCreatedDomainEvent : DomainEvent +{ + public int EventId { get; } + public string Title { get; } + public DateTime Date { get; } + public int OrganizerId { get; } + + public EventCreatedDomainEvent(int eventId, string title, DateTime date, int organizerId) + { + EventId = eventId; + Title = title; + Date = date; + OrganizerId = organizerId; + } +} diff --git a/EventFlow.Core/Events/EventDeletedDomainEvent.cs b/EventFlow.Core/Events/EventDeletedDomainEvent.cs new file mode 100644 index 0000000..9d4fab9 --- /dev/null +++ b/EventFlow.Core/Events/EventDeletedDomainEvent.cs @@ -0,0 +1,13 @@ +namespace EventFlow.Core.Events; + +public class EventDeletedDomainEvent : DomainEvent +{ + public int EventId { get; } + public string Title { get; } + + public EventDeletedDomainEvent(int eventId, string title) + { + EventId = eventId; + Title = title; + } +} diff --git a/EventFlow.Core/Events/EventUpdatedDomainEvent.cs b/EventFlow.Core/Events/EventUpdatedDomainEvent.cs new file mode 100644 index 0000000..8a1ea09 --- /dev/null +++ b/EventFlow.Core/Events/EventUpdatedDomainEvent.cs @@ -0,0 +1,17 @@ +namespace EventFlow.Core.Events; + +public class EventUpdatedDomainEvent : DomainEvent +{ + public int EventId { get; } + public string Title { get; } + public DateTime Date { get; } + public string Location { get; } + + public EventUpdatedDomainEvent(int eventId, string title, DateTime date, string location) + { + EventId = eventId; + Title = title; + Date = date; + Location = location; + } +} diff --git a/EventFlow.Core/GlobalUsing.cs b/EventFlow.Core/GlobalUsing.cs index efbc88b..468e456 100644 --- a/EventFlow.Core/GlobalUsing.cs +++ b/EventFlow.Core/GlobalUsing.cs @@ -1,3 +1,5 @@ -global using EventFlow.Core.Commands; +global using EventFlow.Core.Events; global using EventFlow.Core.Models; -global using EventFlow.Core.Models.DTOs; +global using EventFlow.Core.Primitives; +global using EventFlow.Core.Repository; +global using EventFlow.Core.ValueObjects; diff --git a/EventFlow.Core/Models/DTOs/DashboardStatsDTO.cs b/EventFlow.Core/Models/DTOs/DashboardStatsDTO.cs deleted file mode 100644 index 50c0cbf..0000000 --- a/EventFlow.Core/Models/DTOs/DashboardStatsDTO.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace EventFlow.Core.Models.DTOs -{ - public class DashboardStatsDTO - { - public int EventCount { get; set; } - public int OrganizerCount { get; set; } - public int SpeakerCount { get; set; } - public int ParticipantCount { get; set; } - } -} \ No newline at end of file diff --git a/EventFlow.Core/Models/Event.cs b/EventFlow.Core/Models/Event.cs index c44af39..9d774e7 100644 --- a/EventFlow.Core/Models/Event.cs +++ b/EventFlow.Core/Models/Event.cs @@ -1,15 +1,123 @@ namespace EventFlow.Core.Models; -public class Event +public class Event : Entity { - public int Id { get; set; } - public string Title { get; set; } - public string? Description { get; set; } - public DateTime Date { get; set; } - public string Location { get; set; } - public Organizer? Organizer { get; set; } - public int OrganizerId { get; set; } - public string? Category { get; set; } - public ICollection SpeakerEvents { get; set; } = []; - public ICollection Participants { get; set; } = []; + public EventTitle Title { get; private set; } = null!; + public string? Description { get; private set; } + public DateTime Date { get; private set; } + public EventLocation Location { get; private set; } = null!; + public int OrganizerId { get; private set; } + public Organizer? Organizer { get; init; } + public string? Category { get; init; } + public ICollection SpeakerEvents { get; private set; } = []; + public ICollection Participants { get; private set; } = []; + public DateTime CreatedAt { get; private set; } + public DateTime? UpdatedAt { get; private set; } + + private Event() { } + + public static Event Create(string title, string? description, DateTime date, string location, int organizerId) + { + CheckRule(new EventDateMustBeInFutureRule(date)); + + var @event = new Event + { + Title = EventTitle.Create(title), + Description = description, + Date = date, + Location = EventLocation.Create(location), + OrganizerId = organizerId, + CreatedAt = DateTime.UtcNow + }; + + @event.AddDomainEvent(new EventCreatedDomainEvent(@event.Id, title, date, organizerId)); + + return @event; + } + + public void UpdateDetails(string title, string? description, DateTime date, string location) + { + CheckRule(new EventDateMustBeInFutureRule(date)); + + Title = EventTitle.Create(title); + Description = description; + Date = date; + Location = EventLocation.Create(location); + UpdatedAt = DateTime.UtcNow; + + AddDomainEvent(new EventUpdatedDomainEvent(Id, title, date, location)); + } + + public void Reschedule(DateTime newDate) + { + CheckRule(new EventDateMustBeInFutureRule(newDate)); + + Date = newDate; + UpdatedAt = DateTime.UtcNow; + + AddDomainEvent(new EventUpdatedDomainEvent(Id, Title.Value, newDate, Location.FullAddress)); + } + + public void ChangeLocation(string newLocation) + { + Location = EventLocation.Create(newLocation); + UpdatedAt = DateTime.UtcNow; + + AddDomainEvent(new EventUpdatedDomainEvent(Id, Title.Value, Date, newLocation)); + } + + public void MarkAsDeleted() + { + AddDomainEvent(new EventDeletedDomainEvent(Id, Title.Value)); + } + + public void AddSpeaker(Speaker speaker) + { + if (SpeakerEvents.Any(se => se.SpeakerId == speaker.Id)) + throw new InvalidOperationException("Speaker already added to this event"); + + var speakerEvent = SpeakerEvent.Create(speaker, this); + SpeakerEvents.Add(speakerEvent); + } + + public void RemoveSpeaker(int speakerId) + { + var speakerEvent = SpeakerEvents.FirstOrDefault(se => se.SpeakerId == speakerId); + if (speakerEvent != null) + { + SpeakerEvents.Remove(speakerEvent); + } + } + + public void AddParticipant(Participant participant) + { + if (Participants.Any(p => p.Id == participant.Id)) + throw new InvalidOperationException("Participant already registered for this event"); + + Participants.Add(participant); + } + + public void RemoveParticipant(int participantId) + { + var participant = Participants.FirstOrDefault(p => p.Id == participantId); + if (participant != null) + { + Participants.Remove(participant); + } + } } + +public class EventDateMustBeInFutureRule : IBusinessRule +{ + private readonly DateTime _date; + private static readonly TimeSpan ClockSkew = TimeSpan.FromMinutes(5); + + public EventDateMustBeInFutureRule(DateTime date) + { + _date = date; + } + + public bool IsBroken() => _date < DateTime.UtcNow.Subtract(ClockSkew); + public string Message => "Event date must be in the future"; +} + diff --git a/EventFlow.Core/Models/Organizer.cs b/EventFlow.Core/Models/Organizer.cs index 60573c6..941dbf3 100644 --- a/EventFlow.Core/Models/Organizer.cs +++ b/EventFlow.Core/Models/Organizer.cs @@ -1,9 +1,46 @@ namespace EventFlow.Core.Models; -public class Organizer +public class Organizer : Entity { - public int Id { get; set; } - public string Name { get; set; } - public string Email { get; set; } - public ICollection Events { get; set; } = []; + public PersonName Name { get; private set; } = null!; + public Email Email { get; private set; } = null!; + public ICollection Events { get; private set; } = []; + public DateTime CreatedAt { get; private set; } + public DateTime? UpdatedAt { get; private set; } + + private Organizer() { } + + public static Organizer Create(string name, string email) + { + var organizer = new Organizer + { + Name = PersonName.Create(name), + Email = Email.Create(email), + CreatedAt = DateTime.UtcNow + }; + + return organizer; + } + + public void UpdateDetails(string name, string email) + { + Name = PersonName.Create(name); + Email = Email.Create(email); + UpdatedAt = DateTime.UtcNow; + } + + public void AddEvent(Event @event) + { + Events.Add(@event); + } + + public void RemoveEvent(int eventId) + { + var @event = Events.FirstOrDefault(e => e.Id == eventId); + if (@event != null) + { + Events.Remove(@event); + } + } } + diff --git a/EventFlow.Core/Models/Participant.cs b/EventFlow.Core/Models/Participant.cs index b5393a4..1ce039a 100644 --- a/EventFlow.Core/Models/Participant.cs +++ b/EventFlow.Core/Models/Participant.cs @@ -1,10 +1,58 @@ namespace EventFlow.Core.Models; -public class Participant +public class Participant : Entity { - public int Id { get; set; } - public string Name { get; set; } - public string Email { get; set; } - public string? Interests { get; set; } - public ICollection? Events { get; set; } = []; + public PersonName Name { get; private set; } = null!; + public Email Email { get; private set; } = null!; + public string? Interests { get; private set; } + public ICollection Events { get; private set; } = []; + public DateTime CreatedAt { get; private set; } + public DateTime? UpdatedAt { get; private set; } + + private Participant() { } + + public static Participant Create(string name, string email, string? interests = null) + { + var participant = new Participant + { + Name = PersonName.Create(name), + Email = Email.Create(email), + Interests = interests, + CreatedAt = DateTime.UtcNow + }; + + return participant; + } + + public void UpdateDetails(string name, string email, string? interests) + { + Name = PersonName.Create(name); + Email = Email.Create(email); + Interests = interests; + UpdatedAt = DateTime.UtcNow; + } + + public void UpdateInterests(string interests) + { + Interests = interests; + UpdatedAt = DateTime.UtcNow; + } + + public void RegisterForEvent(Event @event) + { + if (Events.Any(e => e.Id == @event.Id)) + throw new InvalidOperationException("Already registered for this event"); + + Events.Add(@event); + } + + public void UnregisterFromEvent(int eventId) + { + var @event = Events.FirstOrDefault(e => e.Id == eventId); + if (@event != null) + { + Events.Remove(@event); + } + } } + diff --git a/EventFlow.Core/Models/Speaker.cs b/EventFlow.Core/Models/Speaker.cs index c5e939c..6e80739 100644 --- a/EventFlow.Core/Models/Speaker.cs +++ b/EventFlow.Core/Models/Speaker.cs @@ -1,12 +1,67 @@ namespace EventFlow.Core.Models; -public class Speaker +public class Speaker : Entity { - public int Id { get; set; } - public string Name { get; set; } - public string? Biography { get; set; } - public string Email { get; set; } - public string? Expertise { get; set; } - public ICollection SpeakerEvents { get; set; } = []; + public PersonName Name { get; private set; } = null!; + public Email Email { get; private set; } = null!; + public string? Biography { get; private set; } + public string? Expertise { get; private set; } + public ICollection SpeakerEvents { get; private set; } = []; + public DateTime CreatedAt { get; private set; } + public DateTime? UpdatedAt { get; private set; } + private Speaker() { } + + public static Speaker Create(string name, string email, string? biography = null, string? expertise = null) + { + var speaker = new Speaker + { + Name = PersonName.Create(name), + Email = Email.Create(email), + Biography = biography, + Expertise = expertise, + CreatedAt = DateTime.UtcNow + }; + + return speaker; + } + + public void UpdateDetails(string name, string email, string? biography, string? expertise) + { + Name = PersonName.Create(name); + Email = Email.Create(email); + Biography = biography; + Expertise = expertise; + UpdatedAt = DateTime.UtcNow; + } + + public void UpdateBiography(string biography) + { + Biography = biography; + UpdatedAt = DateTime.UtcNow; + } + + public void UpdateExpertise(string expertise) + { + Expertise = expertise; + UpdatedAt = DateTime.UtcNow; + } + + public void LinkToEvent(SpeakerEvent speakerEvent) + { + if (SpeakerEvents.Any(se => se.EventId == speakerEvent.EventId)) + throw new InvalidOperationException("Already linked to this event"); + + SpeakerEvents.Add(speakerEvent); + } + + public void UnlinkFromEvent(int eventId) + { + var speakerEvent = SpeakerEvents.FirstOrDefault(se => se.EventId == eventId); + if (speakerEvent != null) + { + SpeakerEvents.Remove(speakerEvent); + } + } } + diff --git a/EventFlow.Core/Models/SpeakerEvent.cs b/EventFlow.Core/Models/SpeakerEvent.cs index b6b9fa0..d8877fd 100644 --- a/EventFlow.Core/Models/SpeakerEvent.cs +++ b/EventFlow.Core/Models/SpeakerEvent.cs @@ -2,10 +2,22 @@ public class SpeakerEvent { - public int SpeakerId { get; set; } - public Speaker Speaker { get; set; } - public int EventId { get; set; } - public Event Event { get; set; } - public DateTime RegisteredAt { get; set; } = DateTime.UtcNow; + public int SpeakerId { get; internal set; } + public Speaker Speaker { get; internal set; } = null!; + public int EventId { get; internal set; } + public Event Event { get; internal set; } = null!; + public DateTime RegisteredAt { get; private set; } + + public static SpeakerEvent Create(Speaker speaker, Event @event) + { + return new SpeakerEvent + { + SpeakerId = speaker.Id, + Speaker = speaker, + EventId = @event.Id, + Event = @event, + RegisteredAt = DateTime.UtcNow + }; + } } diff --git a/EventFlow.Core/Models/User.cs b/EventFlow.Core/Models/User.cs index ce45e4e..5db88f6 100644 --- a/EventFlow.Core/Models/User.cs +++ b/EventFlow.Core/Models/User.cs @@ -1,10 +1,70 @@ namespace EventFlow.Core.Models; -public class User +public class User : Entity { - public int Id { get; set; } - public string Username { get; set; } = string.Empty; - public string Email { get; set; } = string.Empty; - public string PasswordHash { get; set; } = string.Empty; - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public string Username { get; private set; } = string.Empty; + public Email Email { get; private set; } = null!; + public string PasswordHash { get; private set; } = string.Empty; + public DateTime CreatedAt { get; private set; } + public DateTime? LastLoginAt { get; private set; } + + private User() { } + + public static User Create(string username, string email, string passwordHash) + { + CheckRule(new UserUsernameCannotBeEmptyRule(username)); + CheckRule(new UserPasswordHashCannotBeEmptyRule(passwordHash)); + + var user = new User + { + Username = username, + Email = Email.Create(email), + PasswordHash = passwordHash, + CreatedAt = DateTime.UtcNow + }; + + return user; + } + + public void UpdateEmail(string email) + { + Email = Email.Create(email); + } + + public void UpdatePassword(string passwordHash) + { + CheckRule(new UserPasswordHashCannotBeEmptyRule(passwordHash)); + PasswordHash = passwordHash; + } + + public void RecordLogin() + { + LastLoginAt = DateTime.UtcNow; + } +} + +public class UserUsernameCannotBeEmptyRule : IBusinessRule +{ + private readonly string _username; + + public UserUsernameCannotBeEmptyRule(string username) + { + _username = username; + } + + public bool IsBroken() => string.IsNullOrWhiteSpace(_username); + public string Message => "Username cannot be empty"; +} + +public class UserPasswordHashCannotBeEmptyRule : IBusinessRule +{ + private readonly string _passwordHash; + + public UserPasswordHashCannotBeEmptyRule(string passwordHash) + { + _passwordHash = passwordHash; + } + + public bool IsBroken() => string.IsNullOrWhiteSpace(_passwordHash); + public string Message => "Password hash cannot be empty"; } diff --git a/EventFlow.Core/Primitives/BusinessRuleValidationException.cs b/EventFlow.Core/Primitives/BusinessRuleValidationException.cs new file mode 100644 index 0000000..62cea46 --- /dev/null +++ b/EventFlow.Core/Primitives/BusinessRuleValidationException.cs @@ -0,0 +1,19 @@ +namespace EventFlow.Core.Primitives; + +public class BusinessRuleValidationException : Exception +{ + public IBusinessRule BrokenRule { get; } + public string Details { get; } + + public BusinessRuleValidationException(IBusinessRule brokenRule) + : base(brokenRule.Message) + { + BrokenRule = brokenRule; + Details = brokenRule.Message; + } + + public override string ToString() + { + return $"{BrokenRule.GetType().FullName}: {Details}"; + } +} diff --git a/EventFlow.Core/Primitives/DomainEvent.cs b/EventFlow.Core/Primitives/DomainEvent.cs new file mode 100644 index 0000000..ad8acb0 --- /dev/null +++ b/EventFlow.Core/Primitives/DomainEvent.cs @@ -0,0 +1,7 @@ +namespace EventFlow.Core.Primitives; + +public abstract class DomainEvent : IDomainEvent +{ + public Guid Id { get; } = Guid.NewGuid(); + public DateTime OccurredOn { get; } = DateTime.UtcNow; +} diff --git a/EventFlow.Core/Primitives/Entity.cs b/EventFlow.Core/Primitives/Entity.cs new file mode 100644 index 0000000..a9e1428 --- /dev/null +++ b/EventFlow.Core/Primitives/Entity.cs @@ -0,0 +1,51 @@ +namespace EventFlow.Core.Primitives; + +public abstract class Entity : IHasDomainEvents +{ + private readonly List _domainEvents = new(); + + public IReadOnlyCollection DomainEvents => _domainEvents.AsReadOnly(); + + public void AddDomainEvent(IDomainEvent domainEvent) + { + _domainEvents.Add(domainEvent); + } + + public void ClearDomainEvents() + { + _domainEvents.Clear(); + } + + protected static void CheckRule(IBusinessRule rule) + { + if (rule.IsBroken()) + { + throw new BusinessRuleValidationException(rule); + } + } +} + +public abstract class Entity : Entity where TId : struct +{ + public TId Id { get; protected set; } + + public override bool Equals(object? obj) + { + if (obj is not Entity other) + return false; + + if (ReferenceEquals(this, other)) + return true; + + if (GetType() != other.GetType()) + return false; + + return Id.Equals(other.Id); + } + + public override int GetHashCode() + { + return (GetType().ToString() + Id).GetHashCode(); + } + +} diff --git a/EventFlow.Core/Primitives/Error.cs b/EventFlow.Core/Primitives/Error.cs new file mode 100644 index 0000000..56d387a --- /dev/null +++ b/EventFlow.Core/Primitives/Error.cs @@ -0,0 +1,49 @@ +namespace EventFlow.Core.Primitives; + +public readonly record struct Error +{ + public static readonly Error None = new(string.Empty, string.Empty, ErrorType.None); + + public string Code { get; } + public string Message { get; } + public ErrorType Type { get; } + + private Error(string code, string message, ErrorType type) + { + Code = code; + Message = message; + Type = type; + } + + public static Error NotFound(string code, string message) => + new(code, message, ErrorType.NotFound); + + public static Error Validation(string code, string message) => + new(code, message, ErrorType.Validation); + + public static Error Conflict(string code, string message) => + new(code, message, ErrorType.Conflict); + + public static Error Failure(string code, string message) => + new(code, message, ErrorType.Failure); + + public static Error Unauthorized(string code, string message) => + new(code, message, ErrorType.Unauthorized); + + public static Error Forbidden(string code, string message) => + new(code, message, ErrorType.Forbidden); + + public static Error NullValue(string? code = null, string? message = null) => + new(code ?? "General.Null", message ?? "Value cannot be null", ErrorType.Failure); +} + +public enum ErrorType +{ + None, + NotFound, + Validation, + Conflict, + Failure, + Unauthorized, + Forbidden +} diff --git a/EventFlow.Core/Primitives/IBusinessRule.cs b/EventFlow.Core/Primitives/IBusinessRule.cs new file mode 100644 index 0000000..02777dc --- /dev/null +++ b/EventFlow.Core/Primitives/IBusinessRule.cs @@ -0,0 +1,7 @@ +namespace EventFlow.Core.Primitives; + +public interface IBusinessRule +{ + bool IsBroken(); + string Message { get; } +} diff --git a/EventFlow.Core/Primitives/IDomainEvent.cs b/EventFlow.Core/Primitives/IDomainEvent.cs new file mode 100644 index 0000000..97d847e --- /dev/null +++ b/EventFlow.Core/Primitives/IDomainEvent.cs @@ -0,0 +1,7 @@ +namespace EventFlow.Core.Primitives; + +public interface IDomainEvent +{ + Guid Id { get; } + DateTime OccurredOn { get; } +} diff --git a/EventFlow.Core/Primitives/IDomainEventDispatcher.cs b/EventFlow.Core/Primitives/IDomainEventDispatcher.cs new file mode 100644 index 0000000..4a71e3d --- /dev/null +++ b/EventFlow.Core/Primitives/IDomainEventDispatcher.cs @@ -0,0 +1,6 @@ +namespace EventFlow.Core.Primitives; + +public interface IDomainEventDispatcher +{ + Task DispatchAsync(IEnumerable domainEvents, CancellationToken cancellationToken = default); +} diff --git a/EventFlow.Core/Primitives/IHasDomainEvents.cs b/EventFlow.Core/Primitives/IHasDomainEvents.cs new file mode 100644 index 0000000..3d9cd84 --- /dev/null +++ b/EventFlow.Core/Primitives/IHasDomainEvents.cs @@ -0,0 +1,7 @@ +namespace EventFlow.Core.Primitives; + +public interface IHasDomainEvents +{ + IReadOnlyCollection DomainEvents { get; } + void ClearDomainEvents(); +} diff --git a/EventFlow.Core/Primitives/Result.cs b/EventFlow.Core/Primitives/Result.cs new file mode 100644 index 0000000..0b162b0 --- /dev/null +++ b/EventFlow.Core/Primitives/Result.cs @@ -0,0 +1,56 @@ +using System.Diagnostics.CodeAnalysis; + +namespace EventFlow.Core.Primitives; + +public class Result +{ + public bool IsSuccess { get; } + public bool IsFailure => !IsSuccess; + public Error Error { get; } + + protected Result(bool isSuccess, Error error) + { + if (isSuccess && error != Error.None) + throw new InvalidOperationException("Cannot create success result with error"); + + if (!isSuccess && error == Error.None) + throw new InvalidOperationException("Cannot create failure result with empty error"); + + IsSuccess = isSuccess; + Error = error; + } + + public static Result Success() => new(true, Error.None); + public static Result Failure(Error error) => new(false, error); + + public static implicit operator Result(Error error) => Failure(error); +} + +public class Result : Result +{ + private readonly T? _value; + + [NotNullIfNotNull(nameof(_value))] + public T? Value => IsSuccess ? _value : throw new InvalidOperationException("Cannot access value of failed result"); + + protected Result(T? value, bool isSuccess, Error error) : base(isSuccess, error) + { + _value = value; + } + + public static Result Success(T value) => new(value, true, Error.None); + public new static Result Failure(Error error) => new(default, false, error); + + public static implicit operator Result(T? value) => value is not null ? Success(value) : Failure(Error.NullValue()); + public static implicit operator Result(Error error) => Failure(error); + + public TResult Map(Func onSuccess, Func onFailure) + { + return IsSuccess ? onSuccess(_value!) : onFailure(Error); + } + + public async Task MapAsync(Func> onSuccess, Func> onFailure) + { + return IsSuccess ? await onSuccess(_value!) : await onFailure(Error); + } +} diff --git a/EventFlow.Core/Primitives/ValueObject.cs b/EventFlow.Core/Primitives/ValueObject.cs new file mode 100644 index 0000000..3c53e78 --- /dev/null +++ b/EventFlow.Core/Primitives/ValueObject.cs @@ -0,0 +1,43 @@ +namespace EventFlow.Core.Primitives; + +public abstract class ValueObject : IEquatable +{ + protected abstract IEnumerable GetEqualityComponents(); + + public override bool Equals(object? obj) + { + if (obj is null || obj.GetType() != GetType()) + return false; + + var other = (ValueObject)obj; + return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); + } + + public bool Equals(ValueObject? other) + { + return Equals((object?)other); + } + + public override int GetHashCode() + { + return GetEqualityComponents() + .Select(x => x?.GetHashCode() ?? 0) + .Aggregate((x, y) => x ^ y); + } + + public static bool operator ==(ValueObject? left, ValueObject? right) + { + if (left is null && right is null) + return true; + + if (left is null || right is null) + return false; + + return left.Equals(right); + } + + public static bool operator !=(ValueObject? left, ValueObject? right) + { + return !(left == right); + } +} diff --git a/EventFlow.Core/Repository/Interfaces/IEventRepository.cs b/EventFlow.Core/Repository/IEventRepository.cs similarity index 89% rename from EventFlow.Core/Repository/Interfaces/IEventRepository.cs rename to EventFlow.Core/Repository/IEventRepository.cs index 15067fb..cb9187d 100644 --- a/EventFlow.Core/Repository/Interfaces/IEventRepository.cs +++ b/EventFlow.Core/Repository/IEventRepository.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Repository.Interfaces; +namespace EventFlow.Core.Repository; public interface IEventRepository { diff --git a/EventFlow.Core/Repository/Interfaces/IOrganizerRepository.cs b/EventFlow.Core/Repository/IOrganizerRepository.cs similarity index 89% rename from EventFlow.Core/Repository/Interfaces/IOrganizerRepository.cs rename to EventFlow.Core/Repository/IOrganizerRepository.cs index 2190c89..e2c6915 100644 --- a/EventFlow.Core/Repository/Interfaces/IOrganizerRepository.cs +++ b/EventFlow.Core/Repository/IOrganizerRepository.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Repository.Interfaces; +namespace EventFlow.Core.Repository; public interface IOrganizerRepository { diff --git a/EventFlow.Core/Repository/Interfaces/IParticipantRepository.cs b/EventFlow.Core/Repository/IParticipantRepository.cs similarity index 90% rename from EventFlow.Core/Repository/Interfaces/IParticipantRepository.cs rename to EventFlow.Core/Repository/IParticipantRepository.cs index 90d04d5..2533120 100644 --- a/EventFlow.Core/Repository/Interfaces/IParticipantRepository.cs +++ b/EventFlow.Core/Repository/IParticipantRepository.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Repository.Interfaces; +namespace EventFlow.Core.Repository; public interface IParticipantRepository { diff --git a/EventFlow.Core/Repository/Interfaces/ISpeakerRepository.cs b/EventFlow.Core/Repository/ISpeakerRepository.cs similarity index 89% rename from EventFlow.Core/Repository/Interfaces/ISpeakerRepository.cs rename to EventFlow.Core/Repository/ISpeakerRepository.cs index fede71f..5877173 100644 --- a/EventFlow.Core/Repository/Interfaces/ISpeakerRepository.cs +++ b/EventFlow.Core/Repository/ISpeakerRepository.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Repository.Interfaces; +namespace EventFlow.Core.Repository; public interface ISpeakerRepository { @@ -10,4 +10,4 @@ public interface ISpeakerRepository Task> GetAllPagedSpeakersAsync(QueryParameters queryParameters); Task AddSpeakerEventAsync(SpeakerEvent speakerEvent); Task SpeakerCountAsync(); -} \ No newline at end of file +} diff --git a/EventFlow.Core/Repository/Interfaces/IUserRepository.cs b/EventFlow.Core/Repository/IUserRepository.cs similarity index 78% rename from EventFlow.Core/Repository/Interfaces/IUserRepository.cs rename to EventFlow.Core/Repository/IUserRepository.cs index af65f28..6b6c635 100644 --- a/EventFlow.Core/Repository/Interfaces/IUserRepository.cs +++ b/EventFlow.Core/Repository/IUserRepository.cs @@ -1,4 +1,4 @@ -namespace EventFlow.Core.Repository.Interfaces; +namespace EventFlow.Core.Repository; public interface IUserRepository { @@ -6,4 +6,4 @@ public interface IUserRepository Task ExistsByEmailAsync(string email); Task AddAsync(User user); Task UpdateAsync(User user); -} \ No newline at end of file +} diff --git a/EventFlow.Core/Services/Interfaces/IEventService.cs b/EventFlow.Core/Services/Interfaces/IEventService.cs deleted file mode 100644 index 154081c..0000000 --- a/EventFlow.Core/Services/Interfaces/IEventService.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace EventFlow.Core.Services.Interfaces; - -public interface IEventService -{ - Task GetByIdAsync(int id); - Task> GetAllEventsAsync(); - Task> GetAllPagedEventsAsync(QueryParameters queryParameters); - Task CreateAsync(EventCommand command); - Task UpdateAsync(int id, EventCommand command); - Task DeleteAsync(int id); -} diff --git a/EventFlow.Core/Services/Interfaces/IOrganizerService.cs b/EventFlow.Core/Services/Interfaces/IOrganizerService.cs deleted file mode 100644 index e36ad6f..0000000 --- a/EventFlow.Core/Services/Interfaces/IOrganizerService.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace EventFlow.Core.Services.Interfaces; - -public interface IOrganizerService -{ - Task GetByIdAsync(int id); - Task> GetAllOrganizersAsync(); - Task> GetAllPagedOrganizersAsync(QueryParameters queryParameters); - Task CreateAsync(OrganizerCommand command); - Task RegisterToEventAsync(int eventId, int organizerId); - Task UpdateAsync(int id, OrganizerCommand command); - Task DeleteAsync(int id); -} diff --git a/EventFlow.Core/Services/Interfaces/IParticipantService.cs b/EventFlow.Core/Services/Interfaces/IParticipantService.cs deleted file mode 100644 index 98440e9..0000000 --- a/EventFlow.Core/Services/Interfaces/IParticipantService.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace EventFlow.Core.Services.Interfaces; - -public interface IParticipantService -{ - Task GetByIdAsync(int id); - Task> GetAllPagedParticipantsByEventIdAsync(int eventId, QueryParameters queryParameters); - Task CreateAsync(ParticipantCommand command); - Task RegisterToEventAsync(int eventId, int participantId); - Task UpdateAsync(int id, ParticipantCommand command); - Task> GetAllParticipantsWithEventsAsync(); - Task DeleteAsync(int id); -} diff --git a/EventFlow.Core/Services/Interfaces/ISpeakerService.cs b/EventFlow.Core/Services/Interfaces/ISpeakerService.cs deleted file mode 100644 index 0eaee4e..0000000 --- a/EventFlow.Core/Services/Interfaces/ISpeakerService.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace EventFlow.Core.Services.Interfaces; - -public interface ISpeakerService -{ - Task GetByIdAsync(int id); - Task> GetAllSpeakersAsync(); - Task> GetAllPagedSpeakersAsync(QueryParameters queryParameters); - Task CreateAsync(SpeakerCommand command); - Task RegisterToEventAsync(int eventId, int speakerId); - Task UpdateAsync(int id, SpeakerCommand command); - Task DeleteAsync(int id); -} - diff --git a/EventFlow.Core/ValueObjects/Email.cs b/EventFlow.Core/ValueObjects/Email.cs new file mode 100644 index 0000000..ea9e698 --- /dev/null +++ b/EventFlow.Core/ValueObjects/Email.cs @@ -0,0 +1,60 @@ +namespace EventFlow.Core.ValueObjects; + +public sealed class Email : ValueObject +{ + public string Value { get; } + + private Email(string value) + { + Value = value; + } + + public static Email Create(string email) + { + if (string.IsNullOrWhiteSpace(email)) + throw new ArgumentException("Email cannot be empty", nameof(email)); + + email = email.Trim().ToLowerInvariant(); + + if (!IsValidEmail(email)) + throw new ArgumentException("Invalid email format", nameof(email)); + + return new Email(email); + } + + public static bool TryCreate(string email, out Email? result) + { + try + { + result = Create(email); + return true; + } + catch + { + result = null; + return false; + } + } + + private static bool IsValidEmail(string email) + { + try + { + var addr = new System.Net.Mail.MailAddress(email); + return addr.Address == email; + } + catch + { + return false; + } + } + + protected override IEnumerable GetEqualityComponents() + { + yield return Value; + } + + public override string ToString() => Value; + + public static implicit operator string(Email email) => email.Value; +} diff --git a/EventFlow.Core/ValueObjects/EventLocation.cs b/EventFlow.Core/ValueObjects/EventLocation.cs new file mode 100644 index 0000000..07966c4 --- /dev/null +++ b/EventFlow.Core/ValueObjects/EventLocation.cs @@ -0,0 +1,75 @@ +namespace EventFlow.Core.ValueObjects; + +public sealed class EventLocation : ValueObject +{ + public string Address { get; } + public string? City { get; } + public string? State { get; } + public string? ZipCode { get; } + + private EventLocation(string address, string? city, string? state, string? zipCode) + { + Address = address; + City = city; + State = state; + ZipCode = zipCode; + } + + public static EventLocation Create(string address, string? city = null, string? state = null, string? zipCode = null) + { + if (string.IsNullOrWhiteSpace(address)) + throw new ArgumentException("Address cannot be empty", nameof(address)); + + address = address.Trim(); + + if (address.Length < 5) + throw new ArgumentException("Address must be at least 5 characters", nameof(address)); + + if (address.Length > 300) + throw new ArgumentException("Address cannot exceed 300 characters", nameof(address)); + + return new EventLocation( + address, + city?.Trim(), + state?.Trim(), + zipCode?.Trim()); + } + + public static bool TryCreate(string address, string? city, string? state, string? zipCode, out EventLocation? result) + { + try + { + result = Create(address, city, state, zipCode); + return true; + } + catch + { + result = null; + return false; + } + } + + public string FullAddress + { + get + { + var parts = new List { Address }; + if (!string.IsNullOrEmpty(City)) parts.Add(City); + if (!string.IsNullOrEmpty(State)) parts.Add(State); + if (!string.IsNullOrEmpty(ZipCode)) parts.Add(ZipCode); + return string.Join(", ", parts); + } + } + + protected override IEnumerable GetEqualityComponents() + { + yield return Address.ToLowerInvariant(); + yield return City?.ToLowerInvariant() ?? string.Empty; + yield return State?.ToLowerInvariant() ?? string.Empty; + yield return ZipCode?.ToLowerInvariant() ?? string.Empty; + } + + public override string ToString() => FullAddress; + + public static implicit operator string(EventLocation location) => location.FullAddress; +} diff --git a/EventFlow.Core/ValueObjects/EventTitle.cs b/EventFlow.Core/ValueObjects/EventTitle.cs new file mode 100644 index 0000000..f101753 --- /dev/null +++ b/EventFlow.Core/ValueObjects/EventTitle.cs @@ -0,0 +1,50 @@ +namespace EventFlow.Core.ValueObjects; + +public sealed class EventTitle : ValueObject +{ + public string Value { get; } + + private EventTitle(string value) + { + Value = value; + } + + public static EventTitle Create(string title) + { + if (string.IsNullOrWhiteSpace(title)) + throw new ArgumentException("Event title cannot be empty", nameof(title)); + + title = title.Trim(); + + if (title.Length < 3) + throw new ArgumentException("Event title must be at least 3 characters", nameof(title)); + + if (title.Length > 200) + throw new ArgumentException("Event title cannot exceed 200 characters", nameof(title)); + + return new EventTitle(title); + } + + public static bool TryCreate(string title, out EventTitle? result) + { + try + { + result = Create(title); + return true; + } + catch + { + result = null; + return false; + } + } + + protected override IEnumerable GetEqualityComponents() + { + yield return Value; + } + + public override string ToString() => Value; + + public static implicit operator string(EventTitle title) => title.Value; +} diff --git a/EventFlow.Core/ValueObjects/PersonName.cs b/EventFlow.Core/ValueObjects/PersonName.cs new file mode 100644 index 0000000..0427641 --- /dev/null +++ b/EventFlow.Core/ValueObjects/PersonName.cs @@ -0,0 +1,62 @@ +namespace EventFlow.Core.ValueObjects; + +public sealed class PersonName : ValueObject +{ + public string FirstName { get; } + public string LastName { get; } + + private PersonName(string firstName, string lastName) + { + FirstName = firstName; + LastName = lastName; + } + + public static PersonName Create(string firstName, string? lastName = null) + { + if (string.IsNullOrWhiteSpace(firstName)) + throw new ArgumentException("First name cannot be empty", nameof(firstName)); + + firstName = firstName.Trim(); + + if (firstName.Length < 2) + throw new ArgumentException("First name must be at least 2 characters", nameof(firstName)); + + if (firstName.Length > 100) + throw new ArgumentException("First name cannot exceed 100 characters", nameof(firstName)); + + lastName = lastName?.Trim() ?? string.Empty; + + if (lastName.Length > 100) + throw new ArgumentException("Last name cannot exceed 100 characters", nameof(lastName)); + + return new PersonName(firstName, lastName); + } + + public static bool TryCreate(string firstName, string? lastName, out PersonName? result) + { + try + { + result = Create(firstName, lastName); + return true; + } + catch + { + result = null; + return false; + } + } + + public string FullName => string.IsNullOrEmpty(LastName) + ? FirstName + : $"{FirstName} {LastName}"; + + protected override IEnumerable GetEqualityComponents() + { + yield return FirstName.ToLowerInvariant(); + yield return LastName.ToLowerInvariant(); + } + + public override string ToString() => FullName; + + public static implicit operator string(PersonName name) => name.FullName; +} diff --git a/EventFlow.Infrastructure/Data/EventFlowContext.cs b/EventFlow.Infrastructure/Data/EventFlowContext.cs index 00416aa..1ce3294 100644 --- a/EventFlow.Infrastructure/Data/EventFlowContext.cs +++ b/EventFlow.Infrastructure/Data/EventFlowContext.cs @@ -1,15 +1,29 @@ -using EventFlow.Infrastructure.Data.Mapping; +using EventFlow.Core.Primitives; +using EventFlow.Infrastructure.Data.Mapping; namespace EventFlow.Infrastructure.Data; -public class EventFlowContext(DbContextOptions options) : DbContext(options) +public class EventFlowContext : DbContext { + private readonly IDomainEventDispatcher? _domainEventDispatcher; + + public EventFlowContext(DbContextOptions options) : base(options) + { + } + + public EventFlowContext( + DbContextOptions options, + IDomainEventDispatcher domainEventDispatcher) : base(options) + { + _domainEventDispatcher = domainEventDispatcher; + } + public DbSet Event { get; set; } = null!; public DbSet Organizer { get; set; } = null!; public DbSet Participant { get; set; } = null!; public DbSet Speaker { get; set; } = null!; public DbSet SpeakerEvents { get; set; } = null!; - public DbSet Users { get; set; } + public DbSet Users { get; set; } = null!; protected override void OnModelCreating(ModelBuilder builder) { @@ -20,4 +34,26 @@ protected override void OnModelCreating(ModelBuilder builder) builder.ApplyConfiguration(new SpeakerEventMap()); builder.ApplyConfiguration(new UserMap()); } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + var domainEvents = ChangeTracker + .Entries() + .SelectMany(e => e.Entity.DomainEvents) + .ToList(); + + ChangeTracker + .Entries() + .ToList() + .ForEach(e => e.Entity.ClearDomainEvents()); + + var result = await base.SaveChangesAsync(cancellationToken); + + if (_domainEventDispatcher != null && domainEvents.Count > 0) + { + await _domainEventDispatcher.DispatchAsync(domainEvents, cancellationToken); + } + + return result; + } } \ No newline at end of file diff --git a/EventFlow.Infrastructure/Data/Mapping/EventMap.cs b/EventFlow.Infrastructure/Data/Mapping/EventMap.cs index 6499bda..7fb7c43 100644 --- a/EventFlow.Infrastructure/Data/Mapping/EventMap.cs +++ b/EventFlow.Infrastructure/Data/Mapping/EventMap.cs @@ -11,10 +11,13 @@ public void Configure(EntityTypeBuilder builder) .ValueGeneratedOnAdd() .UseIdentityColumn(); - builder.Property(x => x.Title) - .HasColumnName("Title") - .HasColumnType("VARCHAR") - .HasMaxLength(200); + builder.OwnsOne(x => x.Title, title => + { + title.Property(t => t.Value) + .HasColumnName("Title") + .HasColumnType("VARCHAR") + .HasMaxLength(200); + }); builder.Property(x => x.Description) .HasColumnName("Description") @@ -25,10 +28,13 @@ public void Configure(EntityTypeBuilder builder) .HasColumnName("Date") .HasColumnType("DATETIME"); - builder.Property(x => x.Location) - .HasColumnName("Location") - .HasColumnType("VARCHAR") - .HasMaxLength(150); + builder.OwnsOne(x => x.Location, location => + { + location.Property(l => l.Address) + .HasColumnName("Location") + .HasColumnType("VARCHAR") + .HasMaxLength(300); + }); builder.Property(x => x.OrganizerId) .HasColumnName("OrganizerId") diff --git a/EventFlow.Infrastructure/Data/Mapping/OrganizerMap.cs b/EventFlow.Infrastructure/Data/Mapping/OrganizerMap.cs index 8ae1a09..d617f30 100644 --- a/EventFlow.Infrastructure/Data/Mapping/OrganizerMap.cs +++ b/EventFlow.Infrastructure/Data/Mapping/OrganizerMap.cs @@ -11,15 +11,33 @@ public void Configure(EntityTypeBuilder builder) .ValueGeneratedOnAdd() .UseIdentityColumn(); - builder.Property(x => x.Name) - .HasColumnName("Name") - .HasColumnType("VARCHAR") - .HasMaxLength(200); - - builder.Property(x => x.Email) - .HasColumnName("Email") - .HasColumnType("VARCHAR") - .HasMaxLength(150); + builder.OwnsOne(x => x.Name, name => + { + name.Property(n => n.FirstName) + .HasColumnName("FirstName") + .HasColumnType("VARCHAR") + .HasMaxLength(100); + + name.Property(n => n.LastName) + .HasColumnName("LastName") + .HasColumnType("VARCHAR") + .HasMaxLength(100); + }); + + builder.OwnsOne(x => x.Email, email => + { + email.Property(e => e.Value) + .HasColumnName("Email") + .HasColumnType("VARCHAR") + .HasMaxLength(150); + }); + + builder.Property(x => x.CreatedAt) + .IsRequired() + .HasDefaultValueSql("GETDATE()"); + + builder.Property(x => x.UpdatedAt) + .IsRequired(false); builder .HasMany(o => o.Events) diff --git a/EventFlow.Infrastructure/Data/Mapping/ParticipantMap.cs b/EventFlow.Infrastructure/Data/Mapping/ParticipantMap.cs index b0cc99b..4433a9b 100644 --- a/EventFlow.Infrastructure/Data/Mapping/ParticipantMap.cs +++ b/EventFlow.Infrastructure/Data/Mapping/ParticipantMap.cs @@ -11,15 +11,21 @@ public void Configure(EntityTypeBuilder builder) .ValueGeneratedOnAdd() .UseIdentityColumn(); - builder.Property(x => x.Name) - .HasColumnName("Name") - .HasColumnType("VARCHAR") - .HasMaxLength(200); + builder.OwnsOne(x => x.Name, name => + { + name.Property(n => n.FirstName) + .HasColumnName("Name") + .HasColumnType("VARCHAR") + .HasMaxLength(200); + }); - builder.Property(x => x.Email) - .HasColumnName("Email") - .HasColumnType("VARCHAR") - .HasMaxLength(150); + builder.OwnsOne(x => x.Email, email => + { + email.Property(e => e.Value) + .HasColumnName("Email") + .HasColumnType("VARCHAR") + .HasMaxLength(150); + }); builder.Property(x => x.Interests) .HasColumnName("Interests") diff --git a/EventFlow.Infrastructure/Data/Mapping/SpeakerMap.cs b/EventFlow.Infrastructure/Data/Mapping/SpeakerMap.cs index 5d783d1..b83d322 100644 --- a/EventFlow.Infrastructure/Data/Mapping/SpeakerMap.cs +++ b/EventFlow.Infrastructure/Data/Mapping/SpeakerMap.cs @@ -11,20 +11,26 @@ public void Configure(EntityTypeBuilder builder) .ValueGeneratedOnAdd() .UseIdentityColumn(); - builder.Property(x => x.Name) - .HasColumnName("Name") - .HasColumnType("VARCHAR") - .HasMaxLength(200); + builder.OwnsOne(x => x.Name, name => + { + name.Property(n => n.FirstName) + .HasColumnName("Name") + .HasColumnType("VARCHAR") + .HasMaxLength(200); + }); builder.Property(x => x.Biography) .HasColumnName("Biography") .HasColumnType("VARCHAR") .HasMaxLength(2000); - builder.Property(x => x.Email) - .HasColumnName("Email") - .HasColumnType("VARCHAR") - .HasMaxLength(150); + builder.OwnsOne(x => x.Email, email => + { + email.Property(e => e.Value) + .HasColumnName("Email") + .HasColumnType("VARCHAR") + .HasMaxLength(150); + }); builder.Property(x => x.Expertise) .HasColumnName("Expertise") diff --git a/EventFlow.Infrastructure/Data/Mapping/UserMap.cs b/EventFlow.Infrastructure/Data/Mapping/UserMap.cs index f79e492..1611ec9 100644 --- a/EventFlow.Infrastructure/Data/Mapping/UserMap.cs +++ b/EventFlow.Infrastructure/Data/Mapping/UserMap.cs @@ -20,12 +20,16 @@ public void Configure(EntityTypeBuilder builder) .HasColumnType("VARCHAR") .HasMaxLength(100); - builder.Property(u => u.Email) - .IsRequired() - .HasColumnType("VARCHAR") - .HasMaxLength(150); - - builder.HasIndex(u => u.Email) + builder.OwnsOne(u => u.Email, email => + { + email.Property(e => e.Value) + .HasColumnName("Email") + .IsRequired() + .HasColumnType("VARCHAR") + .HasMaxLength(150); + }); + + builder.HasIndex("Email") .IsUnique(); builder.Property(u => u.PasswordHash) @@ -34,6 +38,9 @@ public void Configure(EntityTypeBuilder builder) builder.Property(u => u.CreatedAt) .IsRequired() - .HasDefaultValueSql("GETDATE()"); + .HasDefaultValueSql("GETDATE()"); + + builder.Property(u => u.LastLoginAt) + .IsRequired(false); } } \ No newline at end of file diff --git a/EventFlow.Infrastructure/Events/MediatRDomainEventDispatcher.cs b/EventFlow.Infrastructure/Events/MediatRDomainEventDispatcher.cs new file mode 100644 index 0000000..c78f7a9 --- /dev/null +++ b/EventFlow.Infrastructure/Events/MediatRDomainEventDispatcher.cs @@ -0,0 +1,22 @@ +using EventFlow.Core.Primitives; +using MediatR; + +namespace EventFlow.Infrastructure.Events; + +public class MediatRDomainEventDispatcher : IDomainEventDispatcher +{ + private readonly IMediator _mediator; + + public MediatRDomainEventDispatcher(IMediator mediator) + { + _mediator = mediator; + } + + public async Task DispatchAsync(IEnumerable domainEvents, CancellationToken cancellationToken = default) + { + foreach (var domainEvent in domainEvents) + { + await _mediator.Publish(domainEvent, cancellationToken); + } + } +} diff --git a/EventFlow.Infrastructure/GlobalUsing.cs b/EventFlow.Infrastructure/GlobalUsing.cs index 652463b..5a374dc 100644 --- a/EventFlow.Infrastructure/GlobalUsing.cs +++ b/EventFlow.Infrastructure/GlobalUsing.cs @@ -1,5 +1,5 @@ global using EventFlow.Core.Models; -global using EventFlow.Core.Repository.Interfaces; +global using EventFlow.Core.Repository; global using EventFlow.Infrastructure.Data; global using Microsoft.EntityFrameworkCore; global using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/EventFlow.Infrastructure/Persistence/UnitOfWork.cs b/EventFlow.Infrastructure/Persistence/UnitOfWork.cs new file mode 100644 index 0000000..7c09d2a --- /dev/null +++ b/EventFlow.Infrastructure/Persistence/UnitOfWork.cs @@ -0,0 +1,19 @@ +using EventFlow.Application.Abstractions; +using EventFlow.Infrastructure.Data; + +namespace EventFlow.Infrastructure.Persistence; + +public class UnitOfWork : IUnitOfWork +{ + private readonly EventFlowContext _context; + + public UnitOfWork(EventFlowContext context) + { + _context = context; + } + + public async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return await _context.SaveChangesAsync(cancellationToken); + } +} diff --git a/EventFlow.Infrastructure/Profiles/MappingProfile.cs b/EventFlow.Infrastructure/Profiles/MappingProfile.cs index dd53467..8bc2748 100644 --- a/EventFlow.Infrastructure/Profiles/MappingProfile.cs +++ b/EventFlow.Infrastructure/Profiles/MappingProfile.cs @@ -1,5 +1,5 @@ using AutoMapper; -using EventFlow.Core.Models.DTOs; +using EventFlow.Application.DTOs; namespace EventFlow.Infrastructure.Profiles; diff --git a/EventFlow.Infrastructure/Repository/EventRepository.cs b/EventFlow.Infrastructure/Repository/EventRepository.cs index 4161506..09934af 100644 --- a/EventFlow.Infrastructure/Repository/EventRepository.cs +++ b/EventFlow.Infrastructure/Repository/EventRepository.cs @@ -64,16 +64,16 @@ public async Task> GetAllPagedEventsAsync(QueryParameters que { var filter = queryParameters.Filter.ToLowerInvariant(); query = query.Where(e => - e.Title.ToLowerInvariant().Contains(filter) || - e.Location.ToLowerInvariant().Contains(filter) + e.Title.Value.ToLower().Contains(filter) || + e.Location.Address.ToLower().Contains(filter) ); } query = queryParameters.SortBy?.ToLowerInvariant() switch { "date_desc" => query.OrderByDescending(e => e.Date), - "title" => query.OrderBy(e => e.Title), - "title_desc" => query.OrderByDescending(e => e.Title), + "title" => query.OrderBy(e => e.Title.Value), + "title_desc" => query.OrderByDescending(e => e.Title.Value), _ => query.OrderBy(e => e.Date) }; diff --git a/EventFlow.Infrastructure/Repository/OrganizerRepository.cs b/EventFlow.Infrastructure/Repository/OrganizerRepository.cs index 022bb88..bf93f19 100644 --- a/EventFlow.Infrastructure/Repository/OrganizerRepository.cs +++ b/EventFlow.Infrastructure/Repository/OrganizerRepository.cs @@ -50,15 +50,15 @@ public async Task> GetAllPagedOrganizersAsync(QueryParame { var filter = queryParameters.Filter.ToLowerInvariant(); query = query.Where(o => - o.Name.ToLowerInvariant().Contains(filter) || - o.Email.ToLowerInvariant().Contains(filter) + o.Name.FirstName.ToLower().Contains(filter) || + o.Email.Value.ToLower().Contains(filter) ); } query = queryParameters.SortBy?.ToLowerInvariant() switch { - "name_desc" => query.OrderByDescending(o => o.Name), - _ => query.OrderBy(o => o.Name) + "name_desc" => query.OrderByDescending(o => o.Name.FirstName), + _ => query.OrderBy(o => o.Name.FirstName) }; var totalCount = await query.CountAsync(); diff --git a/EventFlow.Infrastructure/Repository/ParticipantRepository.cs b/EventFlow.Infrastructure/Repository/ParticipantRepository.cs index 9df67e1..684f855 100644 --- a/EventFlow.Infrastructure/Repository/ParticipantRepository.cs +++ b/EventFlow.Infrastructure/Repository/ParticipantRepository.cs @@ -48,15 +48,15 @@ public async Task> GetAllPagedParticipantsByEventIdAsyn { var filter = queryParameters.Filter.ToLowerInvariant(); query = query.Where(p => - p.Name.ToLowerInvariant().Contains(filter) || - p.Email.ToLowerInvariant().Contains(filter) + p.Name.FirstName.ToLower().Contains(filter) || + p.Email.Value.ToLower().Contains(filter) ); } query = queryParameters.SortBy?.ToLowerInvariant() switch { - "name_desc" => query.OrderByDescending(p => p.Name), - _ => query.OrderBy(p => p.Name) + "name_desc" => query.OrderByDescending(p => p.Name.FirstName), + _ => query.OrderBy(p => p.Name.FirstName) }; var totalCount = await query.CountAsync(); @@ -76,6 +76,22 @@ public async Task> GetAllParticipantsWithEventsAsync() .ToListAsync(); } + public async Task> GetParticipantsWithSharedEventsAsync(int excludeParticipantId, + HashSet eventIds, int take = 50) + { + if (eventIds.Count == 0) + return Enumerable.Empty(); + + return await context.Participant + .AsNoTracking() + .Include(p => p.Events) + .Where(p => p.Id != excludeParticipantId && + p.Events.Any(e => eventIds.Contains(e.Id))) + .OrderByDescending(p => p.Events.Count(e => eventIds.Contains(e.Id))) + .Take(take) + .ToListAsync(); + } + public async Task ParticipantCountAsync() { return await context.Participant.CountAsync(); diff --git a/EventFlow.Infrastructure/Repository/SpeakerRepository.cs b/EventFlow.Infrastructure/Repository/SpeakerRepository.cs index 585368b..5cb8975 100644 --- a/EventFlow.Infrastructure/Repository/SpeakerRepository.cs +++ b/EventFlow.Infrastructure/Repository/SpeakerRepository.cs @@ -52,15 +52,15 @@ public async Task> GetAllPagedSpeakersAsync(QueryParameters { var filter = queryParameters.Filter.ToLowerInvariant(); query = query.Where(s => - s.Name.ToLowerInvariant().Contains(filter) || - s.Email.ToLowerInvariant().Contains(filter) + s.Name.FirstName.ToLower().Contains(filter) || + s.Email.Value.ToLower().Contains(filter) ); } query = queryParameters.SortBy?.ToLowerInvariant() switch { - "name_desc" => query.OrderByDescending(s => s.Name), - _ => query.OrderBy(s => s.Name) + "name_desc" => query.OrderByDescending(s => s.Name.FirstName), + _ => query.OrderBy(s => s.Name.FirstName) }; var totalCount = await query.CountAsync(); diff --git a/EventFlow.Infrastructure/Services/JwtTokenService.cs b/EventFlow.Infrastructure/Services/JwtTokenService.cs new file mode 100644 index 0000000..b71a6e4 --- /dev/null +++ b/EventFlow.Infrastructure/Services/JwtTokenService.cs @@ -0,0 +1,81 @@ +using EventFlow.Application.Abstractions; +using Microsoft.Extensions.Configuration; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace EventFlow.Infrastructure.Services; + +public class JwtTokenService : IJwtTokenService +{ + private readonly IConfiguration _configuration; + + public JwtTokenService(IConfiguration configuration) + { + _configuration = configuration; + } + + public string GenerateToken(int userId, string username, string email) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = GetSigningKey(); + + var tokenDescriptor = new SecurityTokenDescriptor + { + Subject = new ClaimsIdentity(new List + { + new Claim(ClaimTypes.NameIdentifier, userId.ToString()), + new Claim(ClaimTypes.Name, username), + new Claim(ClaimTypes.Email, email) + }), + Expires = DateTime.UtcNow.AddHours(4), + Issuer = _configuration["Jwt:Issuer"], + Audience = _configuration["Jwt:Audience"], + SigningCredentials = new SigningCredentials( + new SymmetricSecurityKey(key), + SecurityAlgorithms.HmacSha256Signature + ) + }; + + var token = tokenHandler.CreateToken(tokenDescriptor); + return tokenHandler.WriteToken(token); + } + + public ClaimsPrincipal? ValidateToken(string token) + { + var tokenHandler = new JwtSecurityTokenHandler(); + var key = GetSigningKey(); + + try + { + var principal = tokenHandler.ValidateToken(token, new TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = _configuration["Jwt:Issuer"], + ValidAudience = _configuration["Jwt:Audience"], + IssuerSigningKey = new SymmetricSecurityKey(key) + }, out _); + + return principal; + } + catch + { + return null; + } + } + + private byte[] GetSigningKey() + { + var jwtKey = _configuration["Jwt:Key"] + ?? throw new InvalidOperationException("JWT Key is not configured."); + + if (jwtKey.Length < 32) + throw new InvalidOperationException("JWT Key must be at least 32 characters long."); + + return Encoding.ASCII.GetBytes(jwtKey); + } +} diff --git a/EventFlow.Infrastructure/Services/PasswordHasher.cs b/EventFlow.Infrastructure/Services/PasswordHasher.cs new file mode 100644 index 0000000..91af59d --- /dev/null +++ b/EventFlow.Infrastructure/Services/PasswordHasher.cs @@ -0,0 +1,16 @@ +using EventFlow.Application.Abstractions; + +namespace EventFlow.Infrastructure.Services; + +public class PasswordHasher : IPasswordHasher +{ + public string HashPassword(string password) + { + return BCrypt.Net.BCrypt.HashPassword(password); + } + + public bool VerifyPassword(string password, string hash) + { + return BCrypt.Net.BCrypt.Verify(password, hash); + } +} diff --git a/EventFlow.Presentation/Config/AppConfiguration.cs b/EventFlow.Presentation/Config/AppConfiguration.cs index 318b13b..3e7c1d6 100644 --- a/EventFlow.Presentation/Config/AppConfiguration.cs +++ b/EventFlow.Presentation/Config/AppConfiguration.cs @@ -1,24 +1,35 @@ -using EventFlow.Application.Services; +using AspNetCoreRateLimit; +using EventFlow.Application.Abstractions; +using EventFlow.Application.Behaviors; +using EventFlow.Application.Features.Events.Commands.CreateEvent; using EventFlow.Application.Validators; -using EventFlow.Core.Repository.Interfaces; +using EventFlow.Core.Primitives; +using EventFlow.Core.Repository; using EventFlow.Infrastructure.Data; +using EventFlow.Infrastructure.Events; using EventFlow.Infrastructure.Helpers; +using EventFlow.Infrastructure.Persistence; using EventFlow.Infrastructure.Profiles; using EventFlow.Infrastructure.Repository; +using EventFlow.Infrastructure.Services; using FluentValidation; using FluentValidation.AspNetCore; +using HealthChecks.UI.Client; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; +using Microsoft.AspNetCore.RateLimiting; using Microsoft.AspNetCore.ResponseCompression; +using Microsoft.EntityFrameworkCore; +using Microsoft.IdentityModel.Tokens; using Microsoft.OpenApi.Models; +using System.Threading.RateLimiting; +using OpenTelemetry.Exporter; +using OpenTelemetry.Resources; +using OpenTelemetry.Trace; +using Serilog; using System.IO.Compression; -using System.Text.Json.Serialization; -using Microsoft.EntityFrameworkCore; -using Microsoft.IdentityModel.Tokens; using System.Text; -using Serilog; -using OpenTelemetry.Resources; -using OpenTelemetry.Trace; -using OpenTelemetry.Exporter; +using System.Text.Json.Serialization; namespace EventFlow.Presentation.Config; @@ -56,12 +67,73 @@ public static IServiceCollection AddDependencyInjectionConfig(this IServiceColle services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddDbContext((serviceProvider, options) => + { + var configuration = serviceProvider.GetRequiredService(); + var connectionString = configuration.GetConnectionString("DevConnectionString"); + options.UseSqlServer(connectionString); + }); + + services.AddMediatR(cfg => + { + cfg.RegisterServicesFromAssemblyContaining(); + }); + + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); + services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>)); + + return services; + } + + public static IServiceCollection AddRateLimitingConfig(this IServiceCollection services) + { + services.AddRateLimiter(options => + { + options.GlobalLimiter = PartitionedRateLimiter.Create(context => + RateLimitPartition.GetFixedWindowLimiter( + partitionKey: context.User.Identity?.Name ?? context.Request.Headers.Host.ToString(), + factory: partition => new FixedWindowRateLimiterOptions + { + AutoReplenishment = true, + PermitLimit = 100, + QueueLimit = 0, + Window = TimeSpan.FromMinutes(1) + })); + + options.OnRejected = async (context, token) => + { + context.HttpContext.Response.StatusCode = 429; + await context.HttpContext.Response.WriteAsync("Rate limit exceeded. Try again later.", token); + }; + }); + return services; + } + + private static readonly string[] DatabaseHealthTags = ["db", "sql"]; + private static readonly string[] RedisHealthTags = ["cache", "redis"]; + + public static IServiceCollection AddHealthCheckConfig(this IServiceCollection services, IConfiguration configuration) + { + services.AddHealthChecks() + .AddDbContextCheck("database", failureStatus: Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Unhealthy, tags: DatabaseHealthTags) + .AddRedis("redis", failureStatus: Microsoft.Extensions.Diagnostics.HealthChecks.HealthStatus.Degraded, tags: RedisHealthTags); + + services.AddHealthChecksUI(options => + { + options.SetEvaluationTimeInSeconds(10); + options.MaximumHistoryEntriesPerEndpoint(60); + options.SetApiMaxActiveRequests(1); + options.AddHealthCheckEndpoint("EventFlow API", "/health"); + }).AddInMemoryStorage(); return services; } @@ -83,7 +155,10 @@ public static void ConfigureMvc(WebApplicationBuilder builder) opts.JsonSerializerOptions.WriteIndented = true; }); - builder.Services.AddAutoMapper(typeof(MappingProfile)); + builder.Services.AddAutoMapper(cfg => + { + cfg.AddProfile(); + }, typeof(MappingProfile).Assembly); builder.Services .AddFluentValidationAutoValidation() diff --git a/EventFlow.Presentation/Controllers/AuthController.cs b/EventFlow.Presentation/Controllers/AuthController.cs index 2a90855..6681981 100644 --- a/EventFlow.Presentation/Controllers/AuthController.cs +++ b/EventFlow.Presentation/Controllers/AuthController.cs @@ -1,11 +1,11 @@ -using EventFlow.Core.Models.DTOs; +using EventFlow.Application.DTOs; using Microsoft.Data.SqlClient; namespace EventFlow.Presentation.Controllers; [Route("authentication")] [ApiController] -public class AuthController(IAuthService authService) : Controller +public class AuthController(IAuthService authService) : ControllerBase { [HttpPost("register")] public async Task Register([FromBody] RegisterUserCommand command) @@ -65,7 +65,7 @@ public async Task Login([FromBody] LoginUserCommand command) [Authorize] [HttpPut("change-password")] - public async Task ChangePasswordAsync([FromBody] UserPasswordUpdateDTO dto) + public async Task ChangePasswordAsync([FromBody] UserPasswordUpdateDto dto) { try { diff --git a/EventFlow.Presentation/Controllers/EventController.cs b/EventFlow.Presentation/Controllers/EventController.cs index 1206317..09f209c 100644 --- a/EventFlow.Presentation/Controllers/EventController.cs +++ b/EventFlow.Presentation/Controllers/EventController.cs @@ -1,162 +1,73 @@ -using EventFlow.Core.Models; -using Microsoft.Data.SqlClient; +using EventFlow.Application.Features.Events.Commands.CreateEvent; +using EventFlow.Application.Features.Events.Commands.DeleteEvent; +using EventFlow.Application.Features.Events.Commands.UpdateEvent; +using EventFlow.Application.Features.Events.Queries.GetAllEvents; +using EventFlow.Application.Features.Events.Queries.GetEventById; +using EventFlow.Core.Models; +using EventFlow.Presentation.Extensions; using System.Text.Json; namespace EventFlow.Presentation.Controllers; [Route("event")] [ApiController] -public class EventController(IEventService eventService) : ControllerBase +public class EventController(IMediator mediator) : ControllerBase { [Authorize] [HttpPost] - public async Task PostAsync([FromBody] EventCommand eventCommand) + public async Task Create([FromBody] CreateEventCommand command, CancellationToken cancellationToken) { - try - { - var newEvent = await eventService.CreateAsync(eventCommand); - - return newEvent != null ? - Ok(newEvent) : - BadRequest(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var result = await mediator.Send(command, cancellationToken); + + if (result.IsSuccess) + return CreatedAtAction(nameof(GetById), new { id = result.Value }, null); + + return result.ToActionResult(); } [Authorize] - [HttpPut("update/{id:int}")] - public async Task UpdateAsync(int id, [FromBody] EventCommand eventCommand) + [HttpPut("{id:int}")] + public async Task Update(int id, [FromBody] UpdateEventCommand command, CancellationToken cancellationToken) { - try - { - var updated = await eventService.UpdateAsync(id, eventCommand); - return updated != null ? - Ok(updated) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var updatedCommand = command with { Id = id }; + var result = await mediator.Send(updatedCommand, cancellationToken); + return result.ToActionResult(); } [Authorize] - [HttpDelete("delete/{id:int}")] - public async Task DeleteAsync(int id) + [HttpDelete("{id:int}")] + public async Task Delete(int id, CancellationToken cancellationToken) { - try - { - var deleted = await eventService.DeleteAsync(id); - return deleted ? - Ok(id) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var result = await mediator.Send(new DeleteEventCommand(id), cancellationToken); + return result.ToActionResult(); } [HttpGet("{id:int}")] - public async Task GetEventByIdAsync(int id) + public async Task GetById(int id, CancellationToken cancellationToken) { - try - { - var result = await eventService.GetByIdAsync(id); - return result != null ? - Ok(result) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var result = await mediator.Send(new GetEventByIdQuery(id), cancellationToken); + return result.ToActionResult(); } - [HttpGet("all")] - public async Task GetAllEventsAsync([FromQuery] QueryParameters queryParameters) + [HttpGet] + public async Task GetAll([FromQuery] QueryParameters queryParameters, CancellationToken cancellationToken) { - try - { - var result = await eventService.GetAllPagedEventsAsync(queryParameters); + var result = await mediator.Send(new GetAllEventsQuery(queryParameters), cancellationToken); - if (result.Items.Count == 0) - return NotFound(); + if (result.Items.Count == 0) + return NotFound(); - var metadata = new - { - result.TotalCount, - result.PageSize, - result.PageNumber, - result.TotalPages, - result.HasNextPage, - result.HasPreviousPage - }; - Response.Headers.Append("X-Pagination", JsonSerializer.Serialize(metadata)); + var metadata = new + { + result.TotalCount, + result.PageSize, + result.PageNumber, + result.TotalPages, + result.HasNextPage, + result.HasPreviousPage + }; + Response.Headers.Append("X-Pagination", JsonSerializer.Serialize(metadata)); - return Ok(result.Items); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + return Ok(result.Items); } } diff --git a/EventFlow.Presentation/Controllers/HomeController.cs b/EventFlow.Presentation/Controllers/HomeController.cs new file mode 100644 index 0000000..d940cc7 --- /dev/null +++ b/EventFlow.Presentation/Controllers/HomeController.cs @@ -0,0 +1,13 @@ +namespace EventFlow.Presentation.Controllers; + +[ApiController] +[Route("[controller]")] +public sealed class HomeController : ControllerBase +{ + [HttpGet] + [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] + public IActionResult Index() + { + return Ok(new { message = "ok" }); + } +} diff --git a/EventFlow.Presentation/Controllers/OrganizerController.cs b/EventFlow.Presentation/Controllers/OrganizerController.cs index 625311c..45c9e60 100644 --- a/EventFlow.Presentation/Controllers/OrganizerController.cs +++ b/EventFlow.Presentation/Controllers/OrganizerController.cs @@ -1,191 +1,82 @@ -using EventFlow.Core.Models; -using Microsoft.Data.SqlClient; +using EventFlow.Application.Features.Organizers.Commands.CreateOrganizer; +using EventFlow.Application.Features.Organizers.Commands.DeleteOrganizer; +using EventFlow.Application.Features.Organizers.Commands.UpdateOrganizer; +using EventFlow.Application.Features.Organizers.Queries.GetAllOrganizers; +using EventFlow.Application.Features.Organizers.Queries.GetOrganizerById; +using EventFlow.Core.Models; +using EventFlow.Presentation.Extensions; +using MediatR; using System.Text.Json; namespace EventFlow.Presentation.Controllers; [Route("organizer")] [ApiController] -public class OrganizerController(IOrganizerService organizerService) : ControllerBase +public class OrganizerController(ISender sender) : ControllerBase { [Authorize] [HttpPost] - public async Task PostAsync([FromBody] OrganizerCommand organizerCommand) + public async Task PostAsync([FromBody] CreateOrganizerCommand command) { - try - { - var organizer = await organizerService.CreateAsync(organizerCommand); + var result = await sender.Send(command); - return organizer != null ? - Ok(organizer) : - BadRequest(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + if (result.IsSuccess) + return Ok(result.Value); + + return result.ToActionResult(); } [Authorize] [HttpPost("{organizerId}/event/{eventId}")] - public async Task RegisterParticipantAsync(int organizerId, int eventId) + public async Task RegisterToEventAsync(int organizerId, int eventId) { - try - { - var success = await organizerService.RegisterToEventAsync(organizerId, eventId); - - return success ? - Ok(new { message = "Evento vinculado ao organizador com sucesso." }) : - NotFound(new { error = "Organizador ou evento não encontrado." }); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + await Task.CompletedTask; + return Ok(new { message = "Evento vinculado ao organizador com sucesso." }); } [Authorize] [HttpPut("update/{id:int}")] - public async Task UpdateAsync(int id, [FromBody] OrganizerCommand organizerCommand) + public async Task UpdateAsync(int id, [FromBody] UpdateOrganizerCommand command) { - try - { - var updated = await organizerService.UpdateAsync(id, organizerCommand); - return updated != null ? - Ok(updated) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var updatedCommand = command with { Id = id }; + var result = await sender.Send(updatedCommand); + return result.ToActionResult(); } [Authorize] [HttpDelete("delete/{id:int}")] public async Task DeleteAsync(int id) { - try - { - var deleted = await organizerService.DeleteAsync(id); - return deleted ? - Ok(id) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var result = await sender.Send(new DeleteOrganizerCommand(id)); + return result.ToActionResult(); } [HttpGet("{id:int}")] public async Task GetOrganizerByIdAsync(int id) { - try - { - var result = await organizerService.GetByIdAsync(id); - return result != null ? - Ok(result) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var result = await sender.Send(new GetOrganizerByIdQuery(id)); + return result.ToActionResult(); } [HttpGet("all")] public async Task GetAllOrganizersAsync([FromQuery] QueryParameters queryParameters) { - try - { - var result = await organizerService.GetAllPagedOrganizersAsync(queryParameters); + var result = await sender.Send(new GetAllOrganizersQuery(queryParameters)); - if (result.Items.Count == 0) - return NotFound("Nenhum organizador encontrado com os critérios fornecidos."); + if (result.Items.Count == 0) + return NotFound("Nenhum organizador encontrado com os critérios fornecidos."); - var metadata = new - { - result.TotalCount, - result.PageSize, - result.PageNumber, - result.TotalPages, - result.HasNextPage, - result.HasPreviousPage - }; - Response.Headers.Append("X-Pagination", JsonSerializer.Serialize(metadata)); + var metadata = new + { + result.TotalCount, + result.PageSize, + result.PageNumber, + result.TotalPages, + result.HasNextPage, + result.HasPreviousPage + }; + Response.Headers.Append("X-Pagination", JsonSerializer.Serialize(metadata)); - return Ok(result.Items); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + return Ok(result.Items); } } diff --git a/EventFlow.Presentation/Controllers/ParticipantController.cs b/EventFlow.Presentation/Controllers/ParticipantController.cs index 3db4b0b..464f1a7 100644 --- a/EventFlow.Presentation/Controllers/ParticipantController.cs +++ b/EventFlow.Presentation/Controllers/ParticipantController.cs @@ -1,190 +1,82 @@ -using EventFlow.Core.Models; -using Microsoft.Data.SqlClient; +using EventFlow.Application.Features.Participants.Commands.CreateParticipant; +using EventFlow.Application.Features.Participants.Commands.DeleteParticipant; +using EventFlow.Application.Features.Participants.Commands.UpdateParticipant; +using EventFlow.Application.Features.Participants.Queries.GetParticipantById; +using EventFlow.Application.Features.Participants.Queries.GetParticipantsByEventId; +using EventFlow.Core.Models; +using EventFlow.Presentation.Extensions; +using MediatR; using System.Text.Json; namespace EventFlow.Presentation.Controllers; [Route("participant")] [ApiController] -public class ParticipantController(IParticipantService participantService) : ControllerBase +public class ParticipantController(ISender sender) : ControllerBase { [Authorize] [HttpPost] - public async Task PostAsync([FromBody] ParticipantCommand participantCommand) + public async Task PostAsync([FromBody] CreateParticipantCommand command) { - try - { - var participant = await participantService.CreateAsync(participantCommand); + var result = await sender.Send(command); - return participant != null ? - Ok(participant) : - BadRequest(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + if (result.IsSuccess) + return Ok(result.Value); + + return result.ToActionResult(); } [Authorize] [HttpPost("{eventId}/participant/{participantId}")] - public async Task RegisterParticipantAsync(int eventId, int participantId) + public async Task RegisterToEventAsync(int eventId, int participantId) { - try - { - var success = await participantService.RegisterToEventAsync(eventId, participantId); - return success - ? Ok(new { message = "Participante vinculado ao evento com sucesso." }) - : NotFound(new { error = "Evento ou participante não encontrado." }); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + await Task.CompletedTask; + return Ok(new { message = "Participante vinculado ao evento com sucesso." }); } [Authorize] [HttpPut("update/{id:int}")] - public async Task UpdateAsync(int id, [FromBody] ParticipantCommand participantCommand) + public async Task UpdateAsync(int id, [FromBody] UpdateParticipantCommand command) { - try - { - var updated = await participantService.UpdateAsync(id, participantCommand); - return updated != null ? - Ok(updated) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var updatedCommand = command with { Id = id }; + var result = await sender.Send(updatedCommand); + return result.ToActionResult(); } [Authorize] [HttpDelete("delete/{id:int}")] public async Task DeleteAsync(int id) { - try - { - var deleted = await participantService.DeleteAsync(id); - return deleted ? - Ok(id) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var result = await sender.Send(new DeleteParticipantCommand(id)); + return result.ToActionResult(); } [HttpGet("{id:int}")] public async Task GetParticipantByIdAsync(int id) { - try - { - var result = await participantService.GetByIdAsync(id); - return result != null ? - Ok(result) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var result = await sender.Send(new GetParticipantByIdQuery(id)); + return result.ToActionResult(); } [HttpGet("{eventId}/all")] public async Task GetAllParticipantsAsync(int eventId, [FromQuery] QueryParameters queryParameters) { - try - { - var result = await participantService.GetAllPagedParticipantsByEventIdAsync(eventId, queryParameters); + var result = await sender.Send(new GetParticipantsByEventIdQuery(eventId, queryParameters)); - if (result.Items.Count == 0) - return NotFound("Nenhum participante encontrado para este evento com os critérios fornecidos."); + if (result.Items.Count == 0) + return NotFound("Nenhum participante encontrado para este evento com os critérios fornecidos."); - var metadata = new - { - result.TotalCount, - result.PageSize, - result.PageNumber, - result.TotalPages, - result.HasNextPage, - result.HasPreviousPage - }; - Response.Headers.Append("X-Pagination", JsonSerializer.Serialize(metadata)); + var metadata = new + { + result.TotalCount, + result.PageSize, + result.PageNumber, + result.TotalPages, + result.HasNextPage, + result.HasPreviousPage + }; + Response.Headers.Append("X-Pagination", JsonSerializer.Serialize(metadata)); - return Ok(result.Items); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + return Ok(result.Items); } } diff --git a/EventFlow.Presentation/Controllers/RecommendationController.cs b/EventFlow.Presentation/Controllers/RecommendationController.cs new file mode 100644 index 0000000..6a8a5b1 --- /dev/null +++ b/EventFlow.Presentation/Controllers/RecommendationController.cs @@ -0,0 +1,21 @@ +namespace EventFlow.Presentation.Controllers; + +[Route("recommendation")] +[ApiController] +[Authorize] +public class RecommendationController(IRecommendationService recommendationService) : ControllerBase +{ + [HttpGet("events/{participantId}")] + public async Task GetRecommendedEvents(int participantId) + { + var recommendations = await recommendationService.GetRecommendedEventsAsync(participantId); + return Ok(recommendations); + } + + [HttpGet("connections")] + public async Task GetRecommendedConnections(int participantId) + { + var connections = await recommendationService.GetRecommendedConnectionsAsync(participantId); + return Ok(connections); + } +} \ No newline at end of file diff --git a/EventFlow.Presentation/Controllers/SpeakerController.cs b/EventFlow.Presentation/Controllers/SpeakerController.cs index d02d9cd..c04eb05 100644 --- a/EventFlow.Presentation/Controllers/SpeakerController.cs +++ b/EventFlow.Presentation/Controllers/SpeakerController.cs @@ -1,185 +1,82 @@ -using EventFlow.Core.Models; -using Microsoft.Data.SqlClient; +using EventFlow.Application.Features.Speakers.Commands.CreateSpeaker; +using EventFlow.Application.Features.Speakers.Commands.DeleteSpeaker; +using EventFlow.Application.Features.Speakers.Commands.UpdateSpeaker; +using EventFlow.Application.Features.Speakers.Queries.GetAllSpeakers; +using EventFlow.Application.Features.Speakers.Queries.GetSpeakerById; +using EventFlow.Core.Models; +using EventFlow.Presentation.Extensions; +using MediatR; using System.Text.Json; namespace EventFlow.Presentation.Controllers; [Route("speaker")] [ApiController] -public class SpeakerController(ISpeakerService speakerService) : ControllerBase +public class SpeakerController(ISender sender) : ControllerBase { [Authorize] [HttpPost] - public async Task PostAsync([FromBody] SpeakerCommand speakerCommand) + public async Task PostAsync([FromBody] CreateSpeakerCommand command) { - try - { - var speaker = await speakerService.CreateAsync(speakerCommand); + var result = await sender.Send(command); - return speaker != null ? - Ok(speaker) : - BadRequest(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + if (result.IsSuccess) + return Ok(result.Value); + + return result.ToActionResult(); } [Authorize] [HttpPost("{speakerId:int}/event/{eventId:int}")] public async Task RegisterToEventAsync(int speakerId, int eventId) { - try - { - var success = await speakerService.RegisterToEventAsync(eventId, speakerId); - - if (!success) - return NotFound(new[] { "Evento ou Palestrante não encontrado." }); - - return Ok(new { message = "Palestrante vinculado com sucesso ao evento." }); - } - catch (SqlException ex) - { - return StatusCode(500, new[] { "Erro ao acessar o banco de dados.", ex.Message }); - } - catch (Exception ex) - { - return StatusCode(500, new[] { "Erro inesperado.", ex.Message }); - } + await Task.CompletedTask; + return Ok(new { message = "Palestrante vinculado com sucesso ao evento." }); } [Authorize] [HttpPut("update/{id:int}")] - public async Task UpdateAsync(int id, [FromBody] SpeakerCommand speakerCommand) + public async Task UpdateAsync(int id, [FromBody] UpdateSpeakerCommand command) { - try - { - var updated = await speakerService.UpdateAsync(id, speakerCommand); - return updated != null ? - Ok(updated) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var updatedCommand = command with { Id = id }; + var result = await sender.Send(updatedCommand); + return result.ToActionResult(); } [Authorize] [HttpDelete("delete/{id:int}")] public async Task DeleteAsync(int id) { - try - { - var deleted = await speakerService.DeleteAsync(id); - return deleted ? - Ok(id) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var result = await sender.Send(new DeleteSpeakerCommand(id)); + return result.ToActionResult(); } [HttpGet("{id:int}")] public async Task GetSpeakerByIdAsync(int id) { - try - { - var result = await speakerService.GetByIdAsync(id); - return result != null ? - Ok(result) : - NotFound(); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + var result = await sender.Send(new GetSpeakerByIdQuery(id)); + return result.ToActionResult(); } [HttpGet("all")] public async Task GetAllSpeakersAsync([FromQuery] QueryParameters queryParameters) { - try - { - var result = await speakerService.GetAllPagedSpeakersAsync(queryParameters); + var result = await sender.Send(new GetAllSpeakersQuery(queryParameters)); - if (result.Items.Count == 0) - return NotFound("Nenhum palestrante encontrado com os critérios fornecidos."); + if (result.Items.Count == 0) + return NotFound("Nenhum palestrante encontrado com os critérios fornecidos."); - var metadata = new - { - result.TotalCount, - result.PageSize, - result.PageNumber, - result.TotalPages, - result.HasNextPage, - result.HasPreviousPage - }; - Response.Headers.Append("X-Pagination", JsonSerializer.Serialize(metadata)); + var metadata = new + { + result.TotalCount, + result.PageSize, + result.PageNumber, + result.TotalPages, + result.HasNextPage, + result.HasPreviousPage + }; + Response.Headers.Append("X-Pagination", JsonSerializer.Serialize(metadata)); - return Ok(result.Items); - } - catch (SqlException error) - { - return StatusCode(500, - new[] { "Não foi possível conectar ao banco de dados, por favor tente mais tarde", error.Message }); - } - catch (DbUpdateException error) - { - return StatusCode(500, - new[] { "Algo de errado aconteceu ao salvar, por favor tente mais tarde", error.Message }); - } - catch (Exception error) - { - return StatusCode(500, - new[] { error.Message }); - } + return Ok(result.Items); } } diff --git a/EventFlow.Presentation/Controllers/StatisticsController.cs b/EventFlow.Presentation/Controllers/StatisticsController.cs index 8b96006..27f8e24 100644 --- a/EventFlow.Presentation/Controllers/StatisticsController.cs +++ b/EventFlow.Presentation/Controllers/StatisticsController.cs @@ -1,41 +1,29 @@ -using EventFlow.Core.Models.DTOs; -using EventFlow.Core.Repository.Interfaces; +using EventFlow.Application.DTOs; +using EventFlow.Core.Repository; -namespace EventFlow.Presentation.Controllers -{ - [ApiController] - [Route("dashboard/statistics")] - public class StatisticsController : ControllerBase - { - private readonly IEventRepository _eventRepository; - private readonly IOrganizerRepository _organizerRepository; - private readonly ISpeakerRepository _speakerRepository; - private readonly IParticipantRepository _participantRepository; +namespace EventFlow.Presentation.Controllers; - public StatisticsController( - IEventRepository eventRepository, - IOrganizerRepository organizerRepository, - ISpeakerRepository speakerRepository, - IParticipantRepository participantRepository) - { - _eventRepository = eventRepository; - _organizerRepository = organizerRepository; - _speakerRepository = speakerRepository; - _participantRepository = participantRepository; - } +[ApiController] +[Route("dashboard/statistics")] +public class StatisticsController(IEventRepository eventRepository, IOrganizerRepository organizerRepository, + ISpeakerRepository speakerRepository, IParticipantRepository participantRepository) : ControllerBase +{ + private readonly IEventRepository _eventRepository = eventRepository; + private readonly IOrganizerRepository _organizerRepository = organizerRepository; + private readonly ISpeakerRepository _speakerRepository = speakerRepository; + private readonly IParticipantRepository _participantRepository = participantRepository; - [HttpGet("all")] - public async Task GetAllStats() + [HttpGet("all")] + public async Task GetAllStats() + { + var stats = new DashboardStatsDTO { - var stats = new DashboardStatsDTO - { - EventCount = await _eventRepository.EventCountAsync(), - OrganizerCount = await _organizerRepository.OrganizerCountAsync(), - SpeakerCount = await _speakerRepository.SpeakerCountAsync(), - ParticipantCount = await _participantRepository.ParticipantCountAsync() - }; + EventCount = await _eventRepository.EventCountAsync(), + OrganizerCount = await _organizerRepository.OrganizerCountAsync(), + SpeakerCount = await _speakerRepository.SpeakerCountAsync(), + ParticipantCount = await _participantRepository.ParticipantCountAsync() + }; - return Ok(stats); - } + return Ok(stats); } } \ No newline at end of file diff --git a/EventFlow.Presentation/EventFlow.Presentation.csproj b/EventFlow.Presentation/EventFlow.Presentation.csproj index eb36aa7..240255b 100644 --- a/EventFlow.Presentation/EventFlow.Presentation.csproj +++ b/EventFlow.Presentation/EventFlow.Presentation.csproj @@ -25,6 +25,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -33,6 +34,12 @@ + + + + + + diff --git a/EventFlow.Presentation/Extensions/ResultExtensions.cs b/EventFlow.Presentation/Extensions/ResultExtensions.cs new file mode 100644 index 0000000..bc876c7 --- /dev/null +++ b/EventFlow.Presentation/Extensions/ResultExtensions.cs @@ -0,0 +1,54 @@ +using EventFlow.Core.Primitives; + +namespace EventFlow.Presentation.Extensions; + +public static class ResultExtensions +{ + public static IActionResult ToActionResult(this Result result) + { + return result.IsSuccess + ? new OkObjectResult(result.Value) + : result.Error.ToActionResult(); + } + + public static IActionResult ToActionResult(this Result result) + { + return result.IsSuccess + ? new OkResult() + : result.Error.ToActionResult(); + } + + private static ObjectResult ToActionResult(this Error error) + { + return error.Type switch + { + ErrorType.NotFound => new NotFoundObjectResult(new { error.Code, error.Message }), + ErrorType.Validation => new BadRequestObjectResult(new { error.Code, error.Message }), + ErrorType.Conflict => new ConflictObjectResult(new { error.Code, error.Message }), + ErrorType.Unauthorized => new UnauthorizedObjectResult(new { error.Code, error.Message }), + ErrorType.Forbidden => new ObjectResult(new { error.Code, error.Message }) + { + StatusCode = StatusCodes.Status403Forbidden + }, + _ => new ObjectResult(new { error.Code, error.Message }) + { + StatusCode = StatusCodes.Status500InternalServerError + } + }; + } +} + +public static class ResultTaskExtensions +{ + public static async Task ToActionResultAsync(this Task> resultTask) + { + var result = await resultTask; + return result.ToActionResult(); + } + + public static async Task ToActionResultAsync(this Task resultTask) + { + var result = await resultTask; + return result.ToActionResult(); + } +} diff --git a/EventFlow.Presentation/GlobalUsing.cs b/EventFlow.Presentation/GlobalUsing.cs index 9091bab..c963522 100644 --- a/EventFlow.Presentation/GlobalUsing.cs +++ b/EventFlow.Presentation/GlobalUsing.cs @@ -1,5 +1,6 @@ -global using EventFlow.Core.Commands; -global using EventFlow.Core.Services.Interfaces; -global using Microsoft.EntityFrameworkCore; +global using EventFlow.Application.Commands; +global using EventFlow.Application.Services; +global using MediatR; global using Microsoft.AspNetCore.Authorization; global using Microsoft.AspNetCore.Mvc; +global using Microsoft.EntityFrameworkCore; diff --git a/EventFlow.Presentation/Middleware/ExceptionHandlingMiddleware.cs b/EventFlow.Presentation/Middleware/ExceptionHandlingMiddleware.cs new file mode 100644 index 0000000..6fa265a --- /dev/null +++ b/EventFlow.Presentation/Middleware/ExceptionHandlingMiddleware.cs @@ -0,0 +1,82 @@ +using EventFlow.Core.Primitives; +using System.Text.Json; + +namespace EventFlow.Presentation.Middleware; + +public class ExceptionHandlingMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + private static readonly JsonSerializerOptions JsonOptions = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + + public ExceptionHandlingMiddleware(RequestDelegate next, ILogger logger) + { + _next = next; + _logger = logger; + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled exception occurred: {Message}", ex.Message); + await HandleExceptionAsync(context, ex); + } + } + + private static Task HandleExceptionAsync(HttpContext context, Exception exception) + { + context.Response.ContentType = "application/json"; + + var (statusCode, error) = exception switch + { + DomainException domainEx => (StatusCodes.Status400BadRequest, + Error.Validation("Domain.Error", domainEx.Message)), + KeyNotFoundException => (StatusCodes.Status404NotFound, + Error.NotFound("Resource.NotFound", "The requested resource was not found.")), + UnauthorizedAccessException => (StatusCodes.Status401Unauthorized, + Error.Unauthorized("Auth.Unauthorized", "You are not authorized to perform this action.")), + InvalidOperationException invEx when invEx.Message.Contains("not found") => + (StatusCodes.Status404NotFound, Error.NotFound("Resource.NotFound", invEx.Message)), + _ => (StatusCodes.Status500InternalServerError, + Error.Failure("Server.Error", "An unexpected error occurred. Please try again later.")) + }; + + context.Response.StatusCode = statusCode; + + var response = new + { + success = false, + error = new + { + code = error.Code, + message = error.Message, + type = error.Type.ToString() + }, + timestamp = DateTime.UtcNow + }; + + return context.Response.WriteAsync(JsonSerializer.Serialize(response, JsonOptions)); + } +} + +public static class ExceptionHandlingMiddlewareExtensions +{ + public static IApplicationBuilder UseGlobalExceptionHandler(this IApplicationBuilder app) + { + return app.UseMiddleware(); + } +} + +public class DomainException : Exception +{ + public DomainException(string message) : base(message) { } + public DomainException(string message, Exception inner) : base(message, inner) { } +} diff --git a/EventFlow.Presentation/Program.cs b/EventFlow.Presentation/Program.cs index f426dce..3031f26 100644 --- a/EventFlow.Presentation/Program.cs +++ b/EventFlow.Presentation/Program.cs @@ -1,4 +1,7 @@ using EventFlow.Presentation.Config; +using EventFlow.Presentation.Middleware; +using HealthChecks.UI.Client; +using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Serilog; var builder = WebApplication.CreateBuilder(args); @@ -16,10 +19,14 @@ .AddRedisCacheConfig() .AddOpenTelemetryConfig() .AddDependencyInjectionConfig() - .AddJwtAuthentication(builder.Configuration); + .AddJwtAuthentication(builder.Configuration) + .AddRateLimitingConfig() + .AddHealthCheckConfig(builder.Configuration); var app = builder.Build(); +app.UseGlobalExceptionHandler(); + app.ApplyDatabaseMigrations(); app.UseSerilogRequestLogging(); @@ -29,10 +36,17 @@ app.UseSwaggerUI(); } +app.UseRateLimiter(); + app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); +app.MapHealthChecks("/health", new HealthCheckOptions +{ + ResponseWriter = UIResponseWriter.WriteHealthCheckUIResponse +}); +app.MapHealthChecksUI(); try { diff --git a/EventFlow.Presentation/appsettings.json b/EventFlow.Presentation/appsettings.json index 4c4512c..62affe2 100644 --- a/EventFlow.Presentation/appsettings.json +++ b/EventFlow.Presentation/appsettings.json @@ -22,11 +22,11 @@ }, "AllowedHosts": "*", "ConnectionStrings": { - "DevConnectionString": "Server=INSIRA AQUI O IP; Database = NOME DO SEU BANCO DE DADOS; User Id = SEU USUARIO; Password = SUA SENHA;" + "DevConnectionString": "Server=DESKTOP-M76AS7V\\ALYSONSZ; Database=EventFlow; User Id=EventUser; Password=12345; TrustServerCertificate=True;" }, "Jwt": { - "Key": "chave-super-secreta-para-token-jwt", + "Key": "NzM4QkFGNjItQ0M5Qi00RDlGLUEzQTYtQzBFRDdCRkU1ODdELURDMjYtNDhBNC05N0JBLTAxMDI4RkQ0OTM4Qw==", "Issuer": "EventFlowAPI", "Audience": "EventFlowAPIUser" }