Watch
1
0
Fork
You've already forked test-web
0

Initial commit
All checks were successful
Build / build (push) Successful in 52s

This commit is contained in:
Chris 2026-05-30 12:26:50 +01:00 • committed by Chris Dill
commit 0e79a8eb5e
43 changed files with 1282 additions and 0 deletions

51
.editorconfig Executable file
View file

@ -0,0 +1,51 @@
# EditorConfig is awesome: http://EditorConfig.org
root = true
[*]
indent_style = space
# General
[*.{cs,csx,vb,vbx}]
indent_size = 4
insert_final_newline = true
charset = utf-8
trim_trailing_whitespace = true
# C# styles
[*.cs]
# Indentation preferences
csharp_indent_block_contents = true
csharp_indent_braces = false
csharp_indent_case_contents = true
csharp_indent_case_contents_when_block = true
csharp_indent_switch_labels = true
csharp_indent_labels = flush_left
csharp_style_namespace_declarations = file_scoped:warning
# Newline settings
csharp_new_line_before_open_brace = all
csharp_new_line_before_else = true
csharp_new_line_before_catch = true
csharp_new_line_before_finally = true
csharp_new_line_before_members_in_object_initializers = true
csharp_new_line_before_members_in_anonymous_types = true
csharp_new_line_between_query_expression_clauses = true
# Spacing
csharp_space_after_cast = false
csharp_space_after_colon_in_inheritance_clause = true
csharp_space_after_keywords_in_control_flow_statements = true
csharp_space_around_binary_operators = before_and_after
csharp_space_before_colon_in_inheritance_clause = true
csharp_space_between_method_call_empty_parameter_list_parentheses = false
csharp_space_between_method_call_name_and_opening_parenthesis = false
csharp_space_between_method_call_parameter_list_parentheses = false
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false
csharp_space_between_method_declaration_parameter_list_parentheses = false
csharp_space_between_parentheses = false
# Blocks are allowed
csharp_prefer_braces = true:error
csharp_preserve_single_line_blocks = false
csharp_preserve_single_line_statements = false

34
.forgejo/workflows/build.yml Executable file
View file

@ -0,0 +1,34 @@
name: Build
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
env:
DOTNET_NOLOGO: true
DOTNET_CLI_TELEMETRY_OPTOUT: true
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up dotnet
uses: https://github.com/actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
- name: Build server
run: dotnet build -c Release
- name: Test server
run: dotnet test -c Release
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 10

2
.gitattributes vendored Executable file
View file

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

6
.gitignore vendored Executable file
View file

@ -0,0 +1,6 @@
.nuget/
bin/
obj/
app.db
app.db-shm
app.db-wal

7
README.md Executable file
View file

@ -0,0 +1,7 @@
# test-web
Test web dev.
```shell
dotnet run --project src
```

8
TestWeb.slnx Normal file
View file

@ -0,0 +1,8 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/TestWeb.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/TestWeb.Tests.csproj" />
</Folder>
</Solution>

View file

@ -0,0 +1,41 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Jobs;
public static class CreateJobEndpoint
{
public record Request(
string ClientName,
string Pickup,
string Dropoff
);
public record Response(int Id);
public class RequestValidator : AbstractValidator<Request>
{
public RequestValidator()
{
RuleFor(x => x.ClientName).NotEmpty().MaximumLength(250);
RuleFor(x => x.Pickup).NotEmpty().MaximumLength(120);
RuleFor(x => x.Dropoff).NotEmpty().MaximumLength(120);
}
}
public static async Task<Ok<Response>> Handle(Request request, AppDbContext db, CancellationToken ct)
{
var row = new Job
{
ClientName = request.ClientName,
Pickup = request.Pickup,
Dropoff = request.Dropoff,
};
await db.Jobs.AddAsync(row, ct);
await db.SaveChangesAsync(ct);
var response = new Response(row.Id);
return TypedResults.Ok(response);
}
}

View file

@ -0,0 +1,21 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Jobs;
public static class DeleteJobEndpoint
{
public static async Task<Results<Ok, NotFound>> Handle(
int id,
AppDbContext db,
CancellationToken ct)
{
var rowsDeleted = await db.Jobs
.Where(x => x.Id == id)
.ExecuteDeleteAsync(ct);
return rowsDeleted == 1
? TypedResults.Ok()
: TypedResults.NotFound();
}
}

View file

@ -0,0 +1,21 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Jobs;
public static class GetJobEndpoint
{
public static async Task<Results<Ok<Job>, NotFound>> Handle(
int id,
AppDbContext db,
CancellationToken ct)
{
var result = await db.Jobs
.Where(x => x.Id == id)
.SingleOrDefaultAsync(ct);
return result is null
? TypedResults.NotFound()
: TypedResults.Ok(result);
}
}

View file

@ -0,0 +1,13 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Jobs;
public static class GetJobsEndpoint
{
public static async Task<Ok<Job[]>> Handle(AppDbContext db, CancellationToken ct)
{
var results = await db.Jobs.ToArrayAsync(ct);
return TypedResults.Ok(results);
}
}

27
src/Core/Jobs/JobModule.cs Executable file
View file

@ -0,0 +1,27 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Jobs;
public static class JobModule
{
public const string Prefix = "/api/jobs";
public static void MapEndpoints(IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup(Prefix)
.WithTags("Jobs");
group.MapPost("/", CreateJobEndpoint.Handle)
.WithSummary("Create a job")
.WithRequestValidation<CreateJobEndpoint.Request>();
group.MapDelete("/{id:int}", DeleteJobEndpoint.Handle)
.WithSummary("Delete job by id");
group.MapGet("/{id:int}", GetJobEndpoint.Handle)
.WithSummary("Get job by id");
group.MapGet("/", GetJobsEndpoint.Handle)
.WithSummary("Get job list");
group.MapPut("/{id:int}", UpdateJobEndpoint.Handle)
.WithSummary("Update job by id")
.WithRequestValidation<UpdateJobEndpoint.Request>();
}
}

View file

@ -0,0 +1,48 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Jobs;
public static class UpdateJobEndpoint
{
public record Request(
string ClientName,
string Pickup,
string Dropoff
);
public class RequestValidator : AbstractValidator<Request>
{
public RequestValidator()
{
RuleFor(x => x.ClientName).NotEmpty().MaximumLength(250);
RuleFor(x => x.Pickup).NotEmpty().MaximumLength(120);
RuleFor(x => x.Dropoff).NotEmpty().MaximumLength(120);
}
}
public static async Task<Results<Ok, NotFound>> Handle(
int id,
Request request,
AppDbContext db,
CancellationToken ct)
{
var result = await db.Jobs
.Where(x => x.Id == id)
.SingleOrDefaultAsync(ct);
if (result == null)
{
return TypedResults.NotFound();
}
result.ClientName = request.ClientName;
result.Pickup = request.Pickup;
result.Dropoff = request.Dropoff;
result.LastUpdated = DateTime.UtcNow;
await db.SaveChangesAsync(ct);
return TypedResults.Ok();
}
}

View file

@ -0,0 +1,35 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Users;
public static class CreateUserEndpoint
{
public record Request(string UserName);
public class RequestValidator : AbstractValidator<Request>
{
public RequestValidator()
{
RuleFor(x => x.UserName).NotEmpty().MaximumLength(250);
}
}
public record Response(int Id);
public static async Task<Ok<Response>> Handle(Request request, AppDbContext db, CancellationToken ct)
{
var row = new User
{
Username = request.UserName,
DisplayName = request.UserName,
Password = string.Empty
};
await db.Users.AddAsync(row, ct);
await db.SaveChangesAsync(ct);
var response = new Response(row.Id);
return TypedResults.Ok(response);
}
}

View file

@ -0,0 +1,21 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Users;
public static class DeleteUserEndpoint
{
public static async Task<Results<Ok, NotFound>> Handle(
int id,
AppDbContext db,
CancellationToken ct)
{
var rowsDeleted = await db.Users
.Where(x => x.Id == id)
.ExecuteDeleteAsync(ct);
return rowsDeleted == 1
? TypedResults.Ok()
: TypedResults.NotFound();
}
}

View file

@ -0,0 +1,21 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Users;
public static class GetUserEndpoint
{
public static async Task<Results<Ok<User>, NotFound>> Handle(
int id,
AppDbContext db,
CancellationToken ct)
{
var result = await db.Users
.Where(x => x.Id == id)
.SingleOrDefaultAsync(ct);
return result is null
? TypedResults.NotFound()
: TypedResults.Ok(result);
}
}

View file

@ -0,0 +1,13 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Users;
public static class GetUsersEndpoint
{
public static async Task<Ok<User[]>> Handle(AppDbContext db, CancellationToken ct)
{
var results = await db.Users.ToArrayAsync(ct);
return TypedResults.Ok(results);
}
}

View file

@ -0,0 +1,40 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Users;
public static class UpdateUserEndpoint
{
public record Request(string DisplayName);
public class RequestValidator : AbstractValidator<Request>
{
public RequestValidator()
{
RuleFor(x => x.DisplayName).NotEmpty().MaximumLength(250);
}
}
public static async Task<Results<Ok, NotFound>> Handle(
int id,
Request request,
AppDbContext db,
CancellationToken ct)
{
var result = await db.Users
.Where(x => x.Id == id)
.SingleOrDefaultAsync(ct);
if (result == null)
{
return TypedResults.NotFound();
}
result.DisplayName = request.DisplayName;
result.LastUpdated = DateTime.UtcNow;
await db.SaveChangesAsync(ct);
return TypedResults.Ok();
}
}

28
src/Core/Users/UserModule.cs Executable file
View file

@ -0,0 +1,28 @@
using Microsoft.Extensions.Options;
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Users;
public static class UserModule
{
public const string Prefix = "/api/users";
public static void MapEndpoints(IEndpointRouteBuilder endpoints)
{
var group = endpoints.MapGroup(Prefix)
.WithTags("Users");
group.MapPost("/", CreateUserEndpoint.Handle)
.WithSummary("Create a user")
.WithRequestValidation<CreateUserEndpoint.Request>();
group.MapDelete("/{id}", DeleteUserEndpoint.Handle)
.WithSummary("Delete user by id");
group.MapGet("/{id}", GetUserEndpoint.Handle)
.WithSummary("Get user by id");
group.MapGet("/", GetUsersEndpoint.Handle)
.WithSummary("Get user list");
group.MapPut("/{id}", UpdateUserEndpoint.Handle)
.WithSummary("Update user by id")
.WithRequestValidation<UpdateUserEndpoint.Request>();
}
}

View file

@ -0,0 +1,36 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Vehicles;
public static class CreateVehicleEndpoint
{
public record Request(string Make, string Model);
public record Response(int Id);
public class RequestValidator : AbstractValidator<Request>
{
public RequestValidator()
{
RuleFor(x => x.Make).NotEmpty().MaximumLength(250);
RuleFor(x => x.Model).NotEmpty().MaximumLength(250);
}
}
public static async Task<Ok<Response>> Handle(Request request, AppDbContext db, CancellationToken ct)
{
var row = new Vehicle
{
Make = request.Make,
Model = request.Model,
Year = DateTime.Now.Year
};
await db.Vehicles.AddAsync(row, ct);
await db.SaveChangesAsync(ct);
var response = new Response(row.Id);
return TypedResults.Ok(response);
}
}

View file

@ -0,0 +1,21 @@
using TestWeb.Infra;
using TestWeb.Utils;
namespace TestWeb.Core.Vehicles;
public static class DeleteVehicleEndpoint
{
public static async Task<Results<Ok, NotFound>> Handle(
int id,
AppDbContext db,
CancellationToken ct)
{
var rowsDeleted = await db.Vehicles
.Where(x => x.Id == id)
.ExecuteDeleteAsync(ct);
return rowsDeleted == 1
? TypedResults.Ok()
: TypedResults.NotFound();
}
}

Some files were not shown because too many files have changed in this diff Show more