Skip to content

Commit 35a3f55

Browse files
Initial commit
0 parents  commit 35a3f55

File tree

19 files changed

+697
-0
lines changed

19 files changed

+697
-0
lines changed

.github/workflows/dotnet.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: .NET 9 CI
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
paths-ignore:
7+
- '**/*.md'
8+
- 'docs/**'
9+
pull_request:
10+
branches: [ main ]
11+
paths-ignore:
12+
- '**/*.md'
13+
- 'docs/**'
14+
15+
jobs:
16+
build-and-test:
17+
runs-on: windows-latest
18+
19+
steps:
20+
- name: Checkout source
21+
uses: actions/checkout@v4
22+
23+
- name: Setup .NET 9 SDK
24+
uses: actions/setup-dotnet@v4
25+
with:
26+
dotnet-version: '9.0.x'
27+
28+
- name: Restore dependencies
29+
run: dotnet restore
30+
31+
- name: Build solution
32+
run: dotnet build --configuration Release --no-restore
33+
34+
- name: Run tests
35+
run: dotnet test --configuration Release --no-build --verbosity normal

.gitignore

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Build directories
2+
bin/
3+
obj/
4+
out/
5+
[Bb]uild/
6+
7+
# User-specific files
8+
*.user
9+
*.suo
10+
*.userosscache
11+
*.sln.docstates
12+
13+
# Visual Studio cache / generated files
14+
.vs/
15+
*.pidb
16+
*.log
17+
*.svclog
18+
*.scc
19+
20+
# Rider / JetBrains
21+
.idea/
22+
*.DotSettings.user
23+
24+
# Autogenerated files
25+
Generated_Code/
26+
27+
# Test results
28+
TestResult*/
29+
*.trx
30+
*.coverage
31+
*.coveragexml
32+
33+
# NuGet packages
34+
*.nupkg
35+
*.snupkg
36+
packages/
37+
.nuget/
38+
project.lock.json
39+
project.fragment.lock.json
40+
41+
# Backup & temporary files
42+
*.bak
43+
*.tmp
44+
*.temp
45+
~$*
46+
47+
# Publish outputs
48+
publish/
49+
*.Publish.xml
50+
*.pubxml
51+
*.pubxml.user
52+
53+
# Env files / secrets
54+
.env
55+
.env.*
56+
57+
# OS junk files
58+
Thumbs.db
59+
.DS_Store
60+
desktop.ini
61+
62+
# ReSharper
63+
_ReSharper*/
64+
*.[Rr]e[Ss]harper
65+
*.DotSettings
66+
67+
# Git merge / diff tools
68+
*.orig
69+
*.rej
70+
71+
# Tool-specific
72+
*.sqlite
73+
*.db
74+
*.mdf
75+
*.ldf
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace MinimalTodos.API.Domain
2+
{
3+
public record TodoCreateDto(string? Title);
4+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace MinimalTodos.API.Domain
4+
{
5+
public class TodoItem
6+
{
7+
[Required]
8+
public int Id { get; set; }
9+
10+
[Required]
11+
[StringLength(100, MinimumLength = 1)]
12+
public string? Title { get; set; }
13+
public bool? IsDone { get; set; }
14+
public TodoItem()
15+
{
16+
Id = 0;
17+
IsDone = false;
18+
}
19+
20+
public TodoItem(int id, string title, bool? isDone)
21+
{
22+
Id = id;
23+
Title = title;
24+
IsDone = isDone;
25+
}
26+
}
27+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
using MinimalTodos.API.Domain;
2+
using MinimalTodos.API.Repositories;
3+
using MinimalTodos.API.Validation;
4+
5+
namespace MinimalTodos.API.Endpoints
6+
{
7+
public static class TodoEndpoints
8+
{
9+
public static RouteGroupBuilder MapTodoEndpoints(this WebApplication app)
10+
{
11+
var todos = app.MapGroup("/todos");
12+
13+
todos.MapPost("/", CreateAsync).WithName("CreateTodo");
14+
todos.MapGet("/", FilterAsync).WithName("FilterTodo");
15+
todos.MapGet("/{id:int}", GetAsync).WithName("GetTodo");
16+
todos.MapPut("/{id:int}", UpdateAsync).WithName("UpdateTodo");
17+
todos.MapPatch("/{id:int}/toggle", ToggleAsync).WithName("ToggleTodo");
18+
todos.MapDelete("/{id:int}", DeleteAsync).WithName("DeleteTodo");
19+
20+
return todos;
21+
}
22+
23+
private static IResult CreateAsync(ITodoRepository repo, TodoCreateDto dto)
24+
{
25+
var errors = TodoValidator.Validate(dto);
26+
if (errors is not null)
27+
return Results.ValidationProblem(errors);
28+
29+
var item = new TodoItem(0, dto.Title!.Trim(), false);
30+
var created = repo.Add(item);
31+
32+
return Results.Created($"/todos/{created.Id}", created);
33+
}
34+
35+
private static IResult FilterAsync(ITodoRepository repo, string? search, int pageIndex = 0, int pageSize = 10)
36+
{
37+
const int MaxPageSize = 100;
38+
if (pageIndex < 0 || pageSize <= 0 || pageSize > MaxPageSize)
39+
return Results.BadRequest();
40+
41+
var items = repo.GetAll();
42+
43+
if (!string.IsNullOrWhiteSpace(search))
44+
{
45+
var txt = search.Trim().ToLowerInvariant();
46+
items = items.Where(x => x.Title!.ToLowerInvariant().Contains(txt));
47+
}
48+
49+
var page = items
50+
.Skip(pageIndex * pageSize)
51+
.Take(pageSize)
52+
.ToList();
53+
54+
return Results.Ok(page);
55+
}
56+
57+
private static IResult GetAsync(ITodoRepository repo, int id) =>
58+
repo.Get(id) is { } item
59+
? Results.Ok(item)
60+
: Results.NotFound();
61+
62+
private static IResult UpdateAsync(ITodoRepository repo, int id, TodoCreateDto dto)
63+
{
64+
var errors = TodoValidator.Validate(dto);
65+
if (errors is not null)
66+
return Results.ValidationProblem(errors);
67+
68+
if (repo.Get(id) is not { } existing)
69+
return Results.NotFound();
70+
71+
existing.Title = dto.Title!.Trim();
72+
repo.Update(existing);
73+
74+
return Results.Ok(existing);
75+
}
76+
77+
private static IResult ToggleAsync(ITodoRepository repo, int id)
78+
{
79+
if (repo.Get(id) is not { } item)
80+
return Results.NotFound();
81+
82+
repo.Toggle(id);
83+
84+
return Results.Ok(item);
85+
}
86+
87+
private static IResult DeleteAsync(ITodoRepository repo, int id) =>
88+
repo.Delete(id)
89+
? Results.NoContent()
90+
: Results.NotFound();
91+
}
92+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using MinimalTodos.API.Endpoints;
2+
3+
namespace MinimalTodos.API.Extensions
4+
{
5+
public static class EndpointsExtensions
6+
{
7+
public static void MapEndpoints(this WebApplication app)
8+
{
9+
// Registers all endpoint groups in the project
10+
app.MapTodoEndpoints();
11+
}
12+
}
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk.Web">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net9.0</TargetFramework>
5+
<Nullable>enable</Nullable>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
</PropertyGroup>
8+
9+
<ItemGroup>
10+
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.6" />
11+
</ItemGroup>
12+
13+
</Project>

MinimalTodos.API/Program.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using MinimalTodos.API.Extensions;
2+
using MinimalTodos.API.Repositories;
3+
4+
var builder = WebApplication.CreateBuilder(args);
5+
6+
// Register essential services
7+
builder.Services.AddEndpointsApiExplorer();
8+
builder.Services.AddSwaggerGen();
9+
10+
// Dependency injection: register in-memory repository
11+
builder.Services.AddSingleton<ITodoRepository, InMemoryTodoRepository>();
12+
13+
var app = builder.Build();
14+
15+
// Enable Swagger middleware
16+
app.UseSwagger();
17+
app.UseSwaggerUI();
18+
19+
// Register all endpoints (currently only /todos)
20+
app.MapEndpoints();
21+
22+
app.Run();
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"$schema": "https://json.schemastore.org/launchsettings.json",
3+
"profiles": {
4+
"MinimalTodos.API": {
5+
"commandName": "Project",
6+
"dotnetRunMessages": true,
7+
"launchBrowser": true,
8+
"applicationUrl": "https://localhost:7026;http://localhost:5000",
9+
"environmentVariables": {
10+
"ASPNETCORE_ENVIRONMENT": "Development"
11+
}
12+
},
13+
"https": {
14+
"commandName": "Project",
15+
"dotnetRunMessages": true,
16+
"launchBrowser": true,
17+
"applicationUrl": "https://localhost:7026;http://localhost:5272",
18+
"environmentVariables": {
19+
"ASPNETCORE_ENVIRONMENT": "Development"
20+
}
21+
}
22+
}
23+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using MinimalTodos.API.Domain;
2+
3+
namespace MinimalTodos.API.Repositories
4+
{
5+
public interface ITodoRepository
6+
{
7+
TodoItem Add(TodoItem item);
8+
bool Delete(int id);
9+
TodoItem? Get(int id);
10+
IEnumerable<TodoItem> GetAll();
11+
void Toggle(int id);
12+
void Update(TodoItem item);
13+
}
14+
}

0 commit comments

Comments
 (0)