diff --git a/Controllers/WorkshopsController.cs b/Controllers/WorkshopsController.cs
new file mode 100644
index 0000000..aaee027
--- /dev/null
+++ b/Controllers/WorkshopsController.cs
@@ -0,0 +1,40 @@
+
+using CampusWorkshops.Api.Dtos;
+using CampusWorkshops.Api.Models;
+using CampusWorkshops.Api.Repositories;
+using Microsoft.AspNetCore.Mvc;
+
+namespace CampusWorkshops.Api.Controllers;
+
+[ApiController]
+[Route("api/[controller]")]
+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)
+ {
+ // TODO: implementar usando _repo.GetAllAsync e mapear para WorkshopResponse
+ return Task.FromResult(Ok(Array.Empty()));
+ }
+
+ /// Obtém um workshop por Id.
+ [HttpGet("{id:guid}")]
+ public Task GetById(Guid id, CancellationToken ct)
+ {
+ // TODO: implementar usando _repo.GetByIdAsync
+ return Task.FromResult(NotFound());
+ }
+
+ /// Cria um novo workshop.
+ [HttpPost]
+ public Task Create([FromBody] CreateWorkshopRequest body, CancellationToken ct)
+ {
+ // TODO: validar ModelState, regras de negócio e chamar _repo.AddAsync; retornar CreatedAtAction
+ return Task.FromResult(BadRequest());
+ }
+}
diff --git a/Dtos/CreateWorkshopRequest.cs b/Dtos/CreateWorkshopRequest.cs
new file mode 100644
index 0000000..7686335
--- /dev/null
+++ b/Dtos/CreateWorkshopRequest.cs
@@ -0,0 +1,13 @@
+
+namespace CampusWorkshops.Api.Dtos;
+
+// DTO para criação de Workshop.
+public record CreateWorkshopRequest(
+ string? Title,
+ string? Description,
+ DateTimeOffset StartAt,
+ DateTimeOffset EndAt,
+ string? Location,
+ int Capacity,
+ bool IsOnline
+);
diff --git a/Dtos/WorkshopResponse.cs b/Dtos/WorkshopResponse.cs
new file mode 100644
index 0000000..161317a
--- /dev/null
+++ b/Dtos/WorkshopResponse.cs
@@ -0,0 +1,14 @@
+
+namespace CampusWorkshops.Api.Dtos;
+
+// DTO de resposta para Workshop
+public record WorkshopResponse(
+ Guid Id,
+ string? Title,
+ string? Description,
+ DateTimeOffset StartAt,
+ DateTimeOffset EndAt,
+ string? Location,
+ int Capacity,
+ bool IsOnline
+);
diff --git a/Models/Workshop.cs b/Models/Workshop.cs
new file mode 100644
index 0000000..64a19d2
--- /dev/null
+++ b/Models/Workshop.cs
@@ -0,0 +1,29 @@
+
+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 3917ef1..29f9166 100644
--- a/Program.cs
+++ b/Program.cs
@@ -1,41 +1,56 @@
+// Note: repository implementation removed for workshop exercise (TODOs in project files)
+using Microsoft.AspNetCore.Diagnostics;
+using Microsoft.AspNetCore.Mvc;
+
var builder = WebApplication.CreateBuilder(args);
-// Add services to the container.
-// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
-builder.Services.AddOpenApi();
+// Add services
+builder.Services.AddControllers();
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen(o =>
+{
+ o.SwaggerDoc("v1", new Microsoft.OpenApi.Models.OpenApiInfo
+ {
+ Title = "CampusWorkshops API",
+ Version = "v1",
+ Description = "API para gestão de workshops do campus (MVP in-memory)."
+ });
+});
+
+// DI
var app = builder.Build();
-// Configure the HTTP request pipeline.
-if (app.Environment.IsDevelopment())
+// Exception handler that returns RFC7807 ProblemDetails for unhandled errors
+app.UseExceptionHandler(errApp =>
{
- app.MapOpenApi();
-}
+ errApp.Run(async context =>
+ {
+ var feature = context.Features.Get();
+ var ex = feature?.Error;
+
+ var pd = new ProblemDetails
+ {
+ Title = "An unexpected error occurred.",
+ Status = StatusCodes.Status500InternalServerError,
+ Detail = app.Environment.IsDevelopment() ? ex?.Message : null
+ };
+
+ context.Response.StatusCode = pd.Status.Value;
+ context.Response.ContentType = "application/problem+json";
+ await context.Response.WriteAsJsonAsync(pd);
+ });
+});
app.UseHttpsRedirection();
-var summaries = new[]
+app.UseSwagger();
+app.UseSwaggerUI(c =>
{
- "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
-};
+ c.SwaggerEndpoint("/swagger/v1/swagger.json", "CampusWorkshops API v1");
+ c.RoutePrefix = "swagger"; // serve at /swagger
+});
-app.MapGet("/weatherforecast", () =>
-{
- var forecast = Enumerable.Range(1, 5).Select(index =>
- new WeatherForecast
- (
- DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
- Random.Shared.Next(-20, 55),
- summaries[Random.Shared.Next(summaries.Length)]
- ))
- .ToArray();
- return forecast;
-})
-.WithName("GetWeatherForecast");
+app.MapControllers();
app.Run();
-
-record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
-{
- public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
-}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bb691bb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,20 @@
+# CampusWorkshops API — Starter for Workshop
+
+Pré-requisitos:
+
+* .NET 8 SDK instalado
+
+Comandos básicos:
+
+```bash
+dotnet restore
+dotnet run
+```
+
+Depois de rodar, abra: https://localhost:5001/swagger
+
+Objetivos do encontro 1
+
+* Entender o contrato REST (recursos, rotas, status codes).
+* Implementar/ajustar `GET /api/workshops`, `GET /api/workshops/{id}` e codar o `POST /api/workshops` (com validação e `201 Created + Location`).
+* Ver erros formatados como `application/problem+json`.
diff --git a/Repositories/IWorkshopRepository.cs b/Repositories/IWorkshopRepository.cs
new file mode 100644
index 0000000..ff8bfda
--- /dev/null
+++ b/Repositories/IWorkshopRepository.cs
@@ -0,0 +1,12 @@
+
+using CampusWorkshops.Api.Models;
+
+namespace CampusWorkshops.Api.Repositories;
+
+// Define repository contract; implementations should be provided by students during the workshop.
+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);
+}
diff --git a/Repositories/InMemoryWorkshopRepository.cs b/Repositories/InMemoryWorkshopRepository.cs
new file mode 100644
index 0000000..e887e44
--- /dev/null
+++ b/Repositories/InMemoryWorkshopRepository.cs
@@ -0,0 +1,31 @@
+
+using CampusWorkshops.Api.Models;
+
+namespace CampusWorkshops.Api.Repositories;
+
+// In-memory repository stub
+public class InMemoryWorkshopRepository : IWorkshopRepository
+{
+ public InMemoryWorkshopRepository()
+ {
+ // TODO: Adicionar workshops iniciais
+ }
+
+ public Task> GetAllAsync(DateTimeOffset? from, DateTimeOffset? to, string? q, CancellationToken ct)
+ {
+ // TODO: retornar uma lista filtrada e ordenada por StartAt
+ return Task.FromResult>(Array.Empty());
+ }
+
+ public Task GetByIdAsync(Guid id, CancellationToken ct)
+ {
+ // TODO: buscar por id
+ return Task.FromResult(null);
+ }
+
+ public Task AddAsync(Workshop workshop, CancellationToken ct)
+ {
+ // TODO: adicionar à lista em memória e retornar criado
+ return Task.FromResult(workshop);
+ }
+}
diff --git a/Workshop10-API.csproj b/Workshop10-API.csproj
index c2caf2d..eb31916 100644
--- a/Workshop10-API.csproj
+++ b/Workshop10-API.csproj
@@ -9,6 +9,7 @@
+