From c1b810d489e6392de5adcff4a80c2473528c2bb5 Mon Sep 17 00:00:00 2001 From: Chris Poulter Date: Fri, 13 Dec 2024 11:13:36 +0000 Subject: [PATCH 1/5] use sql server --- Halcyon.Api/Data/Users/User.cs | 5 +- Halcyon.Api/Data/Users/UserConfiguration.cs | 13 +-- .../ChangePassword/ChangePasswordEndpoint.cs | 2 +- .../Profile/GetProfile/GetProfileResponse.cs | 2 +- Halcyon.Api/Features/UpdateRequest.cs | 2 +- .../Features/Users/GetUser/GetUserResponse.cs | 2 +- .../Users/SearchUsers/SearchUsersEndpoint.cs | 12 ++- Halcyon.Api/Halcyon.Api.csproj | 2 +- ...1028151921_CreateInitialSchema.Designer.cs | 99 ------------------- .../20241028151921_CreateInitialSchema.cs | 62 ------------ ...1125113751_CreateInitialSchema.Designer.cs | 79 +++++++++++++++ .../20241125113751_CreateInitialSchema.cs | 64 ++++++++++++ ...20241127093907_AddUserFullText.Designer.cs | 79 +++++++++++++++ .../20241127093907_AddUserFullText.cs | 37 +++++++ .../HalcyonDbContextModelSnapshot.cs | 62 ++++-------- .../EntityFrameworkExtensions.cs | 2 +- Halcyon.Api/appsettings.json | 2 +- README.md | 8 +- docker-compose.yml | 25 ++--- 19 files changed, 314 insertions(+), 245 deletions(-) delete mode 100644 Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.Designer.cs delete mode 100644 Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.cs create mode 100644 Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.Designer.cs create mode 100644 Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.cs create mode 100644 Halcyon.Api/Migrations/20241127093907_AddUserFullText.Designer.cs create mode 100644 Halcyon.Api/Migrations/20241127093907_AddUserFullText.cs diff --git a/Halcyon.Api/Data/Users/User.cs b/Halcyon.Api/Data/Users/User.cs index f067dfb..8f24930 100644 --- a/Halcyon.Api/Data/Users/User.cs +++ b/Halcyon.Api/Data/Users/User.cs @@ -1,5 +1,4 @@ using Halcyon.Api.Services.Events; -using NpgsqlTypes; namespace Halcyon.Api.Data.Users; @@ -23,7 +22,5 @@ public class User : Entity public List Roles { get; set; } - public uint Version { get; } - - public NpgsqlTsVector SearchVector { get; } + public byte[] Version { get; set; } } diff --git a/Halcyon.Api/Data/Users/UserConfiguration.cs b/Halcyon.Api/Data/Users/UserConfiguration.cs index 492b857..83f3cbd 100644 --- a/Halcyon.Api/Data/Users/UserConfiguration.cs +++ b/Halcyon.Api/Data/Users/UserConfiguration.cs @@ -7,24 +7,13 @@ public class UserConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { + builder.Property(u => u.Id).HasDefaultValueSql("NEWSEQUENTIALID()").ValueGeneratedOnAdd(); builder.Property(u => u.EmailAddress).IsRequired(); builder.HasIndex(u => u.EmailAddress).IsUnique(); builder.Property(u => u.FirstName).IsRequired(); builder.Property(u => u.LastName).IsRequired(); builder.Property(u => u.DateOfBirth).IsRequired(); - builder.Property(u => u.Roles).HasColumnType("text[]"); builder.Property(u => u.IsLockedOut).HasDefaultValue(false); builder.Property(u => u.Version).IsRowVersion(); - - builder.HasGeneratedTsVectorColumn( - u => u.SearchVector, - "english", - u => new - { - u.FirstName, - u.LastName, - u.EmailAddress, - } - ); } } diff --git a/Halcyon.Api/Features/Profile/ChangePassword/ChangePasswordEndpoint.cs b/Halcyon.Api/Features/Profile/ChangePassword/ChangePasswordEndpoint.cs index 1da6430..24cc94e 100644 --- a/Halcyon.Api/Features/Profile/ChangePassword/ChangePasswordEndpoint.cs +++ b/Halcyon.Api/Features/Profile/ChangePassword/ChangePasswordEndpoint.cs @@ -42,7 +42,7 @@ private static async Task HandleAsync( ); } - if (request.Version is not null && request.Version != user.Version) + if (request?.Version is not null && request.Version != user.Version) { return Results.Problem( statusCode: StatusCodes.Status409Conflict, diff --git a/Halcyon.Api/Features/Profile/GetProfile/GetProfileResponse.cs b/Halcyon.Api/Features/Profile/GetProfile/GetProfileResponse.cs index bf0bfe0..31d8e3a 100644 --- a/Halcyon.Api/Features/Profile/GetProfile/GetProfileResponse.cs +++ b/Halcyon.Api/Features/Profile/GetProfile/GetProfileResponse.cs @@ -12,5 +12,5 @@ public class GetProfileResponse public DateOnly DateOfBirth { get; set; } - public uint Version { get; set; } + public byte[] Version { get; set; } } diff --git a/Halcyon.Api/Features/UpdateRequest.cs b/Halcyon.Api/Features/UpdateRequest.cs index 3665903..4d8d9b2 100644 --- a/Halcyon.Api/Features/UpdateRequest.cs +++ b/Halcyon.Api/Features/UpdateRequest.cs @@ -2,5 +2,5 @@ public class UpdateRequest { - public uint? Version { get; set; } + public byte[] Version { get; set; } } diff --git a/Halcyon.Api/Features/Users/GetUser/GetUserResponse.cs b/Halcyon.Api/Features/Users/GetUser/GetUserResponse.cs index 432cb31..2a79a45 100644 --- a/Halcyon.Api/Features/Users/GetUser/GetUserResponse.cs +++ b/Halcyon.Api/Features/Users/GetUser/GetUserResponse.cs @@ -16,5 +16,5 @@ public class GetUserResponse public List Roles { get; set; } - public uint Version { get; set; } + public byte[] Version { get; set; } } diff --git a/Halcyon.Api/Features/Users/SearchUsers/SearchUsersEndpoint.cs b/Halcyon.Api/Features/Users/SearchUsers/SearchUsersEndpoint.cs index 4bdcc02..948cb80 100644 --- a/Halcyon.Api/Features/Users/SearchUsers/SearchUsersEndpoint.cs +++ b/Halcyon.Api/Features/Users/SearchUsers/SearchUsersEndpoint.cs @@ -4,7 +4,6 @@ using Halcyon.Api.Services.Validation; using Mapster; using Microsoft.EntityFrameworkCore; - namespace Halcyon.Api.Features.Users.SearchUsers; public class SearchUsersEndpoint : IEndpoint @@ -29,9 +28,14 @@ private static async Task HandleAsync( if (!string.IsNullOrEmpty(request.Search)) { - query = query.Where(u => - u.SearchVector.Matches(EF.Functions.PhraseToTsQuery("english", request.Search)) - ); + query = query.Where(u => EF.Functions.FreeText(u.EmailAddress, request.Search)); + + //query = query.Where(u => + // EF.Functions.Like( + // u.FirstName + " " + u.LastName + " " + u.EmailAddress, + // $"%{request.Search}%" + // ) + //); } var count = await query.CountAsync(cancellationToken); diff --git a/Halcyon.Api/Halcyon.Api.csproj b/Halcyon.Api/Halcyon.Api.csproj index b219196..6eb61f3 100644 --- a/Halcyon.Api/Halcyon.Api.csproj +++ b/Halcyon.Api/Halcyon.Api.csproj @@ -24,11 +24,11 @@ + - diff --git a/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.Designer.cs b/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.Designer.cs deleted file mode 100644 index 1997d54..0000000 --- a/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Halcyon.Api.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using NpgsqlTypes; - -#nullable disable - -namespace Halcyon.Api.Migrations -{ - [DbContext(typeof(HalcyonDbContext))] - [Migration("20241028151921_CreateInitialSchema")] - partial class CreateInitialSchema - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Halcyon.Api.Data.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("DateOfBirth") - .HasColumnType("date") - .HasColumnName("date_of_birth"); - - b.Property("EmailAddress") - .IsRequired() - .HasColumnType("text") - .HasColumnName("email_address"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("text") - .HasColumnName("first_name"); - - b.Property("IsLockedOut") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_locked_out"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text") - .HasColumnName("last_name"); - - b.Property("Password") - .HasColumnType("text") - .HasColumnName("password"); - - b.Property("PasswordResetToken") - .HasColumnType("uuid") - .HasColumnName("password_reset_token"); - - b.Property>("Roles") - .HasColumnType("text[]") - .HasColumnName("roles"); - - b.Property("SearchVector") - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("tsvector") - .HasColumnName("search_vector") - .HasAnnotation("Npgsql:TsVectorConfig", "english") - .HasAnnotation("Npgsql:TsVectorProperties", new[] { "FirstName", "LastName", "EmailAddress" }); - - b.Property("Version") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("xid") - .HasColumnName("xmin"); - - b.HasKey("Id") - .HasName("pk_users"); - - b.HasIndex("EmailAddress") - .IsUnique() - .HasDatabaseName("ix_users_email_address"); - - b.ToTable("users", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.cs b/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.cs deleted file mode 100644 index d198c1e..0000000 --- a/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; -using NpgsqlTypes; - -#nullable disable - -namespace Halcyon.Api.Migrations -{ - /// - public partial class CreateInitialSchema : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "users", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - email_address = table.Column(type: "text", nullable: false), - password = table.Column(type: "text", nullable: true), - password_reset_token = table.Column(type: "uuid", nullable: true), - first_name = table.Column(type: "text", nullable: false), - last_name = table.Column(type: "text", nullable: false), - date_of_birth = table.Column(type: "date", nullable: false), - is_locked_out = table.Column( - type: "boolean", - nullable: false, - defaultValue: false - ), - roles = table.Column>(type: "text[]", nullable: true), - xmin = table.Column(type: "xid", rowVersion: true, nullable: false), - search_vector = table - .Column(type: "tsvector", nullable: true) - .Annotation("Npgsql:TsVectorConfig", "english") - .Annotation( - "Npgsql:TsVectorProperties", - new[] { "first_name", "last_name", "email_address" } - ), - }, - constraints: table => - { - table.PrimaryKey("pk_users", x => x.id); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_users_email_address", - table: "users", - column: "email_address", - unique: true - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "users"); - } - } -} diff --git a/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.Designer.cs b/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.Designer.cs new file mode 100644 index 0000000..41df793 --- /dev/null +++ b/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.Designer.cs @@ -0,0 +1,79 @@ +// +using System; +using Halcyon.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Halcyon.Api.Migrations +{ + [DbContext(typeof(HalcyonDbContext))] + [Migration("20241125113751_CreateInitialSchema")] + partial class CreateInitialSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Halcyon.Api.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsLockedOut") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordResetToken") + .HasColumnType("uniqueidentifier"); + + b.PrimitiveCollection("Roles") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion"); + + b.HasKey("Id"); + + b.HasIndex("EmailAddress") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.cs b/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.cs new file mode 100644 index 0000000..4c23c09 --- /dev/null +++ b/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.cs @@ -0,0 +1,64 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Halcyon.Api.Migrations +{ + /// + public partial class CreateInitialSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column( + type: "uniqueidentifier", + nullable: false, + defaultValueSql: "NEWSEQUENTIALID()" + ), + EmailAddress = table.Column(type: "nvarchar(450)", nullable: false), + Password = table.Column(type: "nvarchar(max)", nullable: true), + PasswordResetToken = table.Column( + type: "uniqueidentifier", + nullable: true + ), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false), + DateOfBirth = table.Column(type: "date", nullable: false), + IsLockedOut = table.Column( + type: "bit", + nullable: false, + defaultValue: false + ), + Roles = table.Column(type: "nvarchar(max)", nullable: true), + Version = table.Column( + type: "rowversion", + rowVersion: true, + nullable: true + ), + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + } + ); + + migrationBuilder.CreateIndex( + name: "IX_Users_EmailAddress", + table: "Users", + column: "EmailAddress", + unique: true + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable(name: "Users"); + } + } +} diff --git a/Halcyon.Api/Migrations/20241127093907_AddUserFullText.Designer.cs b/Halcyon.Api/Migrations/20241127093907_AddUserFullText.Designer.cs new file mode 100644 index 0000000..fee4816 --- /dev/null +++ b/Halcyon.Api/Migrations/20241127093907_AddUserFullText.Designer.cs @@ -0,0 +1,79 @@ +// +using System; +using Halcyon.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Halcyon.Api.Migrations +{ + [DbContext(typeof(HalcyonDbContext))] + [Migration("20241127093907_AddUserFullText")] + partial class AddUserFullText + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Halcyon.Api.Data.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsLockedOut") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordResetToken") + .HasColumnType("uniqueidentifier"); + + b.PrimitiveCollection("Roles") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion"); + + b.HasKey("Id"); + + b.HasIndex("EmailAddress") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Halcyon.Api/Migrations/20241127093907_AddUserFullText.cs b/Halcyon.Api/Migrations/20241127093907_AddUserFullText.cs new file mode 100644 index 0000000..5800f1d --- /dev/null +++ b/Halcyon.Api/Migrations/20241127093907_AddUserFullText.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Halcyon.Api.Migrations +{ + public partial class AddUserFullText : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + @"CREATE FULLTEXT CATALOG MyFullTextCatalog AS DEFAULT;", + suppressTransaction: true + ); + + migrationBuilder.Sql( + @"CREATE FULLTEXT INDEX ON Users + ( + FirstName LANGUAGE 1033, + LastName LANGUAGE 1033, + EmailAddress LANGUAGE 1033 + ) + KEY INDEX PK_Users + ON MyFullTextCatalog;", + suppressTransaction: true + ); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DROP FULLTEXT INDEX ON Users;", suppressTransaction: true); + + migrationBuilder.Sql( + "DROP FULLTEXT CATALOG MyFullTextCatalog;", + suppressTransaction: true + ); + } + } +} diff --git a/Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs b/Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs index 60796c1..b46ad1c 100644 --- a/Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs +++ b/Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs @@ -1,12 +1,10 @@ // using System; -using System.Collections.Generic; using Halcyon.Api.Data; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using NpgsqlTypes; #nullable disable @@ -19,76 +17,58 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("Halcyon.Api.Data.User", b => { b.Property("Id") .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWSEQUENTIALID()"); b.Property("DateOfBirth") - .HasColumnType("date") - .HasColumnName("date_of_birth"); + .HasColumnType("date"); b.Property("EmailAddress") .IsRequired() - .HasColumnType("text") - .HasColumnName("email_address"); + .HasColumnType("nvarchar(450)"); b.Property("FirstName") .IsRequired() - .HasColumnType("text") - .HasColumnName("first_name"); + .HasColumnType("nvarchar(max)"); b.Property("IsLockedOut") .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_locked_out"); + .HasColumnType("bit") + .HasDefaultValue(false); b.Property("LastName") .IsRequired() - .HasColumnType("text") - .HasColumnName("last_name"); + .HasColumnType("nvarchar(max)"); b.Property("Password") - .HasColumnType("text") - .HasColumnName("password"); + .HasColumnType("nvarchar(max)"); b.Property("PasswordResetToken") - .HasColumnType("uuid") - .HasColumnName("password_reset_token"); + .HasColumnType("uniqueidentifier"); - b.Property>("Roles") - .HasColumnType("text[]") - .HasColumnName("roles"); + b.PrimitiveCollection("Roles") + .HasColumnType("nvarchar(max)"); - b.Property("SearchVector") - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("tsvector") - .HasColumnName("search_vector") - .HasAnnotation("Npgsql:TsVectorConfig", "english") - .HasAnnotation("Npgsql:TsVectorProperties", new[] { "FirstName", "LastName", "EmailAddress" }); - - b.Property("Version") + b.Property("Version") .IsConcurrencyToken() .ValueGeneratedOnAddOrUpdate() - .HasColumnType("xid") - .HasColumnName("xmin"); + .HasColumnType("rowversion"); - b.HasKey("Id") - .HasName("pk_users"); + b.HasKey("Id"); b.HasIndex("EmailAddress") - .IsUnique() - .HasDatabaseName("ix_users_email_address"); + .IsUnique(); - b.ToTable("users", (string)null); + b.ToTable("Users"); }); #pragma warning restore 612, 618 } diff --git a/Halcyon.Api/Services/Infrastructure/EntityFrameworkExtensions.cs b/Halcyon.Api/Services/Infrastructure/EntityFrameworkExtensions.cs index 9ef57f4..9124c9b 100644 --- a/Halcyon.Api/Services/Infrastructure/EntityFrameworkExtensions.cs +++ b/Halcyon.Api/Services/Infrastructure/EntityFrameworkExtensions.cs @@ -15,7 +15,7 @@ string connectionName builder.Services.AddDbContext( (provider, options) => options - .UseNpgsql( + .UseSqlServer( builder.Configuration.GetConnectionString(connectionName), builder => builder.EnableRetryOnFailure() ) diff --git a/Halcyon.Api/appsettings.json b/Halcyon.Api/appsettings.json index 2b00006..f3a75ff 100644 --- a/Halcyon.Api/appsettings.json +++ b/Halcyon.Api/appsettings.json @@ -1,6 +1,6 @@ { "ConnectionStrings": { - "Database": "Host=localhost;Port=5432;Database=halcyon;Username=postgres;Password=password", + "Database": "Server=localhost;Database=Halcyon;User Id=sa;Password=Pass@word;TrustServerCertificate=true;MultipleActiveResultSets=true;", "RabbitMq": "amqp://guest:guest@localhost:5672", "Redis": "localhost" }, diff --git a/README.md b/README.md index 3cf0b26..ce72147 100644 --- a/README.md +++ b/README.md @@ -34,8 +34,8 @@ A .NET Core REST API project template 👷 Built with a sense of peace and tranq ### Prerequisites -- PostgreSQL - [https://www.postgresql.org/](https://www.postgresql.org/) +- SQL Server + [https://www.microsoft.com/en-gb/sql-server/sql-server-2022](https://www.microsoft.com/en-gb/sql-server/sql-server-2022) - RabbitMQ [https://www.rabbitmq.com/](https://www.rabbitmq.com/) - Redis @@ -60,7 +60,7 @@ In the `Halcyon.Api` directory of the project, create a new `appsettings.Develop ``` { "ConnectionStrings": { - "Database": "Host=localhost;Port=5432;Database=halcyon;Username=postgres;Password=password", + "Database": "Server=localhost;Database=Halcyon;User Id=sa;Password=Pass@word;TrustServerCertificate=true;MultipleActiveResultSets=true;", "RabbitMq": "amqp://guest:guest@localhost:5672", "Redis": "localhost" }, @@ -73,7 +73,7 @@ In the `Halcyon.Api` directory of the project, create a new `appsettings.Develop "NoReplyAddress": "noreply@example.com", "CdnUrl": "http://localhost:3000" }, - "Jwt": { + "Jwt": { "SecurityKey": "super_secret_key_that_should_be_changed", "Issuer": "HalcyonApi", "Audience": "HalcyonClient", diff --git a/docker-compose.yml b/docker-compose.yml index 401c5c4..d8059d3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,7 +12,7 @@ services: ASPNETCORE_ENVIRONMENT: Development ASPNETCORE_HTTP_PORTS: 8080 ASPNETCORE_HTTPS_PORTS: 8081 - ConnectionStrings__Database: Host=host.docker.internal;Port=5432;Database=halcyon;Username=postgres;Password=password + ConnectionStrings__Database: Server=host.docker.internal;Database=Halcyon;User Id=sa;Password=Pass@word;TrustServerCertificate=true;MultipleActiveResultSets=true; ConnectionStrings__RabbitMq: amqp://guest:guest@host.docker.internal:5672 ConnectionStrings__Redis: host.docker.internal Email__SmtpServer: host.docker.internal @@ -25,25 +25,26 @@ services: - ${APPDATA}/Microsoft/UserSecrets:/home/app/.microsoft/usersecrets:ro - ${APPDATA}/ASP.NET/Https:/home/app/.aspnet/https:ro depends_on: - postgres: + mssql: condition: service_healthy rabbitmq: condition: service_healthy redis: condition: service_healthy - postgres: - image: postgres:17.0 - user: postgres + mssql: + image: ghcr.io/chrispoulter/mssql-2022-full-text:latest + # image: mcr.microsoft.com/mssql/server:2022-latest environment: - POSTGRES_PASSWORD: password + ACCEPT_EULA: Y + MSSQL_SA_PASSWORD: Pass@word ports: - - 5432:5432 + - 1433:1433 volumes: - - postgres:/var/lib/postgresql/data + - mssql:/var/opt/mssql restart: always healthcheck: - test: [ 'CMD', 'pg_isready' ] + test: /opt/mssql-tools18/bin/sqlcmd -U sa -P "$${MSSQL_SA_PASSWORD}" -C -Q "SELECT 1" -b -o /dev/null interval: 10s timeout: 5s retries: 3 @@ -104,8 +105,8 @@ services: restart: always volumes: - postgres: - name: halcyon-postgres + mssql: + name: halcyon-mssql rabbitmq: name: halcyon-rabbitmq redis: @@ -113,4 +114,4 @@ volumes: maildev: name: halcyon-maildev seq: - name: halcyon-seq + name: halcyon-seq \ No newline at end of file From b51d9ffbca4b412e16fde904376d024cdab11a9a Mon Sep 17 00:00:00 2001 From: Chris Poulter Date: Fri, 13 Dec 2024 12:52:25 +0000 Subject: [PATCH 2/5] code clean up --- .../Users/SearchUsers/SearchUsersEndpoint.cs | 1 + ...1125113751_CreateInitialSchema.Designer.cs | 79 ------------------- .../20241125113751_CreateInitialSchema.cs | 64 --------------- ...20241127093907_AddUserFullText.Designer.cs | 79 ------------------- .../20241127093907_AddUserFullText.cs | 37 --------- .../HalcyonDbContextModelSnapshot.cs | 76 ------------------ 6 files changed, 1 insertion(+), 335 deletions(-) delete mode 100644 Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.Designer.cs delete mode 100644 Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.cs delete mode 100644 Halcyon.Api/Migrations/20241127093907_AddUserFullText.Designer.cs delete mode 100644 Halcyon.Api/Migrations/20241127093907_AddUserFullText.cs delete mode 100644 Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs diff --git a/Halcyon.Api/Features/Users/SearchUsers/SearchUsersEndpoint.cs b/Halcyon.Api/Features/Users/SearchUsers/SearchUsersEndpoint.cs index 948cb80..c1db226 100644 --- a/Halcyon.Api/Features/Users/SearchUsers/SearchUsersEndpoint.cs +++ b/Halcyon.Api/Features/Users/SearchUsers/SearchUsersEndpoint.cs @@ -4,6 +4,7 @@ using Halcyon.Api.Services.Validation; using Mapster; using Microsoft.EntityFrameworkCore; + namespace Halcyon.Api.Features.Users.SearchUsers; public class SearchUsersEndpoint : IEndpoint diff --git a/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.Designer.cs b/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.Designer.cs deleted file mode 100644 index 41df793..0000000 --- a/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.Designer.cs +++ /dev/null @@ -1,79 +0,0 @@ -// -using System; -using Halcyon.Api.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Halcyon.Api.Migrations -{ - [DbContext(typeof(HalcyonDbContext))] - [Migration("20241125113751_CreateInitialSchema")] - partial class CreateInitialSchema - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Halcyon.Api.Data.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasDefaultValueSql("NEWSEQUENTIALID()"); - - b.Property("DateOfBirth") - .HasColumnType("date"); - - b.Property("EmailAddress") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("IsLockedOut") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false); - - b.Property("LastName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Password") - .HasColumnType("nvarchar(max)"); - - b.Property("PasswordResetToken") - .HasColumnType("uniqueidentifier"); - - b.PrimitiveCollection("Roles") - .HasColumnType("nvarchar(max)"); - - b.Property("Version") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("rowversion"); - - b.HasKey("Id"); - - b.HasIndex("EmailAddress") - .IsUnique(); - - b.ToTable("Users"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.cs b/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.cs deleted file mode 100644 index 4c23c09..0000000 --- a/Halcyon.Api/Migrations/20241125113751_CreateInitialSchema.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; - -#nullable disable - -namespace Halcyon.Api.Migrations -{ - /// - public partial class CreateInitialSchema : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "Users", - columns: table => new - { - Id = table.Column( - type: "uniqueidentifier", - nullable: false, - defaultValueSql: "NEWSEQUENTIALID()" - ), - EmailAddress = table.Column(type: "nvarchar(450)", nullable: false), - Password = table.Column(type: "nvarchar(max)", nullable: true), - PasswordResetToken = table.Column( - type: "uniqueidentifier", - nullable: true - ), - FirstName = table.Column(type: "nvarchar(max)", nullable: false), - LastName = table.Column(type: "nvarchar(max)", nullable: false), - DateOfBirth = table.Column(type: "date", nullable: false), - IsLockedOut = table.Column( - type: "bit", - nullable: false, - defaultValue: false - ), - Roles = table.Column(type: "nvarchar(max)", nullable: true), - Version = table.Column( - type: "rowversion", - rowVersion: true, - nullable: true - ), - }, - constraints: table => - { - table.PrimaryKey("PK_Users", x => x.Id); - } - ); - - migrationBuilder.CreateIndex( - name: "IX_Users_EmailAddress", - table: "Users", - column: "EmailAddress", - unique: true - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "Users"); - } - } -} diff --git a/Halcyon.Api/Migrations/20241127093907_AddUserFullText.Designer.cs b/Halcyon.Api/Migrations/20241127093907_AddUserFullText.Designer.cs deleted file mode 100644 index fee4816..0000000 --- a/Halcyon.Api/Migrations/20241127093907_AddUserFullText.Designer.cs +++ /dev/null @@ -1,79 +0,0 @@ -// -using System; -using Halcyon.Api.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Halcyon.Api.Migrations -{ - [DbContext(typeof(HalcyonDbContext))] - [Migration("20241127093907_AddUserFullText")] - partial class AddUserFullText - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Halcyon.Api.Data.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasDefaultValueSql("NEWSEQUENTIALID()"); - - b.Property("DateOfBirth") - .HasColumnType("date"); - - b.Property("EmailAddress") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("IsLockedOut") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false); - - b.Property("LastName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Password") - .HasColumnType("nvarchar(max)"); - - b.Property("PasswordResetToken") - .HasColumnType("uniqueidentifier"); - - b.PrimitiveCollection("Roles") - .HasColumnType("nvarchar(max)"); - - b.Property("Version") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("rowversion"); - - b.HasKey("Id"); - - b.HasIndex("EmailAddress") - .IsUnique(); - - b.ToTable("Users"); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/Halcyon.Api/Migrations/20241127093907_AddUserFullText.cs b/Halcyon.Api/Migrations/20241127093907_AddUserFullText.cs deleted file mode 100644 index 5800f1d..0000000 --- a/Halcyon.Api/Migrations/20241127093907_AddUserFullText.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.EntityFrameworkCore.Migrations; - -namespace Halcyon.Api.Migrations -{ - public partial class AddUserFullText : Migration - { - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql( - @"CREATE FULLTEXT CATALOG MyFullTextCatalog AS DEFAULT;", - suppressTransaction: true - ); - - migrationBuilder.Sql( - @"CREATE FULLTEXT INDEX ON Users - ( - FirstName LANGUAGE 1033, - LastName LANGUAGE 1033, - EmailAddress LANGUAGE 1033 - ) - KEY INDEX PK_Users - ON MyFullTextCatalog;", - suppressTransaction: true - ); - } - - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.Sql("DROP FULLTEXT INDEX ON Users;", suppressTransaction: true); - - migrationBuilder.Sql( - "DROP FULLTEXT CATALOG MyFullTextCatalog;", - suppressTransaction: true - ); - } - } -} diff --git a/Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs b/Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs deleted file mode 100644 index b46ad1c..0000000 --- a/Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs +++ /dev/null @@ -1,76 +0,0 @@ -// -using System; -using Halcyon.Api.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -#nullable disable - -namespace Halcyon.Api.Migrations -{ - [DbContext(typeof(HalcyonDbContext))] - partial class HalcyonDbContextModelSnapshot : ModelSnapshot - { - protected override void BuildModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "9.0.0") - .HasAnnotation("Relational:MaxIdentifierLength", 128); - - SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); - - modelBuilder.Entity("Halcyon.Api.Data.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uniqueidentifier") - .HasDefaultValueSql("NEWSEQUENTIALID()"); - - b.Property("DateOfBirth") - .HasColumnType("date"); - - b.Property("EmailAddress") - .IsRequired() - .HasColumnType("nvarchar(450)"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("IsLockedOut") - .ValueGeneratedOnAdd() - .HasColumnType("bit") - .HasDefaultValue(false); - - b.Property("LastName") - .IsRequired() - .HasColumnType("nvarchar(max)"); - - b.Property("Password") - .HasColumnType("nvarchar(max)"); - - b.Property("PasswordResetToken") - .HasColumnType("uniqueidentifier"); - - b.PrimitiveCollection("Roles") - .HasColumnType("nvarchar(max)"); - - b.Property("Version") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("rowversion"); - - b.HasKey("Id"); - - b.HasIndex("EmailAddress") - .IsUnique(); - - b.ToTable("Users"); - }); -#pragma warning restore 612, 618 - } - } -} From b370ac276254addd776eef3237804e59964461c8 Mon Sep 17 00:00:00 2001 From: Chris Poulter Date: Fri, 13 Dec 2024 13:50:34 +0000 Subject: [PATCH 3/5] remove ef core naming convention --- Halcyon.Api/Data/Users/UserConfiguration.cs | 1 - Halcyon.Api/Halcyon.Api.csproj | 1 - ...1213134644_CreateInitialSchema.Designer.cs | 78 +++++++++++++++++++ .../20241213134644_CreateInitialSchema.cs | 48 ++++++++++++ ...20241213134651_AddUserFullText.Designer.cs | 78 +++++++++++++++++++ .../20241213134651_AddUserFullText.cs | 37 +++++++++ .../HalcyonDbContextModelSnapshot.cs | 75 ++++++++++++++++++ .../EntityFrameworkExtensions.cs | 1 - 8 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 Halcyon.Api/Migrations/20241213134644_CreateInitialSchema.Designer.cs create mode 100644 Halcyon.Api/Migrations/20241213134644_CreateInitialSchema.cs create mode 100644 Halcyon.Api/Migrations/20241213134651_AddUserFullText.Designer.cs create mode 100644 Halcyon.Api/Migrations/20241213134651_AddUserFullText.cs create mode 100644 Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs diff --git a/Halcyon.Api/Data/Users/UserConfiguration.cs b/Halcyon.Api/Data/Users/UserConfiguration.cs index 83f3cbd..a952279 100644 --- a/Halcyon.Api/Data/Users/UserConfiguration.cs +++ b/Halcyon.Api/Data/Users/UserConfiguration.cs @@ -7,7 +7,6 @@ public class UserConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { - builder.Property(u => u.Id).HasDefaultValueSql("NEWSEQUENTIALID()").ValueGeneratedOnAdd(); builder.Property(u => u.EmailAddress).IsRequired(); builder.HasIndex(u => u.EmailAddress).IsUnique(); builder.Property(u => u.FirstName).IsRequired(); diff --git a/Halcyon.Api/Halcyon.Api.csproj b/Halcyon.Api/Halcyon.Api.csproj index 6eb61f3..3dc90f9 100644 --- a/Halcyon.Api/Halcyon.Api.csproj +++ b/Halcyon.Api/Halcyon.Api.csproj @@ -12,7 +12,6 @@ - diff --git a/Halcyon.Api/Migrations/20241213134644_CreateInitialSchema.Designer.cs b/Halcyon.Api/Migrations/20241213134644_CreateInitialSchema.Designer.cs new file mode 100644 index 0000000..21a1502 --- /dev/null +++ b/Halcyon.Api/Migrations/20241213134644_CreateInitialSchema.Designer.cs @@ -0,0 +1,78 @@ +// +using System; +using Halcyon.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Halcyon.Api.Migrations +{ + [DbContext(typeof(HalcyonDbContext))] + [Migration("20241213134644_CreateInitialSchema")] + partial class CreateInitialSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Halcyon.Api.Data.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsLockedOut") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordResetToken") + .HasColumnType("uniqueidentifier"); + + b.PrimitiveCollection("Roles") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion"); + + b.HasKey("Id"); + + b.HasIndex("EmailAddress") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Halcyon.Api/Migrations/20241213134644_CreateInitialSchema.cs b/Halcyon.Api/Migrations/20241213134644_CreateInitialSchema.cs new file mode 100644 index 0000000..b53968f --- /dev/null +++ b/Halcyon.Api/Migrations/20241213134644_CreateInitialSchema.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Halcyon.Api.Migrations +{ + /// + public partial class CreateInitialSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + EmailAddress = table.Column(type: "nvarchar(450)", nullable: false), + Password = table.Column(type: "nvarchar(max)", nullable: true), + PasswordResetToken = table.Column(type: "uniqueidentifier", nullable: true), + FirstName = table.Column(type: "nvarchar(max)", nullable: false), + LastName = table.Column(type: "nvarchar(max)", nullable: false), + DateOfBirth = table.Column(type: "date", nullable: false), + IsLockedOut = table.Column(type: "bit", nullable: false, defaultValue: false), + Roles = table.Column(type: "nvarchar(max)", nullable: true), + Version = table.Column(type: "rowversion", rowVersion: true, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_Users_EmailAddress", + table: "Users", + column: "EmailAddress", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/Halcyon.Api/Migrations/20241213134651_AddUserFullText.Designer.cs b/Halcyon.Api/Migrations/20241213134651_AddUserFullText.Designer.cs new file mode 100644 index 0000000..7209739 --- /dev/null +++ b/Halcyon.Api/Migrations/20241213134651_AddUserFullText.Designer.cs @@ -0,0 +1,78 @@ +// +using System; +using Halcyon.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Halcyon.Api.Migrations +{ + [DbContext(typeof(HalcyonDbContext))] + [Migration("20241213134651_AddUserFullText")] + partial class AddUserFullText + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Halcyon.Api.Data.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsLockedOut") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordResetToken") + .HasColumnType("uniqueidentifier"); + + b.PrimitiveCollection("Roles") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion"); + + b.HasKey("Id"); + + b.HasIndex("EmailAddress") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Halcyon.Api/Migrations/20241213134651_AddUserFullText.cs b/Halcyon.Api/Migrations/20241213134651_AddUserFullText.cs new file mode 100644 index 0000000..34c9015 --- /dev/null +++ b/Halcyon.Api/Migrations/20241213134651_AddUserFullText.cs @@ -0,0 +1,37 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Halcyon.Api.Migrations +{ + public partial class AddUserFullText : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + @"CREATE FULLTEXT CATALOG FullTextCatalog AS DEFAULT;", + suppressTransaction: true + ); + + migrationBuilder.Sql( + @"CREATE FULLTEXT INDEX ON Users + ( + FirstName LANGUAGE 1033, + LastName LANGUAGE 1033, + EmailAddress LANGUAGE 1033 + ) + KEY INDEX PK_Users + ON FullTextCatalog;", + suppressTransaction: true + ); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql("DROP FULLTEXT INDEX ON Users;", suppressTransaction: true); + + migrationBuilder.Sql( + "DROP FULLTEXT CATALOG FullTextCatalog;", + suppressTransaction: true + ); + } + } +} diff --git a/Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs b/Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs new file mode 100644 index 0000000..efe0da8 --- /dev/null +++ b/Halcyon.Api/Migrations/HalcyonDbContextModelSnapshot.cs @@ -0,0 +1,75 @@ +// +using System; +using Halcyon.Api.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Halcyon.Api.Migrations +{ + [DbContext(typeof(HalcyonDbContext))] + partial class HalcyonDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("Halcyon.Api.Data.Users.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("DateOfBirth") + .HasColumnType("date"); + + b.Property("EmailAddress") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("FirstName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("IsLockedOut") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(false); + + b.Property("LastName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Password") + .HasColumnType("nvarchar(max)"); + + b.Property("PasswordResetToken") + .HasColumnType("uniqueidentifier"); + + b.PrimitiveCollection("Roles") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("rowversion"); + + b.HasKey("Id"); + + b.HasIndex("EmailAddress") + .IsUnique(); + + b.ToTable("Users"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/Halcyon.Api/Services/Infrastructure/EntityFrameworkExtensions.cs b/Halcyon.Api/Services/Infrastructure/EntityFrameworkExtensions.cs index 9124c9b..2771bdc 100644 --- a/Halcyon.Api/Services/Infrastructure/EntityFrameworkExtensions.cs +++ b/Halcyon.Api/Services/Infrastructure/EntityFrameworkExtensions.cs @@ -19,7 +19,6 @@ string connectionName builder.Configuration.GetConnectionString(connectionName), builder => builder.EnableRetryOnFailure() ) - .UseSnakeCaseNamingConvention() .AddInterceptors(provider.GetServices()) ); From b9ed4e12223da0b41315db8112e0c02687e14b33 Mon Sep 17 00:00:00 2001 From: Chris Poulter Date: Mon, 16 Dec 2024 10:40:16 +0000 Subject: [PATCH 4/5] merge clean up --- ...1028151921_CreateInitialSchema.Designer.cs | 99 ------------------- .../20241028151921_CreateInitialSchema.cs | 62 ------------ 2 files changed, 161 deletions(-) delete mode 100644 src/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.Designer.cs delete mode 100644 src/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.cs diff --git a/src/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.Designer.cs b/src/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.Designer.cs deleted file mode 100644 index 1997d54..0000000 --- a/src/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.Designer.cs +++ /dev/null @@ -1,99 +0,0 @@ -// -using System; -using System.Collections.Generic; -using Halcyon.Api.Data; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; -using NpgsqlTypes; - -#nullable disable - -namespace Halcyon.Api.Migrations -{ - [DbContext(typeof(HalcyonDbContext))] - [Migration("20241028151921_CreateInitialSchema")] - partial class CreateInitialSchema - { - /// - protected override void BuildTargetModel(ModelBuilder modelBuilder) - { -#pragma warning disable 612, 618 - modelBuilder - .HasAnnotation("ProductVersion", "8.0.10") - .HasAnnotation("Relational:MaxIdentifierLength", 63); - - NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); - - modelBuilder.Entity("Halcyon.Api.Data.User", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid") - .HasColumnName("id"); - - b.Property("DateOfBirth") - .HasColumnType("date") - .HasColumnName("date_of_birth"); - - b.Property("EmailAddress") - .IsRequired() - .HasColumnType("text") - .HasColumnName("email_address"); - - b.Property("FirstName") - .IsRequired() - .HasColumnType("text") - .HasColumnName("first_name"); - - b.Property("IsLockedOut") - .ValueGeneratedOnAdd() - .HasColumnType("boolean") - .HasDefaultValue(false) - .HasColumnName("is_locked_out"); - - b.Property("LastName") - .IsRequired() - .HasColumnType("text") - .HasColumnName("last_name"); - - b.Property("Password") - .HasColumnType("text") - .HasColumnName("password"); - - b.Property("PasswordResetToken") - .HasColumnType("uuid") - .HasColumnName("password_reset_token"); - - b.Property>("Roles") - .HasColumnType("text[]") - .HasColumnName("roles"); - - b.Property("SearchVector") - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("tsvector") - .HasColumnName("search_vector") - .HasAnnotation("Npgsql:TsVectorConfig", "english") - .HasAnnotation("Npgsql:TsVectorProperties", new[] { "FirstName", "LastName", "EmailAddress" }); - - b.Property("Version") - .IsConcurrencyToken() - .ValueGeneratedOnAddOrUpdate() - .HasColumnType("xid") - .HasColumnName("xmin"); - - b.HasKey("Id") - .HasName("pk_users"); - - b.HasIndex("EmailAddress") - .IsUnique() - .HasDatabaseName("ix_users_email_address"); - - b.ToTable("users", (string)null); - }); -#pragma warning restore 612, 618 - } - } -} diff --git a/src/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.cs b/src/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.cs deleted file mode 100644 index d198c1e..0000000 --- a/src/Halcyon.Api/Migrations/20241028151921_CreateInitialSchema.cs +++ /dev/null @@ -1,62 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.EntityFrameworkCore.Migrations; -using NpgsqlTypes; - -#nullable disable - -namespace Halcyon.Api.Migrations -{ - /// - public partial class CreateInitialSchema : Migration - { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "users", - columns: table => new - { - id = table.Column(type: "uuid", nullable: false), - email_address = table.Column(type: "text", nullable: false), - password = table.Column(type: "text", nullable: true), - password_reset_token = table.Column(type: "uuid", nullable: true), - first_name = table.Column(type: "text", nullable: false), - last_name = table.Column(type: "text", nullable: false), - date_of_birth = table.Column(type: "date", nullable: false), - is_locked_out = table.Column( - type: "boolean", - nullable: false, - defaultValue: false - ), - roles = table.Column>(type: "text[]", nullable: true), - xmin = table.Column(type: "xid", rowVersion: true, nullable: false), - search_vector = table - .Column(type: "tsvector", nullable: true) - .Annotation("Npgsql:TsVectorConfig", "english") - .Annotation( - "Npgsql:TsVectorProperties", - new[] { "first_name", "last_name", "email_address" } - ), - }, - constraints: table => - { - table.PrimaryKey("pk_users", x => x.id); - } - ); - - migrationBuilder.CreateIndex( - name: "ix_users_email_address", - table: "users", - column: "email_address", - unique: true - ); - } - - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable(name: "users"); - } - } -} From 9de01f04442b6d5cff9e466ac069b71db985e250 Mon Sep 17 00:00:00 2001 From: Chris Poulter Date: Mon, 16 Dec 2024 10:58:13 +0000 Subject: [PATCH 5/5] bugfix concurrency check --- .../Features/Profile/ChangePassword/ChangePasswordEndpoint.cs | 2 +- .../Features/Profile/DeleteProfile/DeleteProfileEndpoint.cs | 2 +- .../Features/Profile/UpdateProfile/UpdateProfileEndpoint.cs | 2 +- src/Halcyon.Api/Features/Users/DeleteUser/DeleteUserEndpoint.cs | 2 +- src/Halcyon.Api/Features/Users/LockUser/LockUserEndpoint.cs | 2 +- src/Halcyon.Api/Features/Users/UnlockUser/UnlockUserEndpoint.cs | 2 +- src/Halcyon.Api/Features/Users/UpdateUser/UpdateUserEndpoint.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Halcyon.Api/Features/Profile/ChangePassword/ChangePasswordEndpoint.cs b/src/Halcyon.Api/Features/Profile/ChangePassword/ChangePasswordEndpoint.cs index f68f434..e97d4cc 100644 --- a/src/Halcyon.Api/Features/Profile/ChangePassword/ChangePasswordEndpoint.cs +++ b/src/Halcyon.Api/Features/Profile/ChangePassword/ChangePasswordEndpoint.cs @@ -41,7 +41,7 @@ private static async Task HandleAsync( ); } - if (request?.Version is not null && request.Version != user.Version) + if (request.Version is not null && !request.Version.SequenceEqual(user.Version)) { return Results.Problem( statusCode: StatusCodes.Status409Conflict, diff --git a/src/Halcyon.Api/Features/Profile/DeleteProfile/DeleteProfileEndpoint.cs b/src/Halcyon.Api/Features/Profile/DeleteProfile/DeleteProfileEndpoint.cs index acffeac..b084b8e 100644 --- a/src/Halcyon.Api/Features/Profile/DeleteProfile/DeleteProfileEndpoint.cs +++ b/src/Halcyon.Api/Features/Profile/DeleteProfile/DeleteProfileEndpoint.cs @@ -38,7 +38,7 @@ private static async Task HandleAsync( ); } - if (request?.Version is not null && request.Version != user.Version) + if (request.Version is not null && !request.Version.SequenceEqual(user.Version)) { return Results.Problem( statusCode: StatusCodes.Status409Conflict, diff --git a/src/Halcyon.Api/Features/Profile/UpdateProfile/UpdateProfileEndpoint.cs b/src/Halcyon.Api/Features/Profile/UpdateProfile/UpdateProfileEndpoint.cs index 936a3c7..3839a25 100644 --- a/src/Halcyon.Api/Features/Profile/UpdateProfile/UpdateProfileEndpoint.cs +++ b/src/Halcyon.Api/Features/Profile/UpdateProfile/UpdateProfileEndpoint.cs @@ -41,7 +41,7 @@ private static async Task HandleAsync( ); } - if (request.Version is not null && request.Version != user.Version) + if (request.Version is not null && !request.Version.SequenceEqual(user.Version)) { return Results.Problem( statusCode: StatusCodes.Status409Conflict, diff --git a/src/Halcyon.Api/Features/Users/DeleteUser/DeleteUserEndpoint.cs b/src/Halcyon.Api/Features/Users/DeleteUser/DeleteUserEndpoint.cs index 2304566..2a46812 100644 --- a/src/Halcyon.Api/Features/Users/DeleteUser/DeleteUserEndpoint.cs +++ b/src/Halcyon.Api/Features/Users/DeleteUser/DeleteUserEndpoint.cs @@ -38,7 +38,7 @@ private static async Task HandleAsync( ); } - if (request?.Version is not null && request.Version != user.Version) + if (request.Version is not null && !request.Version.SequenceEqual(user.Version)) { return Results.Problem( statusCode: StatusCodes.Status409Conflict, diff --git a/src/Halcyon.Api/Features/Users/LockUser/LockUserEndpoint.cs b/src/Halcyon.Api/Features/Users/LockUser/LockUserEndpoint.cs index b99a62f..a06a2d2 100644 --- a/src/Halcyon.Api/Features/Users/LockUser/LockUserEndpoint.cs +++ b/src/Halcyon.Api/Features/Users/LockUser/LockUserEndpoint.cs @@ -38,7 +38,7 @@ private static async Task HandleAsync( ); } - if (request?.Version is not null && request.Version != user.Version) + if (request.Version is not null && !request.Version.SequenceEqual(user.Version)) { return Results.Problem( statusCode: StatusCodes.Status409Conflict, diff --git a/src/Halcyon.Api/Features/Users/UnlockUser/UnlockUserEndpoint.cs b/src/Halcyon.Api/Features/Users/UnlockUser/UnlockUserEndpoint.cs index 8007861..80977d2 100644 --- a/src/Halcyon.Api/Features/Users/UnlockUser/UnlockUserEndpoint.cs +++ b/src/Halcyon.Api/Features/Users/UnlockUser/UnlockUserEndpoint.cs @@ -35,7 +35,7 @@ private static async Task HandleAsync( ); } - if (request?.Version is not null && request.Version != user.Version) + if (request.Version is not null && !request.Version.SequenceEqual(user.Version)) { return Results.Problem( statusCode: StatusCodes.Status409Conflict, diff --git a/src/Halcyon.Api/Features/Users/UpdateUser/UpdateUserEndpoint.cs b/src/Halcyon.Api/Features/Users/UpdateUser/UpdateUserEndpoint.cs index 8b62e13..d65d07e 100644 --- a/src/Halcyon.Api/Features/Users/UpdateUser/UpdateUserEndpoint.cs +++ b/src/Halcyon.Api/Features/Users/UpdateUser/UpdateUserEndpoint.cs @@ -38,7 +38,7 @@ private static async Task HandleAsync( ); } - if (request.Version is not null && request.Version != user.Version) + if (request.Version is not null && !request.Version.SequenceEqual(user.Version)) { return Results.Problem( statusCode: StatusCodes.Status409Conflict,