Compare commits

...

4 Commits

Author SHA1 Message Date
a9de9c3a4c update README
Some checks failed
.NET Tests / test (push) Failing after 1m23s
2025-04-03 14:02:14 +02:00
bdb52d9faf add deployment configurations 2025-04-03 14:02:09 +02:00
975dfb2086 finalize entry point to program 2025-04-03 14:01:54 +02:00
fe358e44a3 add Database migrations 2025-04-03 14:01:46 +02:00
9 changed files with 327 additions and 2 deletions

17
Dockerfile Normal file
View File

@ -0,0 +1,17 @@
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS base
WORKDIR /src
COPY ["PTV-Street.sln","./"]
COPY ["src/PTV-Street.csproj", "PTV-Street/"]
RUN dotnet restore "PTV-Street/PTV-Street.csproj"
COPY . .
WORKDIR "/src/"
RUN dotnet publish src -c Release -o /PTV-Street/publish
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS ports
WORKDIR /PTV-Street
EXPOSE 8080
FROM base AS final
WORKDIR /PTV-Street
COPY --from=base /PTV-Street/publish .
ENTRYPOINT ["dotnet", "PTV-Street.dll"]

View File

@ -1,5 +1,7 @@
# PTV Street Ressource Task
This task was given as a Take-Home Assignment. The task description is the following:
1. Create a service in dotnet. It has a REST API that serves a resource 'street'. A street can be created and deleted. It has a name, a geometry and a capacity (= how many vehicles can use it within a minute).
2. The street data is stored in a Postgres database, and we use EF core to save data there.
3. Implement an endpoint to add a given single point to the geometry of an existing street on either the beginning or the end, whatever fits better.
@ -68,3 +70,19 @@ The following functions are supported:
"usePostGIS": bool // Optional, default is 'false'
}
```
## Notes about the Design
The application is written in a clean architecture style, seperating between Infrastructure (PostGIS-Db in this case), Domain, Application and the API layer.
The task was tackled in a test driven development, enforcing the API-behaviour with tests in the `/tests` folder. These tests use a test-database coming from a PostGIS testcontainer with the `testcontainer` package.
You can run these tests yourself with `dotnet test`.
## Trying it out yourself
This repo contains a Dockerfile to build the service as a container yourself. You can then supply a connection string to a PostGIS database via the `CONNECTION_STRING` environment variable. The containers serve their API on the port `8080`, bind it however you see fit.
Additionally, a functional Docker Compose setup also exists that spins up three such containers together with a function database. These then serve requests on ports `5001`,`5002` and `5003`.
Additionally, feel free to spin these up as kubernetes services. You still need to upload these images to a Hub and fill in the `image`-field in the k8s manifest though.

59
docker-compose.yml Normal file
View File

@ -0,0 +1,59 @@
services:
postgis-db:
image: postgis/postgis:13-3.1
container_name: postgis-db
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
POSTGRES_DB: streets_db
networks:
- DB-Network
streets-1:
build:
context: .
dockerfile: Dockerfile
container_name: Streets-1
environment:
ASPNETCORE_ENVIRONMENT: Production
CONNECTION_STRING: "Host=postgis-db;Port=5432;Username=postgres;Password=password;Database=streets_db;"
ports:
- "5001:8080"
depends_on:
- postgis-db
networks:
- DB-Network
streets-2:
build:
context: .
dockerfile: Dockerfile
container_name: Streets-2
environment:
ASPNETCORE_ENVIRONMENT: Production
CONNECTION_STRING: "Host=postgis-db;Port=5432;Username=postgres;Password=password;Database=streets_db;"
ports:
- "5002:8080"
depends_on:
- postgis-db
networks:
- DB-Network
streets-3:
build:
context: .
dockerfile: Dockerfile
container_name: Streets-3
environment:
ASPNETCORE_ENVIRONMENT: Production
CONNECTION_STRING: "Host=postgis-db;Port=5432;Username=postgres;Password=password;Database=streets_db;"
ports:
- "5003:8080"
depends_on:
- postgis-db
networks:
- DB-Network
networks:
DB-Network:
driver: bridge

78
k8s.yaml Normal file
View File

@ -0,0 +1,78 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: streets
spec:
replicas: 3
selector:
matchLabels:
app: streets
template:
metadata:
labels:
app: streets
spec:
containers:
- name: streets
image: # This needs to be filled in
ports:
- containerPort: 80
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: CONNECTION_STRING
value: "Host=postgis-db-service;Port=5432;Username=postgres;Password=password;Database=streets_db;"
---
apiVersion: v1
kind: Service
metadata:
name: streets-1
spec:
selector:
app: streets
ports:
- protocol: TCP
port: 5001
targetPort: 8080
type: LoadBalancer
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgis-db
spec:
replicas: 1
selector:
matchLabels:
app: postgis-db
template:
metadata:
labels:
app: postgis-db
spec:
containers:
- name: postgis-db
image: postgis/postgis:13-3.1
env:
- name: POSTGRES_USER
value: "postgres"
- name: POSTGRES_PASSWORD
value: "password"
- name: POSTGRES_DB
value: "streets_db"
ports:
- containerPort: 5432
---
apiVersion: v1
kind: Service
metadata:
name: postgis-db-service
spec:
selector:
app: postgis-db
ports:
- protocol: TCP
port: 5432
targetPort: 5432

View File

@ -0,0 +1,53 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NetTopologySuite.Geometries;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace PTV_Street.Migrations
{
[DbContext(typeof(StreetDbContext))]
[Migration("20250403113830_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Street", b =>
{
b.Property<string>("Name")
.HasColumnType("text");
b.Property<int>("Capacity")
.HasColumnType("integer");
b.Property<LineString>("Geometry")
.IsRequired()
.HasColumnType("geometry(LineString,4326)");
b.Property<uint>("Version")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Name");
b.ToTable("Streets");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,39 @@
using Microsoft.EntityFrameworkCore.Migrations;
using NetTopologySuite.Geometries;
#nullable disable
namespace PTV_Street.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AlterDatabase()
.Annotation("Npgsql:PostgresExtension:postgis", ",,");
migrationBuilder.CreateTable(
name: "Streets",
columns: table => new
{
Name = table.Column<string>(type: "text", nullable: false),
Capacity = table.Column<int>(type: "integer", nullable: false),
Geometry = table.Column<LineString>(type: "geometry(LineString,4326)", nullable: false),
xmin = table.Column<uint>(type: "xid", rowVersion: true, nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Streets", x => x.Name);
});
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Streets");
}
}
}

View File

@ -0,0 +1,50 @@
// <auto-generated />
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using NetTopologySuite.Geometries;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace PTV_Street.Migrations
{
[DbContext(typeof(StreetDbContext))]
partial class StreetDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "9.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "postgis");
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("Street", b =>
{
b.Property<string>("Name")
.HasColumnType("text");
b.Property<int>("Capacity")
.HasColumnType("integer");
b.Property<LineString>("Geometry")
.IsRequired()
.HasColumnType("geometry(LineString,4326)");
b.Property<uint>("Version")
.IsConcurrencyToken()
.ValueGeneratedOnAddOrUpdate()
.HasColumnType("xid")
.HasColumnName("xmin");
b.HasKey("Name");
b.ToTable("Streets");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -9,6 +9,10 @@
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.3">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="NetTopologySuite" Version="2.6.0" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageReference Include="npgsql.entityframeworkcore.postgresql.nettopologysuite" Version="9.0.4" />

View File

@ -1,8 +1,11 @@
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<StreetDbContext>(options => options.UseNpgsql("TBD", o => o.UseNetTopologySuite()));
// In testing, the framework replaces the DBContext anyways.
// In Production, this is set in the compose or k8s config
var connectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING");
builder.Services.AddDbContext<StreetDbContext>(options => options.UseNpgsql(connectionString, o => o.UseNetTopologySuite()));
builder.Services.AddScoped<StreetRepository>();
builder.Services.AddScoped<StreetService>();
@ -11,6 +14,10 @@ builder.Services.AddControllers();
var app = builder.Build();
using var scope = app.Services.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<StreetDbContext>();
dbContext.Database.Migrate();
app.UseRouting();
app.MapControllers();