Compare commits
4 Commits
ce4e5848db
...
a9de9c3a4c
| Author | SHA1 | Date | |
|---|---|---|---|
| a9de9c3a4c | |||
| bdb52d9faf | |||
| 975dfb2086 | |||
| fe358e44a3 |
17
Dockerfile
Normal file
17
Dockerfile
Normal 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"]
|
||||
18
README.md
18
README.md
@ -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
59
docker-compose.yml
Normal 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
78
k8s.yaml
Normal 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
|
||||
53
src/Migrations/20250403113830_Initial.Designer.cs
generated
Normal file
53
src/Migrations/20250403113830_Initial.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Migrations/20250403113830_Initial.cs
Normal file
39
src/Migrations/20250403113830_Initial.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
50
src/Migrations/StreetDbContextModelSnapshot.cs
Normal file
50
src/Migrations/StreetDbContextModelSnapshot.cs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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" />
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user