From 0ea8c969bd338396ca2d4fdce0fc91453b831f6b Mon Sep 17 00:00:00 2001 From: Sander van Vliet Date: Sun, 10 Dec 2023 14:23:52 +0100 Subject: [PATCH] Add route hash --- .config/dotnet-tools.json | 12 ++ .../EntityFramework/RoadCaptainDataContext.cs | 6 + .../Adapters/EntityFramework/Route.cs | 1 + .../Adapters/SqliteRouteStore.cs | 16 +-- src/RoadCaptain.App.Web/HashUtilities.cs | 20 ++++ src/RoadCaptain.App.Web/MainModule.cs | 21 +++- .../20231210130249_InitialSchema.Designer.cs | 103 +++++++++++++++++ .../20231210130249_InitialSchema.cs | 71 ++++++++++++ .../20231210132103_AddHashToRoute.Designer.cs | 109 ++++++++++++++++++ .../20231210132103_AddHashToRoute.cs | 29 +++++ .../RoadCaptainDataContextModelSnapshot.cs | 106 +++++++++++++++++ src/RoadCaptain.App.Web/Models/RouteModel.cs | 1 + .../RoadCaptain.App.Web.csproj | 4 + 13 files changed, 486 insertions(+), 13 deletions(-) create mode 100644 .config/dotnet-tools.json create mode 100644 src/RoadCaptain.App.Web/HashUtilities.cs create mode 100644 src/RoadCaptain.App.Web/Migrations/20231210130249_InitialSchema.Designer.cs create mode 100644 src/RoadCaptain.App.Web/Migrations/20231210130249_InitialSchema.cs create mode 100644 src/RoadCaptain.App.Web/Migrations/20231210132103_AddHashToRoute.Designer.cs create mode 100644 src/RoadCaptain.App.Web/Migrations/20231210132103_AddHashToRoute.cs create mode 100644 src/RoadCaptain.App.Web/Migrations/RoadCaptainDataContextModelSnapshot.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 00000000..6b93cca8 --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "7.0.3", + "commands": [ + "dotnet-ef" + ] + } + } +} \ No newline at end of file diff --git a/src/RoadCaptain.App.Web/Adapters/EntityFramework/RoadCaptainDataContext.cs b/src/RoadCaptain.App.Web/Adapters/EntityFramework/RoadCaptainDataContext.cs index cd117cf5..bb7d5e31 100644 --- a/src/RoadCaptain.App.Web/Adapters/EntityFramework/RoadCaptainDataContext.cs +++ b/src/RoadCaptain.App.Web/Adapters/EntityFramework/RoadCaptainDataContext.cs @@ -61,6 +61,12 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) .Property(r => r.Serialized) .IsRequired(); + modelBuilder + .Entity() + .Property(r => r.Hash) + .HasDefaultValue("(not yet calculated)") + .IsRequired(); + modelBuilder .Entity() .Property(r => r.UserId) diff --git a/src/RoadCaptain.App.Web/Adapters/EntityFramework/Route.cs b/src/RoadCaptain.App.Web/Adapters/EntityFramework/Route.cs index 2a01057e..80f4d80f 100644 --- a/src/RoadCaptain.App.Web/Adapters/EntityFramework/Route.cs +++ b/src/RoadCaptain.App.Web/Adapters/EntityFramework/Route.cs @@ -17,5 +17,6 @@ public class Route public bool IsLoop { get; set; } public string? Serialized { get; set; } public string? World { get; set; } + public string Hash { get; set; } = "(not yet calculated)"; } } diff --git a/src/RoadCaptain.App.Web/Adapters/SqliteRouteStore.cs b/src/RoadCaptain.App.Web/Adapters/SqliteRouteStore.cs index 9dfea541..679b0b3a 100644 --- a/src/RoadCaptain.App.Web/Adapters/SqliteRouteStore.cs +++ b/src/RoadCaptain.App.Web/Adapters/SqliteRouteStore.cs @@ -2,7 +2,6 @@ // Licensed under Artistic License 2.0 // See LICENSE or https://choosealicense.com/licenses/artistic-2.0/ -using System.Security.Cryptography; using Microsoft.EntityFrameworkCore; using RoadCaptain.App.Web.Adapters.EntityFramework; using RoadCaptain.App.Web.Models; @@ -173,19 +172,11 @@ public Dictionary FindDuplicates() }) .ToList(); - string HashIt(string serialized) - { - var serializedBytes = System.Text.Encoding.UTF8.GetBytes(serialized); - var hashBytes = SHA256.HashData(serializedBytes); - - return Convert.ToHexString(hashBytes); - } - return allRoutes .Select(x => new { x.Id, - Hash = HashIt(x.Serialized) + Hash = HashUtilities.HashAsHexString(x.Serialized ?? "null") }) .GroupBy(x => x.Hash, x => x.Id, @@ -214,7 +205,7 @@ private static Models.RouteModel RouteModelFrom(Route route) }; } - private Route RouteStorageModelFrom(CreateRouteModel createModel, User user) + private static Route RouteStorageModelFrom(CreateRouteModel createModel, User user) { return new Route { @@ -225,7 +216,8 @@ private Route RouteStorageModelFrom(CreateRouteModel createModel, User user) Distance = createModel.Distance, IsLoop = createModel.IsLoop, ZwiftRouteName = createModel.ZwiftRouteName, - Serialized = createModel.Serialized + Serialized = createModel.Serialized, + Hash = HashUtilities.HashAsHexString(createModel.Serialized!) }; } } diff --git a/src/RoadCaptain.App.Web/HashUtilities.cs b/src/RoadCaptain.App.Web/HashUtilities.cs new file mode 100644 index 00000000..6fbd15d0 --- /dev/null +++ b/src/RoadCaptain.App.Web/HashUtilities.cs @@ -0,0 +1,20 @@ +using System.Security.Cryptography; + +namespace RoadCaptain.App.Web +{ + internal class HashUtilities + { + public static string HashAsHexString(string input) + { + if (string.IsNullOrEmpty(input)) + { + throw new ArgumentException("Input was empty", nameof(input)); + } + + var serializedBytes = System.Text.Encoding.UTF8.GetBytes(input); + var hashBytes = SHA256.HashData(serializedBytes); + + return Convert.ToHexString(hashBytes); + } + } +} \ No newline at end of file diff --git a/src/RoadCaptain.App.Web/MainModule.cs b/src/RoadCaptain.App.Web/MainModule.cs index 7481b1d3..75ddf410 100644 --- a/src/RoadCaptain.App.Web/MainModule.cs +++ b/src/RoadCaptain.App.Web/MainModule.cs @@ -3,6 +3,7 @@ // See LICENSE or https://choosealicense.com/licenses/artistic-2.0/ using Autofac; +using Microsoft.EntityFrameworkCore; using RoadCaptain.App.Web.Adapters; using RoadCaptain.App.Web.Adapters.EntityFramework; using RoadCaptain.App.Web.Ports; @@ -30,7 +31,25 @@ protected override void Load(ContainerBuilder builder) .RegisterType() .AsSelf() .InstancePerLifetimeScope() - .OnActivated(args => args.Instance.Database.EnsureCreated()); + .OnActivated(args => + { + // If the database already exists we need to add the initial migration + // to it and pretend it has already run (which it did but I forgot to + // add migrations...) + try + { + args.Instance.Database.ExecuteSqlRaw( + @"INSERT INTO __EFMigrationsHistory +SELECT '20231210130249_InitialSchema', '7.0.3' +WHERE NOT EXISTS(SELECT 1 FROM __EFMigrationsHistory WHERE MigrationId = '20231210130249_InitialSchema')"); + } + catch + { + //Nop + } + + args.Instance.Database.Migrate(); + }); } } } diff --git a/src/RoadCaptain.App.Web/Migrations/20231210130249_InitialSchema.Designer.cs b/src/RoadCaptain.App.Web/Migrations/20231210130249_InitialSchema.Designer.cs new file mode 100644 index 00000000..318406a0 --- /dev/null +++ b/src/RoadCaptain.App.Web/Migrations/20231210130249_InitialSchema.Designer.cs @@ -0,0 +1,103 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RoadCaptain.App.Web.Adapters.EntityFramework; + +#nullable disable + +namespace RoadCaptain.App.Web.Migrations +{ + [DbContext(typeof(RoadCaptainDataContext))] + [Migration("20231210130249_InitialSchema")] + partial class InitialSchema + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Ascent") + .HasColumnType("TEXT"); + + b.Property("Descent") + .HasColumnType("TEXT"); + + b.Property("Distance") + .HasColumnType("TEXT"); + + b.Property("IsLoop") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Serialized") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("World") + .HasColumnType("TEXT"); + + b.Property("ZwiftRouteName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ZwiftProfileId") + .HasColumnType("TEXT"); + + b.Property("ZwiftSubject") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.Route", b => + { + b.HasOne("RoadCaptain.App.Web.Adapters.EntityFramework.User", "User") + .WithMany("Routes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.User", b => + { + b.Navigation("Routes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/RoadCaptain.App.Web/Migrations/20231210130249_InitialSchema.cs b/src/RoadCaptain.App.Web/Migrations/20231210130249_InitialSchema.cs new file mode 100644 index 00000000..e8ebcfc4 --- /dev/null +++ b/src/RoadCaptain.App.Web/Migrations/20231210130249_InitialSchema.cs @@ -0,0 +1,71 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RoadCaptain.App.Web.Migrations +{ + /// + public partial class InitialSchema : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + Name = table.Column(type: "TEXT", nullable: false), + ZwiftSubject = table.Column(type: "TEXT", nullable: false), + ZwiftProfileId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Routes", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UserId = table.Column(type: "INTEGER", nullable: false), + Name = table.Column(type: "TEXT", nullable: true), + ZwiftRouteName = table.Column(type: "TEXT", nullable: true), + Distance = table.Column(type: "TEXT", nullable: false), + Ascent = table.Column(type: "TEXT", nullable: false), + Descent = table.Column(type: "TEXT", nullable: false), + IsLoop = table.Column(type: "INTEGER", nullable: false), + Serialized = table.Column(type: "TEXT", nullable: false), + World = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Routes", x => x.Id); + table.ForeignKey( + name: "FK_Routes_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Routes_UserId", + table: "Routes", + column: "UserId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "Routes"); + + migrationBuilder.DropTable( + name: "Users"); + } + } +} diff --git a/src/RoadCaptain.App.Web/Migrations/20231210132103_AddHashToRoute.Designer.cs b/src/RoadCaptain.App.Web/Migrations/20231210132103_AddHashToRoute.Designer.cs new file mode 100644 index 00000000..b9a1d2bc --- /dev/null +++ b/src/RoadCaptain.App.Web/Migrations/20231210132103_AddHashToRoute.Designer.cs @@ -0,0 +1,109 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RoadCaptain.App.Web.Adapters.EntityFramework; + +#nullable disable + +namespace RoadCaptain.App.Web.Migrations +{ + [DbContext(typeof(RoadCaptainDataContext))] + [Migration("20231210132103_AddHashToRoute")] + partial class AddHashToRoute + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Ascent") + .HasColumnType("TEXT"); + + b.Property("Descent") + .HasColumnType("TEXT"); + + b.Property("Distance") + .HasColumnType("TEXT"); + + b.Property("Hash") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("(not yet calculated)"); + + b.Property("IsLoop") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Serialized") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("World") + .HasColumnType("TEXT"); + + b.Property("ZwiftRouteName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ZwiftProfileId") + .HasColumnType("TEXT"); + + b.Property("ZwiftSubject") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.Route", b => + { + b.HasOne("RoadCaptain.App.Web.Adapters.EntityFramework.User", "User") + .WithMany("Routes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.User", b => + { + b.Navigation("Routes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/RoadCaptain.App.Web/Migrations/20231210132103_AddHashToRoute.cs b/src/RoadCaptain.App.Web/Migrations/20231210132103_AddHashToRoute.cs new file mode 100644 index 00000000..05740cb5 --- /dev/null +++ b/src/RoadCaptain.App.Web/Migrations/20231210132103_AddHashToRoute.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace RoadCaptain.App.Web.Migrations +{ + /// + public partial class AddHashToRoute : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Hash", + table: "Routes", + type: "TEXT", + nullable: false, + defaultValue: "(not yet calculated)"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Hash", + table: "Routes"); + } + } +} diff --git a/src/RoadCaptain.App.Web/Migrations/RoadCaptainDataContextModelSnapshot.cs b/src/RoadCaptain.App.Web/Migrations/RoadCaptainDataContextModelSnapshot.cs new file mode 100644 index 00000000..3fc51688 --- /dev/null +++ b/src/RoadCaptain.App.Web/Migrations/RoadCaptainDataContextModelSnapshot.cs @@ -0,0 +1,106 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using RoadCaptain.App.Web.Adapters.EntityFramework; + +#nullable disable + +namespace RoadCaptain.App.Web.Migrations +{ + [DbContext(typeof(RoadCaptainDataContext))] + partial class RoadCaptainDataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "7.0.3"); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.Route", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Ascent") + .HasColumnType("TEXT"); + + b.Property("Descent") + .HasColumnType("TEXT"); + + b.Property("Distance") + .HasColumnType("TEXT"); + + b.Property("Hash") + .IsRequired() + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValue("(not yet calculated)"); + + b.Property("IsLoop") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Serialized") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("World") + .HasColumnType("TEXT"); + + b.Property("ZwiftRouteName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Routes"); + }); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ZwiftProfileId") + .HasColumnType("TEXT"); + + b.Property("ZwiftSubject") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.Route", b => + { + b.HasOne("RoadCaptain.App.Web.Adapters.EntityFramework.User", "User") + .WithMany("Routes") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("RoadCaptain.App.Web.Adapters.EntityFramework.User", b => + { + b.Navigation("Routes"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/RoadCaptain.App.Web/Models/RouteModel.cs b/src/RoadCaptain.App.Web/Models/RouteModel.cs index 068c6d03..4f05b223 100644 --- a/src/RoadCaptain.App.Web/Models/RouteModel.cs +++ b/src/RoadCaptain.App.Web/Models/RouteModel.cs @@ -16,5 +16,6 @@ public class RouteModel public decimal Descent { get; set; } public bool IsLoop { get; set; } public string? Serialized { get; set; } + public string Hash { get; set; } = "(unknown)"; } } diff --git a/src/RoadCaptain.App.Web/RoadCaptain.App.Web.csproj b/src/RoadCaptain.App.Web/RoadCaptain.App.Web.csproj index be3d326d..5087b50d 100644 --- a/src/RoadCaptain.App.Web/RoadCaptain.App.Web.csproj +++ b/src/RoadCaptain.App.Web/RoadCaptain.App.Web.csproj @@ -11,6 +11,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive +