diff --git a/Controllers/WorkshopsController.cs b/Controllers/WorkshopsController.cs
index aaee027..ef4b995 100644
--- a/Controllers/WorkshopsController.cs
+++ b/Controllers/WorkshopsController.cs
@@ -1,8 +1,9 @@
-
using CampusWorkshops.Api.Dtos;
using CampusWorkshops.Api.Models;
using CampusWorkshops.Api.Repositories;
+using FluentValidation;
using Microsoft.AspNetCore.Mvc;
+using Workshop10_API.Validation;
namespace CampusWorkshops.Api.Controllers;
@@ -11,30 +12,67 @@ namespace CampusWorkshops.Api.Controllers;
public class WorkshopsController : ControllerBase
{
private readonly IWorkshopRepository _repo;
-
public WorkshopsController(IWorkshopRepository repo) => _repo = repo;
/// Lista workshops com filtros opcionais.
[HttpGet]
- public Task GetAll([FromQuery] DateTimeOffset? from, [FromQuery] DateTimeOffset? to, [FromQuery] string? q, CancellationToken ct)
+ public async Task GetAll([FromQuery] DateTimeOffset? from, [FromQuery] DateTimeOffset? to, [FromQuery] string? q, CancellationToken ct)
{
- // TODO: implementar usando _repo.GetAllAsync e mapear para WorkshopResponse
- return Task.FromResult(Ok(Array.Empty()));
+ var workshops = await _repo.GetAllAsync(from, to, q, ct);
+ if (!workshops.Any())
+ return NoContent();
+
+ return Ok(workshops);
}
- /// Obtém um workshop por Id.
[HttpGet("{id:guid}")]
- public Task GetById(Guid id, CancellationToken ct)
+ public async Task GetById(Guid id, CancellationToken ct)
{
- // TODO: implementar usando _repo.GetByIdAsync
- return Task.FromResult(NotFound());
+ var result = await _repo.GetByIdAsync(id, ct);
+
+ if (result == null)
+ return NoContent();
+
+ return Ok(result);
}
- /// Cria um novo workshop.
[HttpPost]
- public Task Create([FromBody] CreateWorkshopRequest body, CancellationToken ct)
+ public async Task Create([FromBody] CreateWorkshopRequestDTO workshopDTO, CancellationToken ct)
{
- // TODO: validar ModelState, regras de negócio e chamar _repo.AddAsync; retornar CreatedAtAction
- return Task.FromResult(BadRequest());
+ var dtoValidator = new WorkshopRequestDTOValidator();
+ var validationResult = await dtoValidator.ValidateAsync(workshopDTO);
+ if (!validationResult.IsValid)
+ {
+ var erros = validationResult.Errors.Select(e => new { e.PropertyName, e.ErrorMessage });
+ return BadRequest(erros);
+ }
+
+ Workshop workshop = new Workshop
+ {
+ Capacity = workshopDTO.Capacity,
+ Description = workshopDTO.Description,
+ EndAt = workshopDTO.EndAt,
+ IsOnline = workshopDTO.IsOnline,
+ Location = workshopDTO.Location,
+ StartAt = workshopDTO.StartAt,
+ Title = workshopDTO.Title
+ };
+
+ var result = await _repo.AddAsync(workshop, ct);
+
+ if (result != null)
+ return CreatedAtAction(nameof(GetById), new { workshopDTO });
+
+ return BadRequest();
+ }
+
+ [HttpDelete]
+ public async Task Delete(Guid id, CancellationToken ct)
+ {
+ var result = await _repo.DeleteAsync(id, ct);
+ if(result)
+ return NoContent();
+
+ return NotFound();
}
}
diff --git a/Dtos/CreateWorkshopRequest.cs b/Dtos/CreateWorkshopRequestDTO.cs
similarity index 72%
rename from Dtos/CreateWorkshopRequest.cs
rename to Dtos/CreateWorkshopRequestDTO.cs
index 7686335..b182b9b 100644
--- a/Dtos/CreateWorkshopRequest.cs
+++ b/Dtos/CreateWorkshopRequestDTO.cs
@@ -1,8 +1,7 @@
namespace CampusWorkshops.Api.Dtos;
-// DTO para criação de Workshop.
-public record CreateWorkshopRequest(
+public record CreateWorkshopRequestDTO(
string? Title,
string? Description,
DateTimeOffset StartAt,
@@ -10,4 +9,4 @@ public record CreateWorkshopRequest(
string? Location,
int Capacity,
bool IsOnline
-);
+);
\ No newline at end of file
diff --git a/Dtos/WorkshopResponse.cs b/Dtos/WorkshopResponseDTO.cs
similarity index 74%
rename from Dtos/WorkshopResponse.cs
rename to Dtos/WorkshopResponseDTO.cs
index 161317a..b825c13 100644
--- a/Dtos/WorkshopResponse.cs
+++ b/Dtos/WorkshopResponseDTO.cs
@@ -1,8 +1,7 @@
namespace CampusWorkshops.Api.Dtos;
-// DTO de resposta para Workshop
-public record WorkshopResponse(
+public record WorkshopResponseDTO(
Guid Id,
string? Title,
string? Description,
diff --git a/Models/Workshop.cs b/Models/Workshop.cs
index 64a19d2..1fdba8e 100644
--- a/Models/Workshop.cs
+++ b/Models/Workshop.cs
@@ -1,28 +1,19 @@
-
namespace CampusWorkshops.Api.Models;
-// Modelo mínimo para Workshop.Completar as validações
-// e ajustar os tipos/atributos durante a atividade.
public class Workshop
{
- // Identificador único
public Guid Id { get; set; } = Guid.NewGuid();
- // TODO: adicionar [Required], [StringLength(120, MinimumLength = 3)]
public string? Title { get; set; }
- // TODO: limitar tamanho (ex.: 2000)
public string? Description { get; set; }
- // TODO: usar DateTimeOffset com formato ISO 8601
public DateTimeOffset StartAt { get; set; }
-
+
public DateTimeOffset EndAt { get; set; }
- // Location deve ser obrigatório se IsOnline == false
public string? Location { get; set; }
- // TODO: validar Capacity >= 1
public int Capacity { get; set; } = 1;
public bool IsOnline { get; set; }
diff --git a/Program.cs b/Program.cs
index 29f9166..f0089b6 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,23 +1,26 @@
// Note: repository implementation removed for workshop exercise (TODOs in project files)
+using CampusWorkshops.Api.Repositories;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Mvc;
+using SharpGrip.FluentValidation.AutoValidation.Mvc.Extensions;
var builder = WebApplication.CreateBuilder(args);
-// Add services
builder.Services.AddControllers();
+
+builder.Services.AddTransient();
+builder.Services.AddFluentValidationAutoValidation();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(o =>
{
o.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
{
- Title = "CampusWorkshops API",
+ Title = "Workshop 10 - API's RESTFul",
Version = "v1",
Description = "API para gestão de workshops do campus (MVP in-memory)."
});
});
-// DI
var app = builder.Build();
diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json
index d373fdd..4a73244 100644
--- a/Properties/launchSettings.json
+++ b/Properties/launchSettings.json
@@ -5,7 +5,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
- "applicationUrl": "http://localhost:5298",
+ "applicationUrl": "http://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
@@ -14,7 +14,7 @@
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
- "applicationUrl": "https://localhost:7006;http://localhost:5298",
+ "applicationUrl": "https://localhost:7006;http://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
diff --git a/Repositories/IWorkshopRepository.cs b/Repositories/IWorkshopRepository.cs
index ff8bfda..e3cd381 100644
--- a/Repositories/IWorkshopRepository.cs
+++ b/Repositories/IWorkshopRepository.cs
@@ -9,4 +9,5 @@ public interface IWorkshopRepository
Task> GetAllAsync(DateTimeOffset? from, DateTimeOffset? to, string? q, CancellationToken ct);
Task GetByIdAsync(Guid id, CancellationToken ct);
Task AddAsync(Workshop workshop, CancellationToken ct);
+ Task DeleteAsync(Guid id, CancellationToken ct);
}
diff --git a/Repositories/InMemoryWorkshopRepository.cs b/Repositories/InMemoryWorkshopRepository.cs
index e887e44..996c211 100644
--- a/Repositories/InMemoryWorkshopRepository.cs
+++ b/Repositories/InMemoryWorkshopRepository.cs
@@ -6,26 +6,107 @@ namespace CampusWorkshops.Api.Repositories;
// In-memory repository stub
public class InMemoryWorkshopRepository : IWorkshopRepository
{
+ private readonly List workshops = new List();
+
public InMemoryWorkshopRepository()
{
- // TODO: Adicionar workshops iniciais
+ workshops.AddRange(Seed());
}
- public Task> GetAllAsync(DateTimeOffset? from, DateTimeOffset? to, string? q, CancellationToken ct)
+ public async Task GetByIdAsync(Guid id, CancellationToken ct)
{
- // TODO: retornar uma lista filtrada e ordenada por StartAt
- return Task.FromResult>(Array.Empty());
+ ct.ThrowIfCancellationRequested();
+
+ Workshop workshop = workshops.FirstOrDefault(w => w.Id == id);
+ if(workshop == null) return await Task.FromResult(null);
+ return await Task.FromResult(workshop);
}
- public Task GetByIdAsync(Guid id, CancellationToken ct)
+ public async Task AddAsync(Workshop workshop, CancellationToken ct)
{
- // TODO: buscar por id
- return Task.FromResult(null);
+ ct.ThrowIfCancellationRequested();
+ ArgumentNullException.ThrowIfNull(workshop);
+
+ workshops.Add(workshop);
+
+ return await Task.FromResult(workshop);
+
}
- public Task AddAsync(Workshop workshop, CancellationToken ct)
+ public async Task> GetAllAsync(DateTimeOffset? from, DateTimeOffset? to, string? q, CancellationToken ct)
{
- // TODO: adicionar à lista em memória e retornar criado
- return Task.FromResult(workshop);
+ ct.ThrowIfCancellationRequested();
+
+ IEnumerable retorno = workshops;
+ if (from.HasValue)
+ retorno = retorno.Where(w => w.StartAt >= from.Value);
+ if (to.HasValue)
+ retorno = retorno.Where(w => w.StartAt <= to.Value);
+ if (!string.IsNullOrWhiteSpace(q))
+ retorno = retorno.Where(w => (!string.IsNullOrWhiteSpace(w.Title) && w.Title.Contains(q)));
+ IReadOnlyList orderedWorkshops = retorno.OrderBy(r => r.StartAt).ToList();
+
+ return await Task.FromResult>(orderedWorkshops);
}
+ public async Task DeleteAsync(Guid id, CancellationToken ct)
+ {
+ bool retorno = false;
+ Workshop workshop = await GetByIdAsync(id, ct);
+ if(workshop != null)
+ retorno = workshops.Remove(workshop);
+
+ return retorno;
+ }
+
+ ///
+ /// Método auxiliar para popular o repositório in memory com alguns workshops de exemplo
+ ///
+ private static List Seed()
+ {
+ // Base de datas: sempre a partir da próxima semana, para não “envelhecer” o seed
+ var baseDate = DateTimeOffset.UtcNow.Date.AddDays(7);
+ List workShops = new();
+ var w1 = new Workshop
+ {
+ Id = Guid.Parse("8a0b9f1b-6c9b-4a2a-9d5e-1c2f3a4b5c6d"),
+ Title = "APIs RESTful — visão geral",
+ Description = "Modelagem de recursos, status codes e boas práticas.",
+ StartAt = baseDate.AddHours(12), // daqui a 7 dias, 12:00 UTC
+ EndAt = baseDate.AddHours(15), // 15:00 UTC
+ Location = "Lab 101",
+ Capacity = 25,
+ IsOnline = false
+ };
+
+ var w2 = new Workshop
+ {
+ Id = Guid.Parse("1c3d5e7f-9a2b-4c6d-8e0f-1a2b3c4d5e6f"),
+ Title = "Entity Framework — mapeamentos e LINQ",
+ Description = "Relações, consultas eficientes e boas práticas.",
+ StartAt = baseDate.AddDays(2).AddHours(12), // +2 dias, 12:00 UTC
+ EndAt = baseDate.AddDays(2).AddHours(14), // 14:00 UTC
+ Location = null, // online
+ Capacity = 40,
+ IsOnline = true
+ };
+
+ var w3 = new Workshop
+ {
+ Id = Guid.Parse("f0e1d2c3-b4a5-6789-9a8b-7c6d5e4f3a2b"),
+ Title = "Construindo contratos com Swagger/OpenAPI",
+ Description = "Documentação viva, exemplos e testes via Swagger UI.",
+ StartAt = baseDate.AddDays(5).AddHours(13), // +5 dias, 13:00 UTC
+ EndAt = baseDate.AddDays(5).AddHours(16), // 16:00 UTC
+ Location = "Auditório Central",
+ Capacity = 80,
+ IsOnline = false
+ };
+
+ workShops.Add(w1);
+ workShops.Add(w2);
+ workShops.Add(w3);
+ return workShops;
+ }
+
+
}
diff --git a/Validation/WorkshopRequestDTOValidator.cs b/Validation/WorkshopRequestDTOValidator.cs
new file mode 100644
index 0000000..3c6d496
--- /dev/null
+++ b/Validation/WorkshopRequestDTOValidator.cs
@@ -0,0 +1,33 @@
+using CampusWorkshops.Api.Dtos;
+using FluentValidation;
+
+namespace Workshop10_API.Validation
+{
+ public class WorkshopRequestDTOValidator : AbstractValidator
+ {
+ public WorkshopRequestDTOValidator()
+ {
+ RuleFor(x => x.Title)
+ .NotEmpty().WithMessage("O título é obrigatório.")
+ .Length(3, 120).WithMessage("O título deve ter entre 3 e 120 caracteres.");
+
+ RuleFor(x => x.Capacity)
+ .GreaterThan(0).WithMessage("Capacidade deve ser no mínimo 1.");
+
+ RuleFor(x => x.StartAt)
+ .LessThan(x => x.EndAt).WithMessage("A data de início deve ser menor que a data de término.");
+
+ RuleFor(x => x.Location)
+ .NotEmpty().When(x => !x.IsOnline)
+ .WithMessage("Local é obrigatório quando o evento não é online.");
+
+ RuleFor(x => x.StartAt)
+ .Must(date => DateTime.TryParse(date.ToString("yyyy/MM/dd HH:mm:ss"), out DateTime x))
+ .WithMessage("A data de início deve estar em formato ISO 8601 (YYYY-MM-DDTHH:mm:ssZ).");
+
+ RuleFor(x => x.EndAt)
+ .Must(date => DateTime.TryParse(date.ToString("yyyy/MM/dd HH:mm:ss"), out DateTime x))
+ .WithMessage("A data fim deve estar em formato ISO 8601 (YYYY-MM-DDTHH:mm:ssZ).");
+ }
+ }
+}
diff --git a/Workshop10-API.csproj b/Workshop10-API.csproj
index eb31916..7ee0b1a 100644
--- a/Workshop10-API.csproj
+++ b/Workshop10-API.csproj
@@ -8,8 +8,15 @@
+
+
+
+
+
+
+
diff --git a/Workshop10-API.http b/Workshop10-API.http
index 173c808..44287f4 100644
--- a/Workshop10-API.http
+++ b/Workshop10-API.http
@@ -1,6 +1,6 @@
-@Workshop10_API_HostAddress = http://localhost:5298
+@Workshop10_API_HostAddress = http://localhost:5001
-GET {{Workshop10_API_HostAddress}}/weatherforecast/
+GET {{Workshop10_API_HostAddress}}/
Accept: application/json
###