Compare commits
No commits in common. "a9de9c3a4c2a5f1260afcc2050bd83dd9a629a4a" and "ce4e5848dbc7dd5624083c763f50944e5bbab674" have entirely different histories.
a9de9c3a4c
...
ce4e5848db
17
Dockerfile
17
Dockerfile
@ -1,17 +0,0 @@
|
|||||||
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,7 +1,5 @@
|
|||||||
# PTV Street Ressource Task
|
# 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).
|
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.
|
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.
|
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.
|
||||||
@ -70,19 +68,3 @@ The following functions are supported:
|
|||||||
"usePostGIS": bool // Optional, default is 'false'
|
"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.
|
|
||||||
|
|||||||
@ -1,59 +0,0 @@
|
|||||||
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
78
k8s.yaml
@ -1,78 +0,0 @@
|
|||||||
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
53
src/Migrations/20250403113830_Initial.Designer.cs
generated
@ -1,53 +0,0 @@
|
|||||||
// <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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
// <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,10 +9,6 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.3" />
|
<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="NetTopologySuite" Version="2.6.0" />
|
||||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
|
||||||
<PackageReference Include="npgsql.entityframeworkcore.postgresql.nettopologysuite" Version="9.0.4" />
|
<PackageReference Include="npgsql.entityframeworkcore.postgresql.nettopologysuite" Version="9.0.4" />
|
||||||
|
|||||||
@ -1,11 +1,8 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
// In testing, the framework replaces the DBContext anyways.
|
builder.Services.AddDbContext<StreetDbContext>(options => options.UseNpgsql("TBD", o => o.UseNetTopologySuite()));
|
||||||
// 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<StreetRepository>();
|
||||||
builder.Services.AddScoped<StreetService>();
|
builder.Services.AddScoped<StreetService>();
|
||||||
@ -14,10 +11,6 @@ builder.Services.AddControllers();
|
|||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
using var scope = app.Services.CreateScope();
|
|
||||||
var dbContext = scope.ServiceProvider.GetRequiredService<StreetDbContext>();
|
|
||||||
dbContext.Database.Migrate();
|
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user