Implement all car endpoints
This commit is contained in:
1
Create-MigrationScript.ps1
Normal file
1
Create-MigrationScript.ps1
Normal file
@@ -0,0 +1 @@
|
|||||||
|
dotnet ef migrations script --idempotent --project .\src\WebApi\WebApi.csproj -o migrations/migration.sql
|
||||||
48
migrations/migration.sql
Normal file
48
migrations/migration.sql
Normal file
@@ -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;
|
||||||
|
|
||||||
78
src/WebApi/Authentication/UserAccessor.cs
Normal file
78
src/WebApi/Authentication/UserAccessor.cs
Normal file
@@ -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> _jwtOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the username upon first retrieval
|
||||||
|
/// </summary>
|
||||||
|
private string? _cachedUsername;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores the id upon first retrieval
|
||||||
|
/// </summary>
|
||||||
|
private string? _cachedId;
|
||||||
|
|
||||||
|
public UserAccessor(IHttpContextAccessor httpContextAccessor, IOptions<JwtOptions> 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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
public class Car
|
||||||
{
|
{
|
||||||
@@ -6,5 +10,27 @@ public class Car
|
|||||||
|
|
||||||
public string Name { get; set; } = "";
|
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<Car>
|
||||||
|
{
|
||||||
|
public const int NameMaxLength = 50;
|
||||||
|
|
||||||
|
public void Configure(EntityTypeBuilder<Car> 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using FluentValidation.Results;
|
using FluentValidation.Results;
|
||||||
|
using Vegasco.WebApi.Authentication;
|
||||||
using Vegasco.WebApi.Common;
|
using Vegasco.WebApi.Common;
|
||||||
|
using Vegasco.WebApi.Persistence;
|
||||||
|
using Vegasco.WebApi.Users;
|
||||||
|
|
||||||
namespace Vegasco.WebApi.Cars;
|
namespace Vegasco.WebApi.Cars;
|
||||||
|
|
||||||
@@ -12,7 +15,7 @@ public static class CreateCar
|
|||||||
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
public static RouteHandlerBuilder MapEndpoint(IEndpointRouteBuilder builder)
|
||||||
{
|
{
|
||||||
return builder
|
return builder
|
||||||
.MapPost("cars", Handler)
|
.MapPost("cars", Endpoint)
|
||||||
.WithTags("Cars");
|
.WithTags("Cars");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,18 +24,46 @@ public static class CreateCar
|
|||||||
public Validator()
|
public Validator()
|
||||||
{
|
{
|
||||||
RuleFor(x => x.Name)
|
RuleFor(x => x.Name)
|
||||||
.NotEmpty();
|
.NotEmpty()
|
||||||
|
.MaximumLength(CarTableConfiguration.NameMaxLength);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<IResult> Handler(Request request, IEnumerable<IValidator<Request>> validators)
|
public static async Task<IResult> Endpoint(
|
||||||
|
Request request,
|
||||||
|
IEnumerable<IValidator<Request>> validators,
|
||||||
|
ApplicationDbContext dbContext,
|
||||||
|
UserAccessor userAccessor,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request);
|
List<ValidationResult> failedValidations = await validators.ValidateAllAsync(request, cancellationToken: cancellationToken);
|
||||||
if (failedValidations.Count > 0)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
31
src/WebApi/Cars/DeleteCar.cs
Normal file
31
src/WebApi/Cars/DeleteCar.cs
Normal file
@@ -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<IResult> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
31
src/WebApi/Cars/GetCar.cs
Normal file
31
src/WebApi/Cars/GetCar.cs
Normal file
@@ -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<IResult> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
src/WebApi/Cars/GetCars.cs
Normal file
27
src/WebApi/Cars/GetCars.cs
Normal file
@@ -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<IResult> Endpoint(
|
||||||
|
ApplicationDbContext dbContext,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var cars = await dbContext.Cars
|
||||||
|
.Select(x => new Response(x.Id, x.Name))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return TypedResults.Ok(cars);
|
||||||
|
}
|
||||||
|
}
|
||||||
58
src/WebApi/Cars/UpdateCar.cs
Normal file
58
src/WebApi/Cars/UpdateCar.cs
Normal file
@@ -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<Request>
|
||||||
|
{
|
||||||
|
public Validator()
|
||||||
|
{
|
||||||
|
RuleFor(x => x.Name)
|
||||||
|
.NotEmpty()
|
||||||
|
.MaximumLength(CarTableConfiguration.NameMaxLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task<IResult> Endpoint(
|
||||||
|
Guid id,
|
||||||
|
Request request,
|
||||||
|
IEnumerable<IValidator<Request>> validators,
|
||||||
|
ApplicationDbContext dbContext,
|
||||||
|
UserAccessor userAccessor,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
List<ValidationResult> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
using Asp.Versioning;
|
using Asp.Versioning;
|
||||||
using FluentValidation;
|
using FluentValidation;
|
||||||
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
using Microsoft.AspNetCore.Authentication.JwtBearer;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using OpenTelemetry.Trace;
|
using OpenTelemetry.Trace;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Vegasco.WebApi.Authentication;
|
using Vegasco.WebApi.Authentication;
|
||||||
using Vegasco.WebApi.Endpoints;
|
using Vegasco.WebApi.Endpoints;
|
||||||
using Vegasco.WebApi.Endpoints.OpenApi;
|
using Vegasco.WebApi.Endpoints.OpenApi;
|
||||||
|
using Vegasco.WebApi.Persistence;
|
||||||
|
|
||||||
namespace Vegasco.WebApi.Common;
|
namespace Vegasco.WebApi.Common;
|
||||||
|
|
||||||
@@ -16,14 +18,15 @@ public static class DependencyInjectionExtensions
|
|||||||
/// Adds all the WebApi related services to the Dependency Injection container.
|
/// Adds all the WebApi related services to the Dependency Injection container.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="services"></param>
|
/// <param name="services"></param>
|
||||||
public static void AddWebApiServices(this IServiceCollection services)
|
public static void AddWebApiServices(this IServiceCollection services, IConfiguration configuration)
|
||||||
{
|
{
|
||||||
services
|
services
|
||||||
.AddMiscellaneousServices()
|
.AddMiscellaneousServices()
|
||||||
.AddOpenApi()
|
.AddOpenApi()
|
||||||
.AddApiVersioning()
|
.AddApiVersioning()
|
||||||
.AddOtel()
|
.AddOtel()
|
||||||
.AddAuthenticationAndAuthorization();
|
.AddAuthenticationAndAuthorization()
|
||||||
|
.AddDbContext(configuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IServiceCollection AddMiscellaneousServices(this IServiceCollection services)
|
private static IServiceCollection AddMiscellaneousServices(this IServiceCollection services)
|
||||||
@@ -38,6 +41,8 @@ public static class DependencyInjectionExtensions
|
|||||||
services.AddHealthChecks();
|
services.AddHealthChecks();
|
||||||
services.AddEndpointsFromAssemblyContaining<IWebApiMarker>();
|
services.AddEndpointsFromAssemblyContaining<IWebApiMarker>();
|
||||||
|
|
||||||
|
services.AddHttpContextAccessor();
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +51,27 @@ public static class DependencyInjectionExtensions
|
|||||||
services.ConfigureOptions<ConfigureSwaggerGenOptions>();
|
services.ConfigureOptions<ConfigureSwaggerGenOptions>();
|
||||||
|
|
||||||
services.AddEndpointsApiExplorer();
|
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;
|
return services;
|
||||||
}
|
}
|
||||||
@@ -119,6 +144,21 @@ public static class DependencyInjectionExtensions
|
|||||||
.RequireAuthenticatedUser()
|
.RequireAuthenticatedUser()
|
||||||
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme));
|
.AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme));
|
||||||
|
|
||||||
|
services.AddScoped<UserAccessor>();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IServiceCollection AddDbContext(this IServiceCollection services, IConfiguration configuration)
|
||||||
|
{
|
||||||
|
services.AddDbContext<ApplicationDbContext>(o =>
|
||||||
|
{
|
||||||
|
o.UseNpgsql(configuration.GetConnectionString("Database"), c =>
|
||||||
|
{
|
||||||
|
c.EnableRetryOnFailure();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
src/WebApi/Common/FluentValidationOptions.cs
Normal file
36
src/WebApi/Common/FluentValidationOptions.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using FluentValidation;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Common;
|
||||||
|
|
||||||
|
public class FluentValidationOptions<TOptions> : IValidateOptions<TOptions>
|
||||||
|
where TOptions : class
|
||||||
|
{
|
||||||
|
private readonly IEnumerable<IValidator<TOptions>> _validators;
|
||||||
|
|
||||||
|
public string? Name { get; set; }
|
||||||
|
|
||||||
|
public FluentValidationOptions(string? name, IEnumerable<IValidator<TOptions>> 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ internal static class StartupExtensions
|
|||||||
{
|
{
|
||||||
builder.Configuration.AddEnvironmentVariables("Vegasco_");
|
builder.Configuration.AddEnvironmentVariables("Vegasco_");
|
||||||
|
|
||||||
builder.Services.AddWebApiServices();
|
builder.Services.AddWebApiServices(builder.Configuration);
|
||||||
|
|
||||||
WebApplication app = builder.Build();
|
WebApplication app = builder.Build();
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ public static class ValidatorExtensions
|
|||||||
/// <param name="validators"></param>
|
/// <param name="validators"></param>
|
||||||
/// <param name="instance"></param>
|
/// <param name="instance"></param>
|
||||||
/// <returns>The failed validation results.</returns>
|
/// <returns>The failed validation results.</returns>
|
||||||
public static async Task<List<ValidationResult>> ValidateAllAsync<T>(this IEnumerable<IValidator<T>> validators, T instance)
|
public static async Task<List<ValidationResult>> ValidateAllAsync<T>(this IEnumerable<IValidator<T>> validators, T instance, CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
var validationTasks = validators
|
var validationTasks = validators
|
||||||
.Select(validator => validator.ValidateAsync(instance))
|
.Select(validator => validator.ValidateAsync(instance, cancellationToken))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
await Task.WhenAll(validationTasks);
|
await Task.WhenAll(validationTasks);
|
||||||
@@ -60,35 +60,3 @@ public static class ValidatorExtensions
|
|||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class FluentValidationOptions<TOptions> : IValidateOptions<TOptions>
|
|
||||||
where TOptions : class
|
|
||||||
{
|
|
||||||
private readonly IEnumerable<IValidator<TOptions>> _validators;
|
|
||||||
|
|
||||||
public string? Name { get; set; }
|
|
||||||
|
|
||||||
public FluentValidationOptions(string? name, IEnumerable<IValidator<TOptions>> 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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,6 +34,10 @@ public static class EndpointExtensions
|
|||||||
.WithApiVersionSet(apiVersionSet)
|
.WithApiVersionSet(apiVersionSet)
|
||||||
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
.RequireAuthorization(Constants.Authorization.RequireAuthenticatedUserPolicy);
|
||||||
|
|
||||||
|
GetCar.MapEndpoint(versionedApis);
|
||||||
|
GetCars.MapEndpoint(versionedApis);
|
||||||
CreateCar.MapEndpoint(versionedApis);
|
CreateCar.MapEndpoint(versionedApis);
|
||||||
|
UpdateCar.MapEndpoint(versionedApis);
|
||||||
|
DeleteCar.MapEndpoint(versionedApis);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
src/WebApi/Persistence/ApplicationDbContext.cs
Normal file
19
src/WebApi/Persistence/ApplicationDbContext.cs
Normal file
@@ -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<ApplicationDbContext> options) : DbContext(options)
|
||||||
|
{
|
||||||
|
public DbSet<Car> Cars { get; set; }
|
||||||
|
|
||||||
|
public DbSet<User> Users { get; set; }
|
||||||
|
|
||||||
|
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||||
|
{
|
||||||
|
base.OnModelCreating(modelBuilder);
|
||||||
|
modelBuilder.ApplyConfigurationsFromAssembly(typeof(IWebApiMarker).Assembly);
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/WebApi/Persistence/Migrations/20240803173803_AddCarsAndUsers.Designer.cs
generated
Normal file
78
src/WebApi/Persistence/Migrations/20240803173803_AddCarsAndUsers.Designer.cs
generated
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Cars");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Vegasco.WebApi.Users.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
using System;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Persistence.Migrations
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class AddCarsAndUsers : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Users",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<string>(type: "text", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_Users", x => x.Id);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "Cars",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Name = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
|
||||||
|
UserId = table.Column<string>(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");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Cars");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "Users");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// <auto-generated />
|
||||||
|
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<Guid>("Id")
|
||||||
|
.ValueGeneratedOnAdd()
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Name")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(50)
|
||||||
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
|
b.Property<string>("UserId")
|
||||||
|
.IsRequired()
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("UserId");
|
||||||
|
|
||||||
|
b.ToTable("Cars");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Vegasco.WebApi.Users.User", b =>
|
||||||
|
{
|
||||||
|
b.Property<string>("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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/WebApi/Users/User.cs
Normal file
10
src/WebApi/Users/User.cs
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
using Vegasco.WebApi.Cars;
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Users;
|
||||||
|
|
||||||
|
public class User
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = "";
|
||||||
|
|
||||||
|
public virtual IList<Car> Cars { get; set; } = [];
|
||||||
|
}
|
||||||
12
src/WebApi/Users/UserTableConfiguration.cs
Normal file
12
src/WebApi/Users/UserTableConfiguration.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace Vegasco.WebApi.Users;
|
||||||
|
|
||||||
|
public class UserTableConfiguration : IEntityTypeConfiguration<User>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<User> builder)
|
||||||
|
{
|
||||||
|
builder.HasKey(user => user.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,13 @@
|
|||||||
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
|
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.9.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.7" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7" />
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.7" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.7">
|
||||||
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
<PrivateAssets>all</PrivateAssets>
|
||||||
|
</PackageReference>
|
||||||
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.21.0" />
|
||||||
|
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||||
<PackageReference Include="OpenTelemetry" Version="1.9.0" />
|
<PackageReference Include="OpenTelemetry" Version="1.9.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
|
<PackageReference Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.9.0" />
|
||||||
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
|
<PackageReference Include="OpenTelemetry.Extensions.Hosting" Version="1.9.0" />
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
{
|
{
|
||||||
|
"ConnectionStrings": {
|
||||||
|
"Database": "Host=localhost;Port=5432;Database=postgres;Username=postgres;Password=postgres"
|
||||||
|
},
|
||||||
"Logging": {
|
"Logging": {
|
||||||
"LogLevel": {
|
"LogLevel": {
|
||||||
"Default": "Information",
|
"Default": "Information",
|
||||||
|
|||||||
Reference in New Issue
Block a user