From 039b1cb9e38acb2d9e37a9d57ba3373635c0cd50 Mon Sep 17 00:00:00 2001 From: undrcrxwn <69521267+undrcrxwn@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:41:55 +0300 Subject: [PATCH 1/5] chore: migrate to .NET 9 --- .github/workflows/openapi.yml | 4 +- .github/workflows/test.yml | 2 +- Dockerfile | 4 +- .../CrowdParlay.Users.Api.csproj | 38 +-- .../CrowdParlay.Users.Application.csproj | 20 +- .../CrowdParlay.Users.Domain.csproj | 4 +- ...ay.Users.Infrastructure.Persistence.csproj | 31 +-- .../Extensions/ConfigureServices.cs | 2 + .../OpenIddictDbContextModelSnapshot.cs | 237 +----------------- .../OpenIddictDbContextFactory.cs | 14 ++ .../CrowdParlay.Users.Infrastructure.csproj | 6 +- .../CrowdParlay.Users.IntegrationTests.csproj | 15 +- .../Tests/HealthChecksTests.cs | 2 +- .../Tests/TraceIdMiddlewareTests.cs | 6 +- .../Tests/UsersControllerTests.cs | 18 +- 15 files changed, 97 insertions(+), 306 deletions(-) create mode 100644 src/CrowdParlay.Users.Infrastructure.Persistence/OpenIddictDbContextFactory.cs diff --git a/.github/workflows/openapi.yml b/.github/workflows/openapi.yml index d702f63..25c9b49 100644 --- a/.github/workflows/openapi.yml +++ b/.github/workflows/openapi.yml @@ -19,7 +19,7 @@ jobs: - name: ⚙️ Setup .NET uses: actions/setup-dotnet@v1 with: - dotnet-version: '7.0.x' + dotnet-version: '9.0.x' - name: ⚒ Build solution run: dotnet build - name: ⚙️ Install Swashbuckle.AspNetCore.Cli @@ -27,7 +27,7 @@ jobs: dotnet tool install Swashbuckle.AspNetCore.Cli --global echo "$DOTNET_ROOT/tools/swagger" >> $GITHUB_PATH - name: 📝 Generate OpenAPI specification - run: swagger tofile --output openapi.yaml --yaml ./src/CrowdParlay.Users.Api/bin/Debug/net7.0/CrowdParlay.Users.Api.dll v1 + run: swagger tofile --output openapi.yaml --yaml ./src/CrowdParlay.Users.Api/bin/Debug/net9.0/CrowdParlay.Users.Api.dll v1 - name: 🔗 Upload OpenAPI specification as release asset uses: actions/upload-release-asset@v1 env: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 80a36b1..6700a40 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,7 @@ jobs: - name: ⚙️ Setup .NET uses: actions/setup-dotnet@v3 with: - dotnet-version: 7.0.x + dotnet-version: 9.0.x - name: 📥 Restore dependencies run: dotnet restore - name: ⚒ Build solution diff --git a/Dockerfile b/Dockerfile index b9eb825..bb7e41d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ -FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build WORKDIR /app COPY /src . RUN dotnet restore "CrowdParlay.Users.Api/CrowdParlay.Users.Api.csproj" RUN dotnet publish "CrowdParlay.Users.Api/CrowdParlay.Users.Api.csproj" -c Release -o out -FROM mcr.microsoft.com/dotnet/aspnet:7.0 +FROM mcr.microsoft.com/dotnet/aspnet:9.0 WORKDIR /app COPY --from=build /app/out . ENTRYPOINT ["dotnet", "CrowdParlay.Users.Api.dll"] diff --git a/src/CrowdParlay.Users.Api/CrowdParlay.Users.Api.csproj b/src/CrowdParlay.Users.Api/CrowdParlay.Users.Api.csproj index c32a5b9..dca42e8 100644 --- a/src/CrowdParlay.Users.Api/CrowdParlay.Users.Api.csproj +++ b/src/CrowdParlay.Users.Api/CrowdParlay.Users.Api.csproj @@ -1,7 +1,7 @@ - net7.0 + net9.0 enable enable Linux @@ -10,30 +10,30 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + - - + + - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - + + + + + + + diff --git a/src/CrowdParlay.Users.Application/CrowdParlay.Users.Application.csproj b/src/CrowdParlay.Users.Application/CrowdParlay.Users.Application.csproj index 27324d1..6751c52 100644 --- a/src/CrowdParlay.Users.Application/CrowdParlay.Users.Application.csproj +++ b/src/CrowdParlay.Users.Application/CrowdParlay.Users.Application.csproj @@ -1,27 +1,27 @@  - net7.0 + net9.0 enable enable - - - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - + + + + + diff --git a/src/CrowdParlay.Users.Domain/CrowdParlay.Users.Domain.csproj b/src/CrowdParlay.Users.Domain/CrowdParlay.Users.Domain.csproj index 01e0878..0c85eb1 100644 --- a/src/CrowdParlay.Users.Domain/CrowdParlay.Users.Domain.csproj +++ b/src/CrowdParlay.Users.Domain/CrowdParlay.Users.Domain.csproj @@ -1,13 +1,13 @@ - net7.0 + net9.0 enable enable - + \ No newline at end of file diff --git a/src/CrowdParlay.Users.Infrastructure.Persistence/CrowdParlay.Users.Infrastructure.Persistence.csproj b/src/CrowdParlay.Users.Infrastructure.Persistence/CrowdParlay.Users.Infrastructure.Persistence.csproj index d122d08..d6e311b 100644 --- a/src/CrowdParlay.Users.Infrastructure.Persistence/CrowdParlay.Users.Infrastructure.Persistence.csproj +++ b/src/CrowdParlay.Users.Infrastructure.Persistence/CrowdParlay.Users.Infrastructure.Persistence.csproj @@ -1,25 +1,28 @@ - net7.0 + net9.0 enable enable - - - - - - - - - - - - - + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/CrowdParlay.Users.Infrastructure.Persistence/Extensions/ConfigureServices.cs b/src/CrowdParlay.Users.Infrastructure.Persistence/Extensions/ConfigureServices.cs index cd715c3..b0835c0 100644 --- a/src/CrowdParlay.Users.Infrastructure.Persistence/Extensions/ConfigureServices.cs +++ b/src/CrowdParlay.Users.Infrastructure.Persistence/Extensions/ConfigureServices.cs @@ -6,6 +6,7 @@ using Dapper; using FluentMigrator.Runner; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -31,6 +32,7 @@ public static IServiceCollection ConfigurePersistenceServices(this IServiceColle .WithGlobalConnectionString(connectionString) .ScanIn(Assembly.GetExecutingAssembly()).For.All()) .AddDbContext(options => options + .ConfigureWarnings(warnings => warnings.Ignore(RelationalEventId.PendingModelChangesWarning)) .UseNpgsql(connectionString) .UseOpenIddict()) .AddSingleton(new SqlConnectionFactory(connectionString)) diff --git a/src/CrowdParlay.Users.Infrastructure.Persistence/Migrations/EfCore/OpenIddictDbContextModelSnapshot.cs b/src/CrowdParlay.Users.Infrastructure.Persistence/Migrations/EfCore/OpenIddictDbContextModelSnapshot.cs index 2eb2cac..33a7293 100644 --- a/src/CrowdParlay.Users.Infrastructure.Persistence/Migrations/EfCore/OpenIddictDbContextModelSnapshot.cs +++ b/src/CrowdParlay.Users.Infrastructure.Persistence/Migrations/EfCore/OpenIddictDbContextModelSnapshot.cs @@ -1,5 +1,4 @@ // -using System; using CrowdParlay.Users.Infrastructure.Persistence; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; @@ -17,244 +16,10 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "7.0.12") + .HasAnnotation("ProductVersion", "9.0.6") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("text"); - - b.Property("ClientId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ClientSecret") - .HasColumnType("text"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ConsentType") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("DisplayName") - .HasColumnType("text"); - - b.Property("DisplayNames") - .HasColumnType("text"); - - b.Property("Permissions") - .HasColumnType("text"); - - b.Property("PostLogoutRedirectUris") - .HasColumnType("text"); - - b.Property("Properties") - .HasColumnType("text"); - - b.Property("RedirectUris") - .HasColumnType("text"); - - b.Property("Requirements") - .HasColumnType("text"); - - b.Property("Type") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("ClientId") - .IsUnique(); - - b.ToTable("OpenIddictApplications", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("text"); - - b.Property("ApplicationId") - .HasColumnType("text"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("CreationDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Properties") - .HasColumnType("text"); - - b.Property("Scopes") - .HasColumnType("text"); - - b.Property("Status") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Subject") - .HasMaxLength(400) - .HasColumnType("character varying(400)"); - - b.Property("Type") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("ApplicationId", "Status", "Subject", "Type"); - - b.ToTable("OpenIddictAuthorizations", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreScope", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("text"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Description") - .HasColumnType("text"); - - b.Property("Descriptions") - .HasColumnType("text"); - - b.Property("DisplayName") - .HasColumnType("text"); - - b.Property("DisplayNames") - .HasColumnType("text"); - - b.Property("Name") - .HasMaxLength(200) - .HasColumnType("character varying(200)"); - - b.Property("Properties") - .HasColumnType("text"); - - b.Property("Resources") - .HasColumnType("text"); - - b.HasKey("Id"); - - b.HasIndex("Name") - .IsUnique(); - - b.ToTable("OpenIddictScopes", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("text"); - - b.Property("ApplicationId") - .HasColumnType("text"); - - b.Property("AuthorizationId") - .HasColumnType("text"); - - b.Property("ConcurrencyToken") - .IsConcurrencyToken() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("CreationDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ExpirationDate") - .HasColumnType("timestamp with time zone"); - - b.Property("Payload") - .HasColumnType("text"); - - b.Property("Properties") - .HasColumnType("text"); - - b.Property("RedemptionDate") - .HasColumnType("timestamp with time zone"); - - b.Property("ReferenceId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Status") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("Subject") - .HasMaxLength(400) - .HasColumnType("character varying(400)"); - - b.Property("Type") - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.HasKey("Id"); - - b.HasIndex("AuthorizationId"); - - b.HasIndex("ReferenceId") - .IsUnique(); - - b.HasIndex("ApplicationId", "Status", "Subject", "Type"); - - b.ToTable("OpenIddictTokens", (string)null); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => - { - b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") - .WithMany("Authorizations") - .HasForeignKey("ApplicationId"); - - b.Navigation("Application"); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreToken", b => - { - b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", "Application") - .WithMany("Tokens") - .HasForeignKey("ApplicationId"); - - b.HasOne("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", "Authorization") - .WithMany("Tokens") - .HasForeignKey("AuthorizationId"); - - b.Navigation("Application"); - - b.Navigation("Authorization"); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreApplication", b => - { - b.Navigation("Authorizations"); - - b.Navigation("Tokens"); - }); - - modelBuilder.Entity("OpenIddict.EntityFrameworkCore.Models.OpenIddictEntityFrameworkCoreAuthorization", b => - { - b.Navigation("Tokens"); - }); #pragma warning restore 612, 618 } } diff --git a/src/CrowdParlay.Users.Infrastructure.Persistence/OpenIddictDbContextFactory.cs b/src/CrowdParlay.Users.Infrastructure.Persistence/OpenIddictDbContextFactory.cs new file mode 100644 index 0000000..aabc023 --- /dev/null +++ b/src/CrowdParlay.Users.Infrastructure.Persistence/OpenIddictDbContextFactory.cs @@ -0,0 +1,14 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; + +namespace CrowdParlay.Users.Infrastructure.Persistence; + +public class OpenIddictDbContextFactory : IDesignTimeDbContextFactory +{ + public OpenIddictDbContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder() + .UseNpgsql("Host=localhost;Database=dummy;Username=dummy;Password=dummy"); + return new OpenIddictDbContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/src/CrowdParlay.Users.Infrastructure/CrowdParlay.Users.Infrastructure.csproj b/src/CrowdParlay.Users.Infrastructure/CrowdParlay.Users.Infrastructure.csproj index 0498f6d..945f733 100644 --- a/src/CrowdParlay.Users.Infrastructure/CrowdParlay.Users.Infrastructure.csproj +++ b/src/CrowdParlay.Users.Infrastructure/CrowdParlay.Users.Infrastructure.csproj @@ -1,13 +1,15 @@ - net7.0 + net9.0 enable enable - + + + diff --git a/tests/CrowdParlay.Users.IntegrationTests/CrowdParlay.Users.IntegrationTests.csproj b/tests/CrowdParlay.Users.IntegrationTests/CrowdParlay.Users.IntegrationTests.csproj index aad776b..f7828f7 100644 --- a/tests/CrowdParlay.Users.IntegrationTests/CrowdParlay.Users.IntegrationTests.csproj +++ b/tests/CrowdParlay.Users.IntegrationTests/CrowdParlay.Users.IntegrationTests.csproj @@ -1,7 +1,7 @@ - net7.0 + net9.0 enable enable @@ -15,12 +15,13 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/CrowdParlay.Users.IntegrationTests/Tests/HealthChecksTests.cs b/tests/CrowdParlay.Users.IntegrationTests/Tests/HealthChecksTests.cs index d669484..ffea82e 100644 --- a/tests/CrowdParlay.Users.IntegrationTests/Tests/HealthChecksTests.cs +++ b/tests/CrowdParlay.Users.IntegrationTests/Tests/HealthChecksTests.cs @@ -14,7 +14,7 @@ public class HealthChecksTests : IAssemblyFixture public async Task GetHealth_ReturnsHealthy() { var response = await _client.GetAsync("/healthz"); - response.Should().HaveStatusCode(HttpStatusCode.OK); + response.StatusCode.Should().Be(HttpStatusCode.OK); var content = await response.Content.ReadAsStringAsync(); content.Should().BeEquivalentTo("healthy"); diff --git a/tests/CrowdParlay.Users.IntegrationTests/Tests/TraceIdMiddlewareTests.cs b/tests/CrowdParlay.Users.IntegrationTests/Tests/TraceIdMiddlewareTests.cs index 215e5e6..7e30cde 100644 --- a/tests/CrowdParlay.Users.IntegrationTests/Tests/TraceIdMiddlewareTests.cs +++ b/tests/CrowdParlay.Users.IntegrationTests/Tests/TraceIdMiddlewareTests.cs @@ -1,3 +1,5 @@ +using System.Net; +using System.Net.Http.Json; using CrowdParlay.Users.Application.Features.Users.Commands; using CrowdParlay.Users.IntegrationTests.Fixtures; using FluentAssertions; @@ -26,8 +28,8 @@ public async Task RegisterUserOnFailure_ReturnsTraceId() var command = new Register.Command(string.Empty, string.Empty, string.Empty,string.Empty, null); var response = await _client.PostAsJsonAsync("/api/v1/users/register", command); + response.StatusCode.Should().Be(HttpStatusCode.BadRequest); response.Headers.Should().Contain(header => header.Key == TraceIdHeaderName); - response.Should().HaveClientError(); } [Fact(DisplayName = "Register users returns unique trace IDs", Timeout = 5000)] @@ -40,7 +42,7 @@ public async Task RegisterUser_ReturnsUniqueTraceIds() successMessage.Headers.Should().Contain(header => header.Key == TraceIdHeaderName); failureMessage.Headers.Should().Contain(header => header.Key == TraceIdHeaderName); - failureMessage.Should().HaveClientError(); + failureMessage.StatusCode.Should().Be(HttpStatusCode.BadRequest); var successMessageTraceId = successMessage.Headers.GetValues(TraceIdHeaderName).ToList(); var failureMessageTraceId = failureMessage.Headers.GetValues(TraceIdHeaderName).ToList(); diff --git a/tests/CrowdParlay.Users.IntegrationTests/Tests/UsersControllerTests.cs b/tests/CrowdParlay.Users.IntegrationTests/Tests/UsersControllerTests.cs index 023e4b0..64a44b4 100644 --- a/tests/CrowdParlay.Users.IntegrationTests/Tests/UsersControllerTests.cs +++ b/tests/CrowdParlay.Users.IntegrationTests/Tests/UsersControllerTests.cs @@ -13,6 +13,8 @@ namespace CrowdParlay.Users.IntegrationTests.Tests; +[Collection(nameof(UsersControllerTests))] +[CollectionDefinition(nameof(UsersControllerTests), DisableParallelization = true)] public class UsersControllerTests : IAssemblyFixture { private readonly HttpClient _client; @@ -50,7 +52,7 @@ public async Task RegisterUsernames_Positive(string username, HttpStatusCode exp avatarUrl: null); var response = await _client.PostAsJsonAsync("/api/v1/users/register", request, GlobalSerializerOptions.SnakeCase); - response.Should().HaveStatusCode(expectedStatusCode); + response.StatusCode.Should().Be(expectedStatusCode); } [Fact(DisplayName = "Register user returns new user and publishes event")] @@ -84,7 +86,7 @@ public async Task Register_Negative() var registerRequestDuplicate = new Register.Command("us55e3rn44me3333e", "meily@tup.ye", "display name 2", "password2!", null); var duplicateMessage = await _client.PostAsJsonAsync("/api/v1/users/register", registerRequestDuplicate, GlobalSerializerOptions.SnakeCase); - duplicateMessage.Should().HaveStatusCode(HttpStatusCode.BadRequest); + duplicateMessage.StatusCode.Should().Be(HttpStatusCode.BadRequest); } [Fact(DisplayName = "Register user with invalid username returns validation failures", Timeout = 5000)] @@ -92,7 +94,7 @@ public async Task RegisterValidation_Negative() { var registerRequest = new Register.Command(string.Empty, string.Empty, string.Empty, "Password", null); var registerMessage = await _client.PostAsJsonAsync("/api/v1/users/register", registerRequest, GlobalSerializerOptions.SnakeCase); - registerMessage.Should().HaveStatusCode(HttpStatusCode.BadRequest); + registerMessage.StatusCode.Should().Be(HttpStatusCode.BadRequest); var validationProblem = await registerMessage.Content.ReadFromJsonAsync(GlobalSerializerOptions.SnakeCase); validationProblem!.ValidationErrors.Should().ContainKey("username").WhoseValue.Should().HaveCount(3); @@ -107,7 +109,7 @@ public async Task GetById_Positive() var registerResponse = await registerMessage.Content.ReadFromJsonAsync(GlobalSerializerOptions.SnakeCase); var getByIdMessage = await _client.GetAsync($"/api/v1/users/{registerResponse!.Id}"); - getByIdMessage.Should().HaveStatusCode(HttpStatusCode.OK); + getByIdMessage.StatusCode.Should().Be(HttpStatusCode.OK); var getByIdResponse = await getByIdMessage.Content.ReadFromJsonAsync(GlobalSerializerOptions.SnakeCase); getByIdResponse.Should().Be(new GetById.Response( @@ -125,7 +127,7 @@ public async Task GetByUsername_Positive() var registerResponse = await registerMessage.Content.ReadFromJsonAsync(GlobalSerializerOptions.SnakeCase); var getByUsernameMessage = await _client.GetAsync($"/api/v1/users/resolve?username={registerResponse!.Username}"); - getByUsernameMessage.Should().HaveStatusCode(HttpStatusCode.OK); + getByUsernameMessage.StatusCode.Should().Be(HttpStatusCode.OK); var getByUsernameResponse = await getByUsernameMessage.Content.ReadFromJsonAsync(GlobalSerializerOptions.SnakeCase); getByUsernameResponse.Should().Be(new GetByUsername.Response( @@ -160,7 +162,7 @@ public async Task Update_Positive() }; var updateMessage = await _client.SendAsync(requestMessage); - updateMessage.Should().HaveStatusCode(HttpStatusCode.OK); + updateMessage.StatusCode.Should().Be(HttpStatusCode.OK); var updateResponse = await updateMessage.Content.ReadFromJsonAsync(GlobalSerializerOptions.SnakeCase); updateResponse.Should().Be(new Update.Response( @@ -178,7 +180,7 @@ public async Task Update_Positive() updateResponse.AvatarUrl)); var getByIdMessage = await _client.GetAsync($"/api/v1/users/{registerResponse.Id}"); - getByIdMessage.Should().HaveStatusCode(HttpStatusCode.OK); + getByIdMessage.StatusCode.Should().Be(HttpStatusCode.OK); var getByIdResponse = await getByIdMessage.Content.ReadFromJsonAsync(GlobalSerializerOptions.SnakeCase); getByIdResponse.Should().Be(new GetById.Response( @@ -213,7 +215,7 @@ public async Task UpdatePassword_Positive() }; var updateMessage = await _client.SendAsync(requestMessage); - updateMessage.Should().HaveStatusCode(HttpStatusCode.OK); + updateMessage.StatusCode.Should().Be(HttpStatusCode.OK); var updateResponse = await updateMessage.Content.ReadFromJsonAsync(GlobalSerializerOptions.SnakeCase); updateResponse.Should().Be(new Update.Response( From 5ae02150c88786ea801bb7a7e007b45aa5f04714 Mon Sep 17 00:00:00 2001 From: undrcrxwn <69521267+undrcrxwn@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:45:02 +0300 Subject: [PATCH 2/5] docs: fix typo --- src/CrowdParlay.Users.Api/Services/KebabCaseParameterPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CrowdParlay.Users.Api/Services/KebabCaseParameterPolicy.cs b/src/CrowdParlay.Users.Api/Services/KebabCaseParameterPolicy.cs index e21d4e8..3e64d9c 100644 --- a/src/CrowdParlay.Users.Api/Services/KebabCaseParameterPolicy.cs +++ b/src/CrowdParlay.Users.Api/Services/KebabCaseParameterPolicy.cs @@ -4,7 +4,7 @@ namespace CrowdParlay.Users.Api.Services; /// /// An endpoint parameter transformer that applies kebab-case naming convention to endpoint routes and parameter names. -/// For example, turns '/Animal/8/AddType' into '/animals/8/add-type'. +/// For example, turns '/Animals/8/AddType' into '/animals/8/add-type'. /// public partial class KebabCaseParameterPolicy : IOutboundParameterTransformer { From c817a25909386cdb12e8f6f043d606f89bdfae1d Mon Sep 17 00:00:00 2001 From: undrcrxwn <69521267+undrcrxwn@users.noreply.github.com> Date: Mon, 23 Jun 2025 20:54:48 +0300 Subject: [PATCH 3/5] feat: disable username validation for Users.GetByUsername use-case --- .../Features/Users/Queries/GetByUsername.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/CrowdParlay.Users.Application/Features/Users/Queries/GetByUsername.cs b/src/CrowdParlay.Users.Application/Features/Users/Queries/GetByUsername.cs index e18c298..64850f9 100644 --- a/src/CrowdParlay.Users.Application/Features/Users/Queries/GetByUsername.cs +++ b/src/CrowdParlay.Users.Application/Features/Users/Queries/GetByUsername.cs @@ -1,20 +1,14 @@ using CrowdParlay.Users.Application.Exceptions; -using CrowdParlay.Users.Application.Extensions; using CrowdParlay.Users.Domain.Abstractions; using Dodo.Primitives; -using FluentValidation; using Mediator; namespace CrowdParlay.Users.Application.Features.Users.Queries; + public static class GetByUsername { public sealed record Query(string Username) : IRequest; - public sealed class Validator : AbstractValidator - { - public Validator() => RuleFor(x => x.Username).Username(); - } - public sealed class Handler : IRequestHandler { private readonly IUsersRepository _users; @@ -33,4 +27,4 @@ await _users.GetByUsernameExactAsync(request.Username, cancellationToken) public sealed record Response(Uuid Id, string Username, string DisplayName, string? AvatarUrl); -} +} \ No newline at end of file From dcdc8c096e0f1cb425545f8d8be2f6a36b2bdc4f Mon Sep 17 00:00:00 2001 From: undrcrxwn <69521267+undrcrxwn@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:12:06 +0300 Subject: [PATCH 4/5] feat: implement Users.Search use-case --- .../v1/Controllers/UsersController.cs | 13 ++++ .../Features/Users/Queries/Search.cs | 42 +++++++++++++ .../Abstractions/IAsyncGenericRepository.cs | 1 - .../Abstractions/IUsersRepository.cs | 1 + src/CrowdParlay.Users.Domain/Page.cs | 3 + .../SortingStrategy.cs | 7 +++ .../Order.cs | 7 +++ .../Services/UsersRepository.cs | 60 +++++++++++++++---- .../Tests/UsersControllerTests.cs | 32 ++++++++++ 9 files changed, 153 insertions(+), 13 deletions(-) create mode 100644 src/CrowdParlay.Users.Application/Features/Users/Queries/Search.cs create mode 100644 src/CrowdParlay.Users.Domain/Page.cs create mode 100644 src/CrowdParlay.Users.Domain/SortingStrategy.cs create mode 100644 src/CrowdParlay.Users.Infrastructure.Persistence/Order.cs diff --git a/src/CrowdParlay.Users.Api/v1/Controllers/UsersController.cs b/src/CrowdParlay.Users.Api/v1/Controllers/UsersController.cs index 5aec256..f4a377a 100644 --- a/src/CrowdParlay.Users.Api/v1/Controllers/UsersController.cs +++ b/src/CrowdParlay.Users.Api/v1/Controllers/UsersController.cs @@ -5,6 +5,7 @@ using CrowdParlay.Users.Application.Exceptions; using CrowdParlay.Users.Application.Features.Users.Commands; using CrowdParlay.Users.Application.Features.Users.Queries; +using CrowdParlay.Users.Domain; using Dodo.Primitives; using Mapster; using Microsoft.AspNetCore.Authorization; @@ -15,6 +16,17 @@ namespace CrowdParlay.Users.Api.v1.Controllers; [ApiVersion("1.0")] public class UsersController : ApiControllerBase { + /// + /// Returns users. + /// + [HttpGet] + [Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Json)] + [ProducesResponseType(typeof(Page), (int)HttpStatusCode.OK)] + [ProducesResponseType(typeof(Problem), (int)HttpStatusCode.InternalServerError)] + [ProducesResponseType(typeof(ValidationProblem), (int)HttpStatusCode.BadRequest)] + public async Task> Search(SortingStrategy order, int offset, int count) => + await Mediator.Send(new Search.Query(order, offset, count)); + /// /// Creates a user. /// @@ -39,6 +51,7 @@ public class UsersController : ApiControllerBase [Consumes(MediaTypeNames.Application.Json), Produces(MediaTypeNames.Application.Json)] [ProducesResponseType(typeof(GetById.Response), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(Problem), (int)HttpStatusCode.InternalServerError)] + [ProducesResponseType(typeof(ValidationProblem), (int)HttpStatusCode.BadRequest)] [ProducesResponseType(typeof(Problem), (int)HttpStatusCode.NotFound)] public async Task GetById([FromRoute] Uuid userId) => await Mediator.Send(new GetById.Query(userId)); diff --git a/src/CrowdParlay.Users.Application/Features/Users/Queries/Search.cs b/src/CrowdParlay.Users.Application/Features/Users/Queries/Search.cs new file mode 100644 index 0000000..5240de6 --- /dev/null +++ b/src/CrowdParlay.Users.Application/Features/Users/Queries/Search.cs @@ -0,0 +1,42 @@ +using CrowdParlay.Users.Domain; +using CrowdParlay.Users.Domain.Abstractions; +using Dodo.Primitives; +using FluentValidation; +using Mediator; + +namespace CrowdParlay.Users.Application.Features.Users.Queries; + +public static class Search +{ + public sealed record Query(SortingStrategy Order, int Offset, int Count) : IRequest>; + + public sealed class Validator : AbstractValidator + { + public Validator() + { + RuleFor(x => x.Offset).GreaterThanOrEqualTo(0); + RuleFor(x => x.Count).InclusiveBetween(1, 100); + } + } + + public sealed class Handler : IRequestHandler> + { + private readonly IUsersRepository _users; + + public Handler(IUsersRepository users) => _users = users; + + public async ValueTask> Handle(Query request, CancellationToken cancellationToken) + { + var page = await _users.SearchAsync(request.Order, request.Offset, request.Count, cancellationToken); + return new Page( + page.TotalCount, + page.Items.Select(user => new Response( + user.Id, + user.Username, + user.DisplayName, + user.AvatarUrl))); + } + } + + public sealed record Response(Uuid Id, string Username, string DisplayName, string? AvatarUrl); +} \ No newline at end of file diff --git a/src/CrowdParlay.Users.Domain/Abstractions/IAsyncGenericRepository.cs b/src/CrowdParlay.Users.Domain/Abstractions/IAsyncGenericRepository.cs index ba36812..53dabd0 100644 --- a/src/CrowdParlay.Users.Domain/Abstractions/IAsyncGenericRepository.cs +++ b/src/CrowdParlay.Users.Domain/Abstractions/IAsyncGenericRepository.cs @@ -5,7 +5,6 @@ namespace CrowdParlay.Users.Domain.Abstractions; public interface IAsyncGenericRepository where TEntity : EntityBase where TKey : notnull { public Task GetByIdAsync(TKey id, CancellationToken cancellationToken = default); - public Task> GetManyAsync(int count, int page = 0, CancellationToken cancellationToken = default); public Task AddAsync(TEntity entity, CancellationToken cancellationToken = default); public Task UpdateAsync(TEntity entity, CancellationToken cancellationToken = default); public Task DeleteAsync(TKey id, CancellationToken cancellationToken = default); diff --git a/src/CrowdParlay.Users.Domain/Abstractions/IUsersRepository.cs b/src/CrowdParlay.Users.Domain/Abstractions/IUsersRepository.cs index eebc248..40c160f 100644 --- a/src/CrowdParlay.Users.Domain/Abstractions/IUsersRepository.cs +++ b/src/CrowdParlay.Users.Domain/Abstractions/IUsersRepository.cs @@ -5,6 +5,7 @@ namespace CrowdParlay.Users.Domain.Abstractions; public interface IUsersRepository : IAsyncGenericRepository { + public Task> SearchAsync(SortingStrategy order, int offset, int count, CancellationToken cancellationToken = default); public IAsyncEnumerable GetByIdsAsync(Guid[] ids, CancellationToken cancellationToken = default); public Task GetByUsernameExactAsync(string username, CancellationToken cancellationToken = default); public Task GetByUsernameNormalizedAsync(string username, CancellationToken cancellationToken = default); diff --git a/src/CrowdParlay.Users.Domain/Page.cs b/src/CrowdParlay.Users.Domain/Page.cs new file mode 100644 index 0000000..9f55aeb --- /dev/null +++ b/src/CrowdParlay.Users.Domain/Page.cs @@ -0,0 +1,3 @@ +namespace CrowdParlay.Users.Domain; + +public record Page(int TotalCount, IEnumerable Items); \ No newline at end of file diff --git a/src/CrowdParlay.Users.Domain/SortingStrategy.cs b/src/CrowdParlay.Users.Domain/SortingStrategy.cs new file mode 100644 index 0000000..b89b3b0 --- /dev/null +++ b/src/CrowdParlay.Users.Domain/SortingStrategy.cs @@ -0,0 +1,7 @@ +namespace CrowdParlay.Users.Domain; + +public enum SortingStrategy +{ + NewestFirst, + OldestFirst, +} \ No newline at end of file diff --git a/src/CrowdParlay.Users.Infrastructure.Persistence/Order.cs b/src/CrowdParlay.Users.Infrastructure.Persistence/Order.cs new file mode 100644 index 0000000..30e3fe2 --- /dev/null +++ b/src/CrowdParlay.Users.Infrastructure.Persistence/Order.cs @@ -0,0 +1,7 @@ +namespace CrowdParlay.Users.Infrastructure.Persistence; + +public enum Order +{ + Ascending, + Descending +} \ No newline at end of file diff --git a/src/CrowdParlay.Users.Infrastructure.Persistence/Services/UsersRepository.cs b/src/CrowdParlay.Users.Infrastructure.Persistence/Services/UsersRepository.cs index cdeedc4..50e1eaf 100644 --- a/src/CrowdParlay.Users.Infrastructure.Persistence/Services/UsersRepository.cs +++ b/src/CrowdParlay.Users.Infrastructure.Persistence/Services/UsersRepository.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using CrowdParlay.Users.Application.Exceptions; +using CrowdParlay.Users.Domain; using CrowdParlay.Users.Domain.Abstractions; using CrowdParlay.Users.Domain.Entities; using CrowdParlay.Users.Infrastructure.Persistence.Abstractions; @@ -15,6 +16,49 @@ internal class UsersRepository : IUsersRepository public UsersRepository(IDbConnectionFactory connectionFactory) => _connectionFactory = connectionFactory; + public async Task> SearchAsync(SortingStrategy order, int offset, int count, CancellationToken cancellationToken) + { + var orderByCreatedAtDirection = order switch + { + SortingStrategy.NewestFirst => "DESC", + SortingStrategy.OldestFirst => "ASC", + _ => throw new ArgumentOutOfRangeException(nameof(order), order, "The specified order is not supported.") + }; + + await using var connection = await _connectionFactory.CreateConnectionAsync(cancellationToken); + var result = await connection.QueryMultipleAsync( + $""" + SELECT COUNT(*) FROM {UsersSchema.Table}; + SELECT * FROM {UsersSchema.Table} + ORDER BY {UsersSchema.CreatedAt} {orderByCreatedAtDirection} + LIMIT @{nameof(count)} OFFSET @{nameof(offset)}; + """, + new { offset, count }); + + var totalCount = await result.ReadSingleAsync(); + var users = await result.ReadAsync(); + return new Page(totalCount, users); + } + + public async Task> SearchAsync(Order order, int offset, int count, CancellationToken cancellationToken) + { + var orderDirection = order switch + { + Order.Ascending => "ASC", + Order.Descending => "DESC", + _ => throw new ArgumentOutOfRangeException(nameof(order), order, "The specified order is not supported.") + }; + + await using var connection = await _connectionFactory.CreateConnectionAsync(cancellationToken); + return await connection.QueryAsync( + $""" + SELECT * FROM {UsersSchema.Table} + ORDER BY {UsersSchema.CreatedAt} {orderDirection} + LIMIT @{nameof(count)} OFFSET @{nameof(offset)} + """, + new { offset, count }); + } + public async IAsyncEnumerable GetByIdsAsync(Guid[] ids, [EnumeratorCancellation] CancellationToken cancellationToken) { await using var connection = await _connectionFactory.CreateConnectionAsync(cancellationToken); @@ -64,21 +108,13 @@ public async IAsyncEnumerable GetByIdsAsync(Guid[] ids, [EnumeratorCancell await using var connection = await _connectionFactory.CreateConnectionAsync(cancellationToken); return await connection.QuerySingleOrDefaultAsync( $""" - SELECT * FROM {UsersSchema.Table} - WHERE {UsersSchema.UsernameNormalized} = normalize_username(@{nameof(usernameOrEmail)}) - OR {UsersSchema.EmailNormalized} = normalize_email(@{nameof(usernameOrEmail)}) - """, + SELECT * FROM {UsersSchema.Table} + WHERE {UsersSchema.UsernameNormalized} = normalize_username(@{nameof(usernameOrEmail)}) + OR {UsersSchema.EmailNormalized} = normalize_email(@{nameof(usernameOrEmail)}) + """, new { usernameOrEmail }); } - public async Task> GetManyAsync(int count, int page, CancellationToken cancellationToken) - { - await using var connection = await _connectionFactory.CreateConnectionAsync(cancellationToken); - return await connection.QueryAsync( - $"SELECT * FROM {UsersSchema.Table} LIMIT @{nameof(count)} OFFSET @{nameof(count)} * @{nameof(page)}", - new { count, page }); - } - public async Task AddAsync(User entity, CancellationToken cancellationToken) { await using var connection = await _connectionFactory.CreateConnectionAsync(cancellationToken); diff --git a/tests/CrowdParlay.Users.IntegrationTests/Tests/UsersControllerTests.cs b/tests/CrowdParlay.Users.IntegrationTests/Tests/UsersControllerTests.cs index 64a44b4..09b00b9 100644 --- a/tests/CrowdParlay.Users.IntegrationTests/Tests/UsersControllerTests.cs +++ b/tests/CrowdParlay.Users.IntegrationTests/Tests/UsersControllerTests.cs @@ -6,10 +6,16 @@ using CrowdParlay.Users.Api; using CrowdParlay.Users.Application.Features.Users.Commands; using CrowdParlay.Users.Application.Features.Users.Queries; +using CrowdParlay.Users.Domain; +using CrowdParlay.Users.Domain.Abstractions; +using CrowdParlay.Users.Domain.Entities; using CrowdParlay.Users.IntegrationTests.Extensions; using CrowdParlay.Users.IntegrationTests.Fixtures; +using Dodo.Primitives; using FluentAssertions; using MassTransit.Testing; +using Mediator; +using Microsoft.Extensions.DependencyInjection; namespace CrowdParlay.Users.IntegrationTests.Tests; @@ -19,11 +25,37 @@ public class UsersControllerTests : IAssemblyFixture { private readonly HttpClient _client; private readonly ITestHarness _harness; + private readonly IServiceScopeFactory _serviceScopeFactory; public UsersControllerTests(WebApplicationFixture fixture) { _client = fixture.WebApplicationFactory.CreateClient(); _harness = fixture.Services.GetTestHarness(); + _serviceScopeFactory = fixture.Services.GetRequiredService(); + } + + [Fact(DisplayName = "Search users")] + public async Task SearchUsers_Positive() + { + await using var scope = _serviceScopeFactory.CreateAsyncScope(); + var sender = scope.ServiceProvider.GetRequiredService(); + + var registerUsersTasks = Enumerable.Range(0, 200).Select(i => sender.Send(new Register.Command( + username: $"test_user_{i}_{Guid.NewGuid():N}"[..25], + email: $"test_user_{i}@example.com", + displayName: $"test_user_{i}", + password: Guid.NewGuid().ToString("N")[..25], + avatarUrl: null))); + + foreach (var task in registerUsersTasks) + await task; + + var response = await _client.GetFromJsonAsync>( + "/api/v1/users?order=newestFirst&offset=0&count=100", + GlobalSerializerOptions.SnakeCase); + + response!.TotalCount.Should().BeGreaterOrEqualTo(200); + response.Items.Count().Should().Be(100); } [Theory(DisplayName = "Register users")] From 9592de36abfa3ae5bc5aa165fdb877975c72dc35 Mon Sep 17 00:00:00 2001 From: undrcrxwn <69521267+undrcrxwn@users.noreply.github.com> Date: Mon, 23 Jun 2025 23:48:22 +0300 Subject: [PATCH 5/5] feat: replace Dodo.Primitives.Uuid with Guid.CreateVersion7() --- .../Extensions/HttpContextExtensions.cs | 7 +++---- .../OpenIddict/RefreshTokenGrantEventHandler.cs | 3 +-- .../Services/gRPC/UsersService.cs | 3 +-- .../Swagger/SwaggerWebHostFactory.cs | 3 +-- .../v1/Controllers/UsersController.cs | 7 +++---- src/CrowdParlay.Users.Api/v1/DTOs/UsersDTOs.cs | 4 +--- .../Features/Users/Commands/Delete.cs | 3 +-- .../Features/Users/Commands/Register.cs | 7 +++---- .../Features/Users/Commands/Update.cs | 5 ++--- .../Features/Users/Queries/GetById.cs | 5 ++--- .../Features/Users/Queries/GetByUsername.cs | 3 +-- .../Features/Users/Queries/Search.cs | 3 +-- .../Services/GoogleAuthenticationService.cs | 3 +-- .../Abstractions/IExternalLoginsRepository.cs | 5 ++--- .../Abstractions/IUsersRepository.cs | 3 +-- .../CrowdParlay.Users.Domain.csproj | 4 ---- .../Entities/ExternalLogin.cs | 6 ++---- src/CrowdParlay.Users.Domain/Entities/User.cs | 4 +--- .../Extensions/ConfigureServices.cs | 2 -- .../Services/ExternalLoginsRepository.cs | 5 ++--- .../Services/UsersRepository.cs | 5 ++--- .../SqlTypeHandlers/DodoUuidTypeHandler.cs | 13 ------------- 22 files changed, 31 insertions(+), 72 deletions(-) delete mode 100644 src/CrowdParlay.Users.Infrastructure.Persistence/SqlTypeHandlers/DodoUuidTypeHandler.cs diff --git a/src/CrowdParlay.Users.Api/Extensions/HttpContextExtensions.cs b/src/CrowdParlay.Users.Api/Extensions/HttpContextExtensions.cs index a1efdcc..9a0f23a 100644 --- a/src/CrowdParlay.Users.Api/Extensions/HttpContextExtensions.cs +++ b/src/CrowdParlay.Users.Api/Extensions/HttpContextExtensions.cs @@ -1,5 +1,4 @@ using System.Security.Claims; -using Dodo.Primitives; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using OpenIddict.Abstractions; @@ -9,16 +8,16 @@ namespace CrowdParlay.Users.Api.Extensions; public static class HttpContextExtensions { - public static Uuid? GetUserId(this HttpContext context) + public static Guid? GetUserId(this HttpContext context) { var userId = context.User.GetClaim(CookieAuthenticationConstants.UserIdClaim) ?? context.User.GetClaim(OpenIddictConstants.Claims.Subject); - return Uuid.TryParse(userId, out var value) ? value : null; + return Guid.TryParse(userId, out var value) ? value : null; } - public static async Task SignInAsync(this HttpContext context, string authenticationScheme, Uuid userId) + public static async Task SignInAsync(this HttpContext context, string authenticationScheme, Guid userId) { var userIdClaimType = authenticationScheme switch { diff --git a/src/CrowdParlay.Users.Api/Services/OpenIddict/RefreshTokenGrantEventHandler.cs b/src/CrowdParlay.Users.Api/Services/OpenIddict/RefreshTokenGrantEventHandler.cs index 72ea6a3..1029ad0 100644 --- a/src/CrowdParlay.Users.Api/Services/OpenIddict/RefreshTokenGrantEventHandler.cs +++ b/src/CrowdParlay.Users.Api/Services/OpenIddict/RefreshTokenGrantEventHandler.cs @@ -2,7 +2,6 @@ using System.Security.Claims; using CrowdParlay.Users.Api.Extensions; using CrowdParlay.Users.Domain.Abstractions; -using Dodo.Primitives; using Microsoft.IdentityModel.Tokens; using OpenIddict.Abstractions; using OpenIddict.Server; @@ -23,7 +22,7 @@ public async ValueTask HandleAsync(OpenIddictServerEvents.HandleTokenRequestCont return; var jwt = _jwtHandler.ReadJwtToken(context.Request.RefreshToken); - var userId = Uuid.Parse(jwt.Subject); + var userId = Guid.Parse(jwt.Subject); var user = await _usersRepository.GetByIdAsync(userId); if (user is null) { diff --git a/src/CrowdParlay.Users.Api/Services/gRPC/UsersService.cs b/src/CrowdParlay.Users.Api/Services/gRPC/UsersService.cs index 3552367..6d2ed78 100644 --- a/src/CrowdParlay.Users.Api/Services/gRPC/UsersService.cs +++ b/src/CrowdParlay.Users.Api/Services/gRPC/UsersService.cs @@ -1,6 +1,5 @@ using CrowdParlay.Users.Domain.Abstractions; using CrowdParlay.Users.gRPC; -using Dodo.Primitives; using Google.Protobuf.WellKnownTypes; using Google.Rpc; using Grpc.Core; @@ -17,7 +16,7 @@ public class UsersGrpcService : UsersService.UsersServiceBase public override async Task GetUser(GetUserRequest request, ServerCallContext context) { - var user = await _users.GetByIdAsync(Uuid.Parse(request.Id)); + var user = await _users.GetByIdAsync(Guid.Parse(request.Id)); return user.Adapt(); } diff --git a/src/CrowdParlay.Users.Api/Swagger/SwaggerWebHostFactory.cs b/src/CrowdParlay.Users.Api/Swagger/SwaggerWebHostFactory.cs index 2cc6c30..b2daa6e 100644 --- a/src/CrowdParlay.Users.Api/Swagger/SwaggerWebHostFactory.cs +++ b/src/CrowdParlay.Users.Api/Swagger/SwaggerWebHostFactory.cs @@ -1,6 +1,5 @@ using System.Reflection; using CrowdParlay.Users.Api.Extensions; -using Dodo.Primitives; using Microsoft.AspNetCore; using Microsoft.Extensions.Options; using Microsoft.OpenApi.Models; @@ -33,7 +32,7 @@ public static IWebHost CreateWebHost() => WebHost.CreateDefaultBuilder() options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlDocsFileName)); options.SupportNonNullableReferenceTypes(); - options.MapType(() => new OpenApiSchema + options.MapType(() => new OpenApiSchema { Type = "string", Format = "uuid" diff --git a/src/CrowdParlay.Users.Api/v1/Controllers/UsersController.cs b/src/CrowdParlay.Users.Api/v1/Controllers/UsersController.cs index e89510d..bec1a8c 100644 --- a/src/CrowdParlay.Users.Api/v1/Controllers/UsersController.cs +++ b/src/CrowdParlay.Users.Api/v1/Controllers/UsersController.cs @@ -9,7 +9,6 @@ using CrowdParlay.Users.Application.Features.Users.Queries; using CrowdParlay.Users.Application.Models; using CrowdParlay.Users.Domain; -using Dodo.Primitives; using Mapster; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; @@ -86,7 +85,7 @@ public UsersController(IDataProtectionProvider dataProtectionProvider) => [ProducesResponseType(typeof(GetById.Response), (int)HttpStatusCode.OK)] [ProducesResponseType(typeof(Problem), (int)HttpStatusCode.InternalServerError)] [ProducesResponseType(typeof(Problem), (int)HttpStatusCode.NotFound)] - public async Task GetById([FromRoute] Uuid userId) => + public async Task GetById([FromRoute] Guid userId) => await Mediator.Send(new GetById.Query(userId)); /// @@ -122,7 +121,7 @@ public UsersController(IDataProtectionProvider dataProtectionProvider) => [ProducesResponseType(typeof(ValidationProblem), (int)HttpStatusCode.BadRequest)] [ProducesResponseType(typeof(Problem), (int)HttpStatusCode.Forbidden)] [ProducesResponseType(typeof(Problem), (int)HttpStatusCode.NotFound)] - public async Task Update([FromRoute] Uuid userId, [FromBody] UsersUpdateRequest request) + public async Task Update([FromRoute] Guid userId, [FromBody] UsersUpdateRequest request) { if (userId != HttpContext.GetUserId()) throw new ForbiddenException(); @@ -139,7 +138,7 @@ public UsersController(IDataProtectionProvider dataProtectionProvider) => [ProducesResponseType(typeof(Problem), (int)HttpStatusCode.InternalServerError)] [ProducesResponseType(typeof(Problem), (int)HttpStatusCode.Forbidden)] [ProducesResponseType(typeof(Problem), (int)HttpStatusCode.NotFound)] - public async Task Delete([FromRoute] Uuid userId) + public async Task Delete([FromRoute] Guid userId) { if (userId != HttpContext.GetUserId()) throw new ForbiddenException(); diff --git a/src/CrowdParlay.Users.Api/v1/DTOs/UsersDTOs.cs b/src/CrowdParlay.Users.Api/v1/DTOs/UsersDTOs.cs index cfd6ce7..cf53fd9 100644 --- a/src/CrowdParlay.Users.Api/v1/DTOs/UsersDTOs.cs +++ b/src/CrowdParlay.Users.Api/v1/DTOs/UsersDTOs.cs @@ -1,9 +1,7 @@ -using Dodo.Primitives; - namespace CrowdParlay.Users.Api.v1.DTOs; public sealed record UserInfoResponse( - Uuid Id, + Guid Id, string Username, string DisplayName, string? AvatarUrl); diff --git a/src/CrowdParlay.Users.Application/Features/Users/Commands/Delete.cs b/src/CrowdParlay.Users.Application/Features/Users/Commands/Delete.cs index ae821b4..c160207 100644 --- a/src/CrowdParlay.Users.Application/Features/Users/Commands/Delete.cs +++ b/src/CrowdParlay.Users.Application/Features/Users/Commands/Delete.cs @@ -1,6 +1,5 @@ using CrowdParlay.Communication; using CrowdParlay.Users.Domain.Abstractions; -using Dodo.Primitives; using FluentValidation; using MassTransit; using Mediator; @@ -9,7 +8,7 @@ namespace CrowdParlay.Users.Application.Features.Users.Commands; public static class Delete { - public sealed record Command(Uuid Id) : IRequest; + public sealed record Command(Guid Id) : IRequest; public sealed class Validator : AbstractValidator { diff --git a/src/CrowdParlay.Users.Application/Features/Users/Commands/Register.cs b/src/CrowdParlay.Users.Application/Features/Users/Commands/Register.cs index 4c6be72..7e04405 100644 --- a/src/CrowdParlay.Users.Application/Features/Users/Commands/Register.cs +++ b/src/CrowdParlay.Users.Application/Features/Users/Commands/Register.cs @@ -4,7 +4,6 @@ using CrowdParlay.Users.Application.Models; using CrowdParlay.Users.Domain.Abstractions; using CrowdParlay.Users.Domain.Entities; -using Dodo.Primitives; using FluentValidation; using MassTransit; using Mediator; @@ -91,7 +90,7 @@ public async ValueTask Handle(Command request, CancellationToken cance var user = new User { - Id = Uuid.NewTimeBased(), + Id = Guid.CreateVersion7(), Username = request.Username, Email = request.Email, DisplayName = request.DisplayName, @@ -110,7 +109,7 @@ public async ValueTask Handle(Command request, CancellationToken cance var externalLogin = new ExternalLogin { - Id = Uuid.NewTimeBased(), + Id = Guid.CreateVersion7(), UserId = user.Id, ProviderId = request.ExternalLoginTicket.ProviderId, Identity = request.ExternalLoginTicket.Identity @@ -122,7 +121,7 @@ public async ValueTask Handle(Command request, CancellationToken cance } public sealed record Response( - Uuid Id, + Guid Id, string Username, string Email, string DisplayName, diff --git a/src/CrowdParlay.Users.Application/Features/Users/Commands/Update.cs b/src/CrowdParlay.Users.Application/Features/Users/Commands/Update.cs index 164381f..217eb54 100644 --- a/src/CrowdParlay.Users.Application/Features/Users/Commands/Update.cs +++ b/src/CrowdParlay.Users.Application/Features/Users/Commands/Update.cs @@ -3,7 +3,6 @@ using CrowdParlay.Users.Application.Exceptions; using CrowdParlay.Users.Application.Extensions; using CrowdParlay.Users.Domain.Abstractions; -using Dodo.Primitives; using FluentValidation; using MassTransit; using Mediator; @@ -14,7 +13,7 @@ namespace CrowdParlay.Users.Application.Features.Users.Commands; public static class Update { public sealed record Command( - Uuid Id, + Guid Id, string? Username, string? DisplayName, string? Email, @@ -103,7 +102,7 @@ await _users.GetByIdAsync(request.Id, cancellationToken) } public sealed record Response( - Uuid Id, + Guid Id, string Username, string DisplayName, string Email, diff --git a/src/CrowdParlay.Users.Application/Features/Users/Queries/GetById.cs b/src/CrowdParlay.Users.Application/Features/Users/Queries/GetById.cs index 7e476be..5720515 100644 --- a/src/CrowdParlay.Users.Application/Features/Users/Queries/GetById.cs +++ b/src/CrowdParlay.Users.Application/Features/Users/Queries/GetById.cs @@ -1,6 +1,5 @@ using CrowdParlay.Users.Application.Exceptions; using CrowdParlay.Users.Domain.Abstractions; -using Dodo.Primitives; using FluentValidation; using Mediator; @@ -8,7 +7,7 @@ namespace CrowdParlay.Users.Application.Features.Users.Queries; public static class GetById { - public sealed record Query(Uuid Id) : IRequest; + public sealed record Query(Guid Id) : IRequest; public sealed class Validator : AbstractValidator { @@ -31,5 +30,5 @@ await _users.GetByIdAsync(request.Id, cancellationToken) } } - public sealed record Response(Uuid Id, string Username, string DisplayName, string? AvatarUrl); + public sealed record Response(Guid Id, string Username, string DisplayName, string? AvatarUrl); } \ No newline at end of file diff --git a/src/CrowdParlay.Users.Application/Features/Users/Queries/GetByUsername.cs b/src/CrowdParlay.Users.Application/Features/Users/Queries/GetByUsername.cs index 64850f9..6d8d3b3 100644 --- a/src/CrowdParlay.Users.Application/Features/Users/Queries/GetByUsername.cs +++ b/src/CrowdParlay.Users.Application/Features/Users/Queries/GetByUsername.cs @@ -1,6 +1,5 @@ using CrowdParlay.Users.Application.Exceptions; using CrowdParlay.Users.Domain.Abstractions; -using Dodo.Primitives; using Mediator; namespace CrowdParlay.Users.Application.Features.Users.Queries; @@ -26,5 +25,5 @@ await _users.GetByUsernameExactAsync(request.Username, cancellationToken) } - public sealed record Response(Uuid Id, string Username, string DisplayName, string? AvatarUrl); + public sealed record Response(Guid Id, string Username, string DisplayName, string? AvatarUrl); } \ No newline at end of file diff --git a/src/CrowdParlay.Users.Application/Features/Users/Queries/Search.cs b/src/CrowdParlay.Users.Application/Features/Users/Queries/Search.cs index 5240de6..5e69641 100644 --- a/src/CrowdParlay.Users.Application/Features/Users/Queries/Search.cs +++ b/src/CrowdParlay.Users.Application/Features/Users/Queries/Search.cs @@ -1,6 +1,5 @@ using CrowdParlay.Users.Domain; using CrowdParlay.Users.Domain.Abstractions; -using Dodo.Primitives; using FluentValidation; using Mediator; @@ -38,5 +37,5 @@ public async ValueTask> Handle(Query request, CancellationToken c } } - public sealed record Response(Uuid Id, string Username, string DisplayName, string? AvatarUrl); + public sealed record Response(Guid Id, string Username, string DisplayName, string? AvatarUrl); } \ No newline at end of file diff --git a/src/CrowdParlay.Users.Application/Services/GoogleAuthenticationService.cs b/src/CrowdParlay.Users.Application/Services/GoogleAuthenticationService.cs index 4a8af8d..84b8f99 100644 --- a/src/CrowdParlay.Users.Application/Services/GoogleAuthenticationService.cs +++ b/src/CrowdParlay.Users.Application/Services/GoogleAuthenticationService.cs @@ -2,7 +2,6 @@ using CrowdParlay.Users.Application.Models; using CrowdParlay.Users.Domain.Abstractions; using CrowdParlay.Users.Domain.Entities; -using Dodo.Primitives; using Microsoft.Extensions.Logging; using static CrowdParlay.Users.Application.Services.GoogleAuthenticationStatus; @@ -59,7 +58,7 @@ public async Task AuthenticateUserByAuthorizationCod var login = new ExternalLogin { - Id = Uuid.NewTimeBased(), + Id = Guid.CreateVersion7(), UserId = user.Id, ProviderId = GoogleAuthenticationConstants.ExternalLoginProviderId, Identity = googleUserInfo.Email diff --git a/src/CrowdParlay.Users.Domain/Abstractions/IExternalLoginsRepository.cs b/src/CrowdParlay.Users.Domain/Abstractions/IExternalLoginsRepository.cs index b45b154..2f493c5 100644 --- a/src/CrowdParlay.Users.Domain/Abstractions/IExternalLoginsRepository.cs +++ b/src/CrowdParlay.Users.Domain/Abstractions/IExternalLoginsRepository.cs @@ -1,11 +1,10 @@ using CrowdParlay.Users.Domain.Entities; -using Dodo.Primitives; namespace CrowdParlay.Users.Domain.Abstractions; public interface IExternalLoginsRepository { - public Task> GetByUserIdAsync(Uuid userId, CancellationToken cancellationToken = default); + public Task> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken = default); public Task AddAsync(ExternalLogin entity, CancellationToken cancellationToken = default); - public Task DeleteAsync(Uuid id, CancellationToken cancellationToken = default); + public Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/CrowdParlay.Users.Domain/Abstractions/IUsersRepository.cs b/src/CrowdParlay.Users.Domain/Abstractions/IUsersRepository.cs index 40c160f..fc31e4c 100644 --- a/src/CrowdParlay.Users.Domain/Abstractions/IUsersRepository.cs +++ b/src/CrowdParlay.Users.Domain/Abstractions/IUsersRepository.cs @@ -1,9 +1,8 @@ using CrowdParlay.Users.Domain.Entities; -using Dodo.Primitives; namespace CrowdParlay.Users.Domain.Abstractions; -public interface IUsersRepository : IAsyncGenericRepository +public interface IUsersRepository : IAsyncGenericRepository { public Task> SearchAsync(SortingStrategy order, int offset, int count, CancellationToken cancellationToken = default); public IAsyncEnumerable GetByIdsAsync(Guid[] ids, CancellationToken cancellationToken = default); diff --git a/src/CrowdParlay.Users.Domain/CrowdParlay.Users.Domain.csproj b/src/CrowdParlay.Users.Domain/CrowdParlay.Users.Domain.csproj index 0c85eb1..ec37b2f 100644 --- a/src/CrowdParlay.Users.Domain/CrowdParlay.Users.Domain.csproj +++ b/src/CrowdParlay.Users.Domain/CrowdParlay.Users.Domain.csproj @@ -6,8 +6,4 @@ enable - - - - \ No newline at end of file diff --git a/src/CrowdParlay.Users.Domain/Entities/ExternalLogin.cs b/src/CrowdParlay.Users.Domain/Entities/ExternalLogin.cs index ca0c05d..968f6b8 100644 --- a/src/CrowdParlay.Users.Domain/Entities/ExternalLogin.cs +++ b/src/CrowdParlay.Users.Domain/Entities/ExternalLogin.cs @@ -1,10 +1,8 @@ -using Dodo.Primitives; - namespace CrowdParlay.Users.Domain.Entities; -public class ExternalLogin : EntityBase +public class ExternalLogin : EntityBase { - public Uuid UserId { get; set; } + public Guid UserId { get; set; } public string ProviderId { get; set; } public string Identity { get; set; } } \ No newline at end of file diff --git a/src/CrowdParlay.Users.Domain/Entities/User.cs b/src/CrowdParlay.Users.Domain/Entities/User.cs index b8ca744..9536568 100644 --- a/src/CrowdParlay.Users.Domain/Entities/User.cs +++ b/src/CrowdParlay.Users.Domain/Entities/User.cs @@ -1,8 +1,6 @@ -using Dodo.Primitives; - namespace CrowdParlay.Users.Domain.Entities; -public class User : EntityBase +public class User : EntityBase { public required string Username { get; set; } public required string DisplayName { get; set; } diff --git a/src/CrowdParlay.Users.Infrastructure.Persistence/Extensions/ConfigureServices.cs b/src/CrowdParlay.Users.Infrastructure.Persistence/Extensions/ConfigureServices.cs index b0835c0..5c665bf 100644 --- a/src/CrowdParlay.Users.Infrastructure.Persistence/Extensions/ConfigureServices.cs +++ b/src/CrowdParlay.Users.Infrastructure.Persistence/Extensions/ConfigureServices.cs @@ -2,7 +2,6 @@ using CrowdParlay.Users.Domain.Abstractions; using CrowdParlay.Users.Infrastructure.Persistence.Abstractions; using CrowdParlay.Users.Infrastructure.Persistence.Services; -using CrowdParlay.Users.Infrastructure.Persistence.SqlTypeHandlers; using Dapper; using FluentMigrator.Runner; using Microsoft.EntityFrameworkCore; @@ -16,7 +15,6 @@ public static class ConfigureServices { public static IServiceCollection ConfigurePersistenceServices(this IServiceCollection services, IConfiguration configuration) { - SqlMapper.AddTypeHandler(new DodoUuidTypeHandler()); DefaultTypeMap.MatchNamesWithUnderscores = true; var connectionString = diff --git a/src/CrowdParlay.Users.Infrastructure.Persistence/Services/ExternalLoginsRepository.cs b/src/CrowdParlay.Users.Infrastructure.Persistence/Services/ExternalLoginsRepository.cs index 6663525..e6cc049 100644 --- a/src/CrowdParlay.Users.Infrastructure.Persistence/Services/ExternalLoginsRepository.cs +++ b/src/CrowdParlay.Users.Infrastructure.Persistence/Services/ExternalLoginsRepository.cs @@ -3,7 +3,6 @@ using CrowdParlay.Users.Domain.Entities; using CrowdParlay.Users.Infrastructure.Persistence.Abstractions; using Dapper; -using Dodo.Primitives; namespace CrowdParlay.Users.Infrastructure.Persistence.Services; @@ -14,7 +13,7 @@ public class ExternalLoginsRepository : IExternalLoginsRepository public ExternalLoginsRepository(IDbConnectionFactory connectionFactory) => _connectionFactory = connectionFactory; - public async Task> GetByUserIdAsync(Uuid userId, CancellationToken cancellationToken) + public async Task> GetByUserIdAsync(Guid userId, CancellationToken cancellationToken) { await using var connection = await _connectionFactory.CreateConnectionAsync(cancellationToken); return await connection.QueryAsync( @@ -43,7 +42,7 @@ await connection.ExecuteAsync( entity); } - public async Task DeleteAsync(Uuid id, CancellationToken cancellationToken) + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken) { await using var connection = await _connectionFactory.CreateConnectionAsync(cancellationToken); diff --git a/src/CrowdParlay.Users.Infrastructure.Persistence/Services/UsersRepository.cs b/src/CrowdParlay.Users.Infrastructure.Persistence/Services/UsersRepository.cs index 60e7891..5b21c03 100644 --- a/src/CrowdParlay.Users.Infrastructure.Persistence/Services/UsersRepository.cs +++ b/src/CrowdParlay.Users.Infrastructure.Persistence/Services/UsersRepository.cs @@ -5,7 +5,6 @@ using CrowdParlay.Users.Domain.Entities; using CrowdParlay.Users.Infrastructure.Persistence.Abstractions; using Dapper; -using Dodo.Primitives; namespace CrowdParlay.Users.Infrastructure.Persistence.Services; @@ -52,7 +51,7 @@ public async IAsyncEnumerable GetByIdsAsync(Guid[] ids, [EnumeratorCancell yield return parser(reader); } - public async Task GetByIdAsync(Uuid id, CancellationToken cancellationToken) + public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken) { await using var connection = await _connectionFactory.CreateConnectionAsync(cancellationToken); return await connection.QuerySingleOrDefaultAsync( @@ -141,7 +140,7 @@ await connection.ExecuteAsync( entity); } - public async Task DeleteAsync(Uuid id, CancellationToken cancellationToken) + public async Task DeleteAsync(Guid id, CancellationToken cancellationToken) { await using var connection = await _connectionFactory.CreateConnectionAsync(cancellationToken); diff --git a/src/CrowdParlay.Users.Infrastructure.Persistence/SqlTypeHandlers/DodoUuidTypeHandler.cs b/src/CrowdParlay.Users.Infrastructure.Persistence/SqlTypeHandlers/DodoUuidTypeHandler.cs deleted file mode 100644 index f74edf5..0000000 --- a/src/CrowdParlay.Users.Infrastructure.Persistence/SqlTypeHandlers/DodoUuidTypeHandler.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Data; -using Dapper; -using Dodo.Primitives; - -namespace CrowdParlay.Users.Infrastructure.Persistence.SqlTypeHandlers; - -public class DodoUuidTypeHandler : SqlMapper.TypeHandler -{ - public override void SetValue(IDbDataParameter parameter, Uuid value) => - parameter.Value = value.ToGuidStringLayout(); - - public override Uuid Parse(object value) => Uuid.Parse(value.ToString()!); -} \ No newline at end of file