From b75b7512c2202c59cc6f9cd019b4d4b8e2c49185 Mon Sep 17 00:00:00 2001 From: ThompsonNye <88248872+ThompsonNye@users.noreply.github.com> Date: Sat, 3 Aug 2024 20:16:30 +0200 Subject: [PATCH] Implement all car endpoints --- Create-MigrationScript.ps1 | 1 + migrations/migration.sql | 48 ++++++++++++ src/WebApi/Authentication/UserAccessor.cs | 78 +++++++++++++++++++ src/WebApi/Cars/Car.cs | 30 ++++++- src/WebApi/Cars/CreateCar.cs | 43 ++++++++-- src/WebApi/Cars/DeleteCar.cs | 31 ++++++++ src/WebApi/Cars/GetCar.cs | 31 ++++++++ src/WebApi/Cars/GetCars.cs | 27 +++++++ src/WebApi/Cars/UpdateCar.cs | 58 ++++++++++++++ .../Common/DependencyInjectionExtensions.cs | 46 ++++++++++- src/WebApi/Common/FluentValidationOptions.cs | 36 +++++++++ src/WebApi/Common/StartupExtensions.cs | 2 +- src/WebApi/Common/ValidatorExtensions.cs | 36 +-------- src/WebApi/Endpoints/EndpointExtensions.cs | 4 + .../Persistence/ApplicationDbContext.cs | 19 +++++ ...20240803173803_AddCarsAndUsers.Designer.cs | 78 +++++++++++++++++++ .../20240803173803_AddCarsAndUsers.cs | 60 ++++++++++++++ .../ApplicationDbContextModelSnapshot.cs | 75 ++++++++++++++++++ src/WebApi/Users/User.cs | 10 +++ src/WebApi/Users/UserTableConfiguration.cs | 12 +++ src/WebApi/WebApi.csproj | 6 ++ src/WebApi/appsettings.Development.json | 3 + 22 files changed, 688 insertions(+), 46 deletions(-) create mode 100644 Create-MigrationScript.ps1 create mode 100644 migrations/migration.sql create mode 100644 src/WebApi/Authentication/UserAccessor.cs create mode 100644 src/WebApi/Cars/DeleteCar.cs create mode 100644 src/WebApi/Cars/GetCar.cs create mode 100644 src/WebApi/Cars/GetCars.cs create mode 100644 src/WebApi/Cars/UpdateCar.cs create mode 100644 src/WebApi/Common/FluentValidationOptions.cs create mode 100644 src/WebApi/Persistence/ApplicationDbContext.cs create mode 100644 src/WebApi/Persistence/Migrations/20240803173803_AddCarsAndUsers.Designer.cs create mode 100644 src/WebApi/Persistence/Migrations/20240803173803_AddCarsAndUsers.cs create mode 100644 src/WebApi/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs create mode 100644 src/WebApi/Users/User.cs create mode 100644 src/WebApi/Users/UserTableConfiguration.cs diff --git a/Create-MigrationScript.ps1 b/Create-MigrationScript.ps1 new file mode 100644 index 0000000..823b10e --- /dev/null +++ b/Create-MigrationScript.ps1 @@ -0,0 +1 @@ +dotnet ef migrations script --idempotent --project .\src\WebApi\WebApi.csproj -o migrations/migration.sql diff --git a/migrations/migration.sql b/migrations/migration.sql new file mode 100644 index 0000000..f4ef165 --- /dev/null +++ b/migrations/migration.sql @@ -0,0 +1,48 @@ +CREATE TABLE IF NOT EXISTS "__EFMigrationsHistory" ( + "MigrationId" character varying(150) NOT NULL, + "ProductVersion" character varying(32) NOT NULL, + CONSTRAINT "PK___EFMigrationsHistory" PRIMARY KEY ("MigrationId") +); + +START TRANSACTION; + + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20240803173803_AddCarsAndUsers') THEN + CREATE TABLE "Users" ( + "Id" text NOT NULL, + CONSTRAINT "PK_Users" PRIMARY KEY ("Id") + ); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20240803173803_AddCarsAndUsers') THEN + CREATE TABLE "Cars" ( + "Id" uuid NOT NULL, + "Name" character varying(50) NOT NULL, + "UserId" text NOT NULL, + CONSTRAINT "PK_Cars" PRIMARY KEY ("Id"), + CONSTRAINT "FK_Cars_Users_UserId" FOREIGN KEY ("UserId") REFERENCES "Users" ("Id") ON DELETE CASCADE + ); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20240803173803_AddCarsAndUsers') THEN + CREATE INDEX "IX_Cars_UserId" ON "Cars" ("UserId"); + END IF; +END $EF$; + +DO $EF$ +BEGIN + IF NOT EXISTS(SELECT 1 FROM "__EFMigrationsHistory" WHERE "MigrationId" = '20240803173803_AddCarsAndUsers') THEN + INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") + VALUES ('20240803173803_AddCarsAndUsers', '8.0.7'); + END IF; +END $EF$; +COMMIT; + diff --git a/src/WebApi/Authentication/UserAccessor.cs b/src/WebApi/Authentication/UserAccessor.cs new file mode 100644 index 0000000..6e69d1a --- /dev/null +++ b/src/WebApi/Authentication/UserAccessor.cs @@ -0,0 +1,78 @@ +using Microsoft.Extensions.Options; +using System.Diagnostics.CodeAnalysis; +using System.Security.Claims; + +namespace Vegasco.WebApi.Authentication; + +public sealed class UserAccessor +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly IOptions _jwtOptions; + + /// + /// Stores the username upon first retrieval + /// + private string? _cachedUsername; + + /// + /// Stores the id upon first retrieval + /// + private string? _cachedId; + + public UserAccessor(IHttpContextAccessor httpContextAccessor, IOptions jwtOptions) + { + _httpContextAccessor = httpContextAccessor; + _jwtOptions = jwtOptions; + } + + public string GetUsername() + { + if (string.IsNullOrEmpty(_cachedUsername)) + { + _cachedUsername = GetClaimValue(_jwtOptions.Value.NameClaimType ?? ClaimTypes.Name); + } + + return _cachedUsername; + } + + public string GetUserId() + { + if (string.IsNullOrEmpty(_cachedId)) + { + _cachedId = GetClaimValue(ClaimTypes.NameIdentifier); + } + + return _cachedId; + } + + private string GetClaimValue(string claimType) + { + var httpContext = _httpContextAccessor.HttpContext; + + if (httpContext is null) + { + ThrowForMissingHttpContext(); + } + + var claimValue = httpContext.User.FindFirstValue(claimType); + + if (string.IsNullOrWhiteSpace(claimValue)) + { + ThrowForMissingClaim(claimType); + } + + return claimValue; + } + + [DoesNotReturn] + private static void ThrowForMissingHttpContext() + { + throw new InvalidOperationException("No HttpContext available."); + } + + [DoesNotReturn] + private static void ThrowForMissingClaim(string claimType) + { + throw new InvalidOperationException($"No claim of type '{claimType}' found on the current user."); + } +} diff --git a/src/WebApi/Cars/Car.cs b/src/WebApi/Cars/Car.cs index da1e72b..da27e1d 100644 --- a/src/WebApi/Cars/Car.cs +++ b/src/WebApi/Cars/Car.cs @@ -1,4 +1,8 @@ -namespace Vegasco.WebApi.Cars; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Vegasco.WebApi.Users; + +namespace Vegasco.WebApi.Cars; public class Car { @@ -6,5 +10,27 @@ public class Car public string Name { get; set; } = ""; - public Guid UserId { get; set; } + public string UserId { get; set; } = ""; + + public virtual User User { get; set; } = null!; +} + +public class CarTableConfiguration : IEntityTypeConfiguration +{ + public const int NameMaxLength = 50; + + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(x => x.Id); + + builder.Property(x => x.Name) + .IsRequired() + .HasMaxLength(NameMaxLength); + + builder.Property(x => x.UserId) + .IsRequired(); + + builder.HasOne(x => x.User) + .WithMany(x => x.Cars); + } } diff --git a/src/WebApi/Cars/CreateCar.cs b/src/WebApi/Cars/CreateCar.cs index ed7630e..2bbbb74 100644 --- a/src/WebApi/Cars/CreateCar.cs +++ b/src/WebApi/Cars/CreateCar.cs @@ -1,6 +1,9 @@ using FluentValidation; using FluentValidation.Results; +using Vegasco.WebApi.Authentication; using Vegasco.WebApi.Common; +using Vegasco.WebApi.Persistence; +using Vegasco.WebApi.Users; namespace Vegasco.WebApi.Cars; @@ -12,7 +15,7 @@ public static class CreateCar public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder) { return builder - .MapPost("cars", Handler) + .MapPost("cars", Endpoint) .WithTags("Cars"); } @@ -21,18 +24,46 @@ public static class CreateCar public Validator() { RuleFor(x => x.Name) - .NotEmpty(); + .NotEmpty() + .MaximumLength(CarTableConfiguration.NameMaxLength); } } - public static async Task Handler(Request request, IEnumerable> validators) + public static async Task Endpoint( + Request request, + IEnumerable> validators, + ApplicationDbContext dbContext, + UserAccessor userAccessor, + CancellationToken cancellationToken) { - List failedValidations = await validators.ValidateAllAsync(request); + List failedValidations = await validators.ValidateAllAsync(request, cancellationToken: cancellationToken); if (failedValidations.Count > 0) { - return Results.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary())); + return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary())); } - return Results.Ok(); + var userId = userAccessor.GetUserId(); + + var user = await dbContext.Users.FindAsync([userId], cancellationToken: cancellationToken); + if (user is null) + { + user = new User + { + Id = userId + }; + await dbContext.Users.AddAsync(user, cancellationToken); + } + + Car car = new() + { + Name = request.Name, + UserId = userId + }; + + await dbContext.Cars.AddAsync(car, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); + + Response response = new(car.Id, car.Name); + return TypedResults.Created($"/v1/cars/{car.Id}", response); } } diff --git a/src/WebApi/Cars/DeleteCar.cs b/src/WebApi/Cars/DeleteCar.cs new file mode 100644 index 0000000..9febdd8 --- /dev/null +++ b/src/WebApi/Cars/DeleteCar.cs @@ -0,0 +1,31 @@ +using Vegasco.WebApi.Persistence; + +namespace Vegasco.WebApi.Cars; + +public static class DeleteCar +{ + public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + return builder + .MapDelete("cars/{id:guid}", Endpoint) + .WithTags("Cars"); + } + + public static async Task Endpoint( + Guid id, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var car = await dbContext.Cars.FindAsync([id], cancellationToken: cancellationToken); + + if (car is null) + { + return TypedResults.NotFound(); + } + + dbContext.Cars.Remove(car); + await dbContext.SaveChangesAsync(cancellationToken); + + return TypedResults.NoContent(); + } +} \ No newline at end of file diff --git a/src/WebApi/Cars/GetCar.cs b/src/WebApi/Cars/GetCar.cs new file mode 100644 index 0000000..ef954ef --- /dev/null +++ b/src/WebApi/Cars/GetCar.cs @@ -0,0 +1,31 @@ +using Vegasco.WebApi.Persistence; + +namespace Vegasco.WebApi.Cars; + +public static class GetCar +{ + public record Response(Guid Id, string Name); + + public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + return builder + .MapGet("cars/{id:guid}", Endpoint) + .WithTags("Cars"); + } + + public static async Task Endpoint( + Guid id, + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var car = await dbContext.Cars.FindAsync([id], cancellationToken: cancellationToken); + + if (car is null) + { + return TypedResults.NotFound(); + } + + var response = new Response(car.Id, car.Name); + return TypedResults.Ok(response); + } +} diff --git a/src/WebApi/Cars/GetCars.cs b/src/WebApi/Cars/GetCars.cs new file mode 100644 index 0000000..9a8ec8b --- /dev/null +++ b/src/WebApi/Cars/GetCars.cs @@ -0,0 +1,27 @@ +using Microsoft.EntityFrameworkCore; +using Vegasco.WebApi.Persistence; + +namespace Vegasco.WebApi.Cars; + +public static class GetCars +{ + public record Response(Guid Id, string Name); + + public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + return builder + .MapGet("cars", Endpoint) + .WithTags("Cars"); + } + + public static async Task Endpoint( + ApplicationDbContext dbContext, + CancellationToken cancellationToken) + { + var cars = await dbContext.Cars + .Select(x => new Response(x.Id, x.Name)) + .ToListAsync(cancellationToken); + + return TypedResults.Ok(cars); + } +} diff --git a/src/WebApi/Cars/UpdateCar.cs b/src/WebApi/Cars/UpdateCar.cs new file mode 100644 index 0000000..6d5ae6b --- /dev/null +++ b/src/WebApi/Cars/UpdateCar.cs @@ -0,0 +1,58 @@ +using FluentValidation; +using FluentValidation.Results; +using Vegasco.WebApi.Authentication; +using Vegasco.WebApi.Common; +using Vegasco.WebApi.Persistence; + +namespace Vegasco.WebApi.Cars; + +public static class UpdateCar +{ + public record Request(string Name); + public record Response(Guid Id, string Name); + + public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder) + { + return builder + .MapPut("cars/{id:guid}", Endpoint) + .WithTags("Cars"); + } + + public class Validator : AbstractValidator + { + public Validator() + { + RuleFor(x => x.Name) + .NotEmpty() + .MaximumLength(CarTableConfiguration.NameMaxLength); + } + } + + public static async Task Endpoint( + Guid id, + Request request, + IEnumerable> validators, + ApplicationDbContext dbContext, + UserAccessor userAccessor, + CancellationToken cancellationToken) + { + List failedValidations = await validators.ValidateAllAsync(request, cancellationToken); + if (failedValidations.Count > 0) + { + return TypedResults.BadRequest(new HttpValidationProblemDetails(failedValidations.ToCombinedDictionary())); + } + + var car = await dbContext.Cars.FindAsync([id], cancellationToken: cancellationToken); + + if (car is null) + { + return TypedResults.NotFound(); + } + + car.Name = request.Name; + await dbContext.SaveChangesAsync(cancellationToken); + + Response response = new(car.Id, car.Name); + return TypedResults.Ok(response); + } +} diff --git a/src/WebApi/Common/DependencyInjectionExtensions.cs b/src/WebApi/Common/DependencyInjectionExtensions.cs index 19de60f..bbcaeee 100644 --- a/src/WebApi/Common/DependencyInjectionExtensions.cs +++ b/src/WebApi/Common/DependencyInjectionExtensions.cs @@ -1,12 +1,14 @@ using Asp.Versioning; using FluentValidation; using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; using OpenTelemetry.Trace; using System.Diagnostics; using Vegasco.WebApi.Authentication; using Vegasco.WebApi.Endpoints; using Vegasco.WebApi.Endpoints.OpenApi; +using Vegasco.WebApi.Persistence; namespace Vegasco.WebApi.Common; @@ -16,14 +18,15 @@ public static class DependencyInjectionExtensions /// Adds all the WebApi related services to the Dependency Injection container. /// /// - public static void AddWebApiServices(this IServiceCollection services) + public static void AddWebApiServices(this IServiceCollection services, IConfiguration configuration) { services .AddMiscellaneousServices() .AddOpenApi() .AddApiVersioning() .AddOtel() - .AddAuthenticationAndAuthorization(); + .AddAuthenticationAndAuthorization() + .AddDbContext(configuration); } private static IServiceCollection AddMiscellaneousServices(this IServiceCollection services) @@ -38,6 +41,8 @@ public static class DependencyInjectionExtensions services.AddHealthChecks(); services.AddEndpointsFromAssemblyContaining(); + services.AddHttpContextAccessor(); + return services; } @@ -46,7 +51,27 @@ public static class DependencyInjectionExtensions services.ConfigureOptions(); services.AddEndpointsApiExplorer(); - services.AddSwaggerGen(); + services.AddSwaggerGen(o => + { + o.CustomSchemaIds(type => + { + if (string.IsNullOrEmpty(type.FullName)) + { + return type.Name; + } + + var fullClassName = type.FullName; + + if (!string.IsNullOrEmpty(type.Namespace)) + { + fullClassName = fullClassName + .Replace(type.Namespace, "") + .TrimStart('.'); + } + + return fullClassName; + }); + }); return services; } @@ -119,6 +144,21 @@ public static class DependencyInjectionExtensions .RequireAuthenticatedUser() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)); + services.AddScoped(); + + return services; + } + + private static IServiceCollection AddDbContext(this IServiceCollection services, IConfiguration configuration) + { + services.AddDbContext(o => + { + o.UseNpgsql(configuration.GetConnectionString("Database"), c => + { + c.EnableRetryOnFailure(); + }); + }); + return services; } } diff --git a/src/WebApi/Common/FluentValidationOptions.cs b/src/WebApi/Common/FluentValidationOptions.cs new file mode 100644 index 0000000..141e2e7 --- /dev/null +++ b/src/WebApi/Common/FluentValidationOptions.cs @@ -0,0 +1,36 @@ +using FluentValidation; +using Microsoft.Extensions.Options; + +namespace Vegasco.WebApi.Common; + +public class FluentValidationOptions : IValidateOptions + where TOptions : class +{ + private readonly IEnumerable> _validators; + + public string? Name { get; set; } + + public FluentValidationOptions(string? name, IEnumerable> validators) + { + Name = name; + _validators = validators; + } + + public ValidateOptionsResult Validate(string? name, TOptions options) + { + if (name is not null && name != Name) + { + return ValidateOptionsResult.Skip; + } + + ArgumentNullException.ThrowIfNull(options); + + var failedValidations = _validators.ValidateAllAsync(options).Result; + if (failedValidations.Count == 0) + { + return ValidateOptionsResult.Success; + } + + return ValidateOptionsResult.Fail(failedValidations.SelectMany(x => x.Errors.Select(x => x.ErrorMessage))); + } +} \ No newline at end of file diff --git a/src/WebApi/Common/StartupExtensions.cs b/src/WebApi/Common/StartupExtensions.cs index 2e1b332..4b6b07d 100644 --- a/src/WebApi/Common/StartupExtensions.cs +++ b/src/WebApi/Common/StartupExtensions.cs @@ -11,7 +11,7 @@ internal static class StartupExtensions { builder.Configuration.AddEnvironmentVariables("Vegasco_"); - builder.Services.AddWebApiServices(); + builder.Services.AddWebApiServices(builder.Configuration); WebApplication app = builder.Build(); return app; diff --git a/src/WebApi/Common/ValidatorExtensions.cs b/src/WebApi/Common/ValidatorExtensions.cs index 2ee107f..64e04f5 100644 --- a/src/WebApi/Common/ValidatorExtensions.cs +++ b/src/WebApi/Common/ValidatorExtensions.cs @@ -13,10 +13,10 @@ public static class ValidatorExtensions /// /// /// The failed validation results. - public static async Task> ValidateAllAsync(this IEnumerable> validators, T instance) + public static async Task> ValidateAllAsync(this IEnumerable> validators, T instance, CancellationToken cancellationToken = default) { var validationTasks = validators - .Select(validator => validator.ValidateAsync(instance)) + .Select(validator => validator.ValidateAsync(instance, cancellationToken)) .ToList(); await Task.WhenAll(validationTasks); @@ -60,35 +60,3 @@ public static class ValidatorExtensions return builder; } } - -internal class FluentValidationOptions : IValidateOptions - where TOptions : class -{ - private readonly IEnumerable> _validators; - - public string? Name { get; set; } - - public FluentValidationOptions(string? name, IEnumerable> validators) - { - Name = name; - _validators = validators; - } - - public ValidateOptionsResult Validate(string? name, TOptions options) - { - if (name is not null && name != Name) - { - return ValidateOptionsResult.Skip; - } - - ArgumentNullException.ThrowIfNull(options); - - var failedValidations = _validators.ValidateAllAsync(options).Result; - if (failedValidations.Count == 0) - { - return ValidateOptionsResult.Success; - } - - return ValidateOptionsResult.Fail(failedValidations.SelectMany(x => x.Errors.Select(x => x.ErrorMessage))); - } -} \ No newline at end of file diff --git a/src/WebApi/Endpoints/EndpointExtensions.cs b/src/WebApi/Endpoints/EndpointExtensions.cs index 17cc430..28f7df1 100644 --- a/src/WebApi/Endpoints/EndpointExtensions.cs +++ b/src/WebApi/Endpoints/EndpointExtensions.cs @@ -34,6 +34,10 @@ public static class EndpointExtensions .WithApiVersionSet(apiVersionSet) .RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy); + GetCar.MapEndpoint(versionedApis); + GetCars.MapEndpoint(versionedApis); CreateCar.MapEndpoint(versionedApis); + UpdateCar.MapEndpoint(versionedApis); + DeleteCar.MapEndpoint(versionedApis); } } diff --git a/src/WebApi/Persistence/ApplicationDbContext.cs b/src/WebApi/Persistence/ApplicationDbContext.cs new file mode 100644 index 0000000..e02de1d --- /dev/null +++ b/src/WebApi/Persistence/ApplicationDbContext.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using Vegasco.WebApi.Cars; +using Vegasco.WebApi.Common; +using Vegasco.WebApi.Users; + +namespace Vegasco.WebApi.Persistence; + +public class ApplicationDbContext(DbContextOptions options) : DbContext(options) +{ + public DbSet Cars { get; set; } + + public DbSet Users { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + modelBuilder.ApplyConfigurationsFromAssembly(typeof(IWebApiMarker).Assembly); + } +} diff --git a/src/WebApi/Persistence/Migrations/20240803173803_AddCarsAndUsers.Designer.cs b/src/WebApi/Persistence/Migrations/20240803173803_AddCarsAndUsers.Designer.cs new file mode 100644 index 0000000..e4149a5 --- /dev/null +++ b/src/WebApi/Persistence/Migrations/20240803173803_AddCarsAndUsers.Designer.cs @@ -0,0 +1,78 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Vegasco.WebApi.Persistence; + +#nullable disable + +namespace Vegasco.WebApi.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20240803173803_AddCarsAndUsers")] + partial class AddCarsAndUsers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Vegasco.WebApi.Cars.Car", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Cars"); + }); + + modelBuilder.Entity("Vegasco.WebApi.Users.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Vegasco.WebApi.Cars.Car", b => + { + b.HasOne("Vegasco.WebApi.Users.User", "User") + .WithMany("Cars") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Vegasco.WebApi.Users.User", b => + { + b.Navigation("Cars"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/WebApi/Persistence/Migrations/20240803173803_AddCarsAndUsers.cs b/src/WebApi/Persistence/Migrations/20240803173803_AddCarsAndUsers.cs new file mode 100644 index 0000000..a3cceaf --- /dev/null +++ b/src/WebApi/Persistence/Migrations/20240803173803_AddCarsAndUsers.cs @@ -0,0 +1,60 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Vegasco.WebApi.Persistence.Migrations +{ + /// + public partial class AddCarsAndUsers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Cars", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Name = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + UserId = table.Column(type: "text", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Cars", x => x.Id); + table.ForeignKey( + name: "FK_Cars_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Cars_UserId", + table: "Cars", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Cars"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/src/WebApi/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs b/src/WebApi/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs new file mode 100644 index 0000000..c29b621 --- /dev/null +++ b/src/WebApi/Persistence/Migrations/ApplicationDbContextModelSnapshot.cs @@ -0,0 +1,75 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using Vegasco.WebApi.Persistence; + +#nullable disable + +namespace Vegasco.WebApi.Persistence.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + partial class ApplicationDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Vegasco.WebApi.Cars.Car", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UserId") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Cars"); + }); + + modelBuilder.Entity("Vegasco.WebApi.Users.User", b => + { + b.Property("Id") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Vegasco.WebApi.Cars.Car", b => + { + b.HasOne("Vegasco.WebApi.Users.User", "User") + .WithMany("Cars") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Vegasco.WebApi.Users.User", b => + { + b.Navigation("Cars"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/WebApi/Users/User.cs b/src/WebApi/Users/User.cs new file mode 100644 index 0000000..cc5a46d --- /dev/null +++ b/src/WebApi/Users/User.cs @@ -0,0 +1,10 @@ +using Vegasco.WebApi.Cars; + +namespace Vegasco.WebApi.Users; + +public class User +{ + public string Id { get; set; } = ""; + + public virtual IList Cars { get; set; } = []; +} diff --git a/src/WebApi/Users/UserTableConfiguration.cs b/src/WebApi/Users/UserTableConfiguration.cs new file mode 100644 index 0000000..cf5a29a --- /dev/null +++ b/src/WebApi/Users/UserTableConfiguration.cs @@ -0,0 +1,12 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Vegasco.WebApi.Users; + +public class UserTableConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(user => user.Id); + } +} diff --git a/src/WebApi/WebApi.csproj b/src/WebApi/WebApi.csproj index aaa80ac..9790ef8 100644 --- a/src/WebApi/WebApi.csproj +++ b/src/WebApi/WebApi.csproj @@ -16,7 +16,13 @@ + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + diff --git a/src/WebApi/appsettings.Development.json b/src/WebApi/appsettings.Development.json index 0c208ae..e66cc17 100644 --- a/src/WebApi/appsettings.Development.json +++ b/src/WebApi/appsettings.Development.json @@ -1,4 +1,7 @@ { + "ConnectionStrings": { + "Database": "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres" + }, "Logging": { "LogLevel": { "Default": "Information",