diff --git a/src/BUTR.Site.NexusMods.Server.CrashReport.v13/BUTR.Site.NexusMods.Server.CrashReport.v13.csproj b/src/BUTR.Site.NexusMods.Server.CrashReport.v13/BUTR.Site.NexusMods.Server.CrashReport.v13.csproj
new file mode 100644
index 00000000..ac555a68
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.CrashReport.v13/BUTR.Site.NexusMods.Server.CrashReport.v13.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BUTR.Site.NexusMods.Server.CrashReport.v13/CrashReportV13.cs b/src/BUTR.Site.NexusMods.Server.CrashReport.v13/CrashReportV13.cs
new file mode 100644
index 00000000..ccca862f
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.CrashReport.v13/CrashReportV13.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text.Json;
+using BUTR.CrashReport.Models;
+using BUTR.Site.NexusMods.Server.Extensions;
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.Database;
+using BUTR.Site.NexusMods.Server.Repositories;
+
+namespace BUTR.Site.NexusMods.Server.CrashReport.v13;
+
+public static class CrashReportV13
+{
+ private static ExceptionTypeId FromException(ExceptionModel exception)
+ {
+ var exc = exception;
+ while (exc.InnerException is not null)
+ exc = exc.InnerException;
+
+ return ExceptionTypeId.From(exc.Type);
+ }
+
+ private static string GetException(ExceptionModel? exception, bool inner = false) => exception is null ? string.Empty : $"""
+
+{(inner ? "Inner " : string.Empty)}Exception information
+Type: {exception.Type}
+Message: {exception.Message}
+CallStack:
+{exception.CallStack}
+
+{GetException(exception.InnerException, true)}
+""";
+
+ public static bool TryFromJson(
+ IUnitOfWrite unitOfWrite,
+ TenantId tenant,
+ CrashReportFileId fileId,
+ CrashReportUrl url,
+ DateTime date,
+ byte version,
+ string content,
+ [NotNullWhen(true)] out CrashReportEntity? crashReportEntity,
+ [NotNullWhen(true)] out CrashReportToMetadataEntity? crashReportToMetadataEntity,
+ [NotNullWhen(true)] out IList? crashReportToModuleMetadataEntities)
+ {
+ if (version != 13)
+ {
+ crashReportEntity = null!;
+ crashReportToMetadataEntity = null!;
+ crashReportToModuleMetadataEntities = null!;
+ return false;
+ }
+
+ var report = JsonSerializer.Deserialize(content);
+ if (report is null)
+ {
+ crashReportEntity = null!;
+ crashReportToMetadataEntity = null!;
+ crashReportToModuleMetadataEntities = null!;
+ return false;
+ }
+
+ var butrLoaderVersion = report.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "BUTRLoaderVersion")?.Value;
+ var blseVersion = report.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "BLSEVersion")?.Value;
+ var launcherExVersion = report.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "LauncherExVersion")?.Value;
+
+ var crashReportId = CrashReportId.From(report.Id);
+ crashReportEntity = new CrashReportEntity
+ {
+ TenantId = tenant,
+ CrashReportId = crashReportId,
+ Url = url,
+ Version = CrashReportVersion.From(report.Version),
+ GameVersion = GameVersion.From(report.Metadata.GameVersion),
+ ExceptionTypeId = FromException(report.Exception),
+ ExceptionType = unitOfWrite.UpsertEntityFactory.GetOrCreateExceptionType(FromException(report.Exception)),
+ Exception = GetException(report.Exception),
+ CreatedAt = fileId.Value.Length == 8 ? DateTimeOffset.UnixEpoch.ToUniversalTime() : date.ToUniversalTime(),
+ };
+ crashReportToMetadataEntity = new CrashReportToMetadataEntity
+ {
+ TenantId = tenant,
+ CrashReportId = crashReportId,
+ LauncherType = string.IsNullOrEmpty(report.Metadata.LauncherType) ? null : report.Metadata.LauncherType,
+ LauncherVersion = string.IsNullOrEmpty(report.Metadata.LauncherVersion) ? null : report.Metadata.LauncherVersion,
+ Runtime = string.IsNullOrEmpty(report.Metadata.Runtime) ? null : report.Metadata.Runtime,
+ BUTRLoaderVersion = butrLoaderVersion,
+ BLSEVersion = blseVersion,
+ LauncherExVersion = launcherExVersion,
+ OperatingSystemType = null,
+ OperatingSystemVersion = null,
+ };
+ crashReportToModuleMetadataEntities = report.Modules.DistinctBy(x => new { x.Id }).Select(x => new CrashReportToModuleMetadataEntity
+ {
+ TenantId = tenant,
+ CrashReportId = crashReportId,
+ ModuleId = ModuleId.From(x.Id),
+ Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(ModuleId.From(x.Id)),
+ Version = ModuleVersion.From(x.Version),
+ NexusModsModId = NexusModsModId.TryParseUrl(x.Url, out var modId1) ? modId1 : null,
+ NexusModsMod = NexusModsModId.TryParseUrl(x.Url, out var modId2) ? unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId2) : null,
+ InvolvedPosition = (byte) (report.InvolvedModules.IndexOf(y => y.ModuleOrLoaderPluginId == x.Id) + 1),
+ IsInvolved = report.InvolvedModules.Any(y => y.ModuleOrLoaderPluginId == x.Id),
+ }).ToArray();
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.CrashReport.v14/BUTR.Site.NexusMods.Server.CrashReport.v14.csproj b/src/BUTR.Site.NexusMods.Server.CrashReport.v14/BUTR.Site.NexusMods.Server.CrashReport.v14.csproj
new file mode 100644
index 00000000..7b38ecc2
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.CrashReport.v14/BUTR.Site.NexusMods.Server.CrashReport.v14.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net8.0
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BUTR.Site.NexusMods.Server.CrashReport.v14/CrashReportV14.cs b/src/BUTR.Site.NexusMods.Server.CrashReport.v14/CrashReportV14.cs
new file mode 100644
index 00000000..da48f9b5
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.CrashReport.v14/CrashReportV14.cs
@@ -0,0 +1,147 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Text.Json;
+using BUTR.CrashReport.Bannerlord.Parser;
+using BUTR.CrashReport.Models;
+using BUTR.Site.NexusMods.Server.Extensions;
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.Database;
+using BUTR.Site.NexusMods.Server.Repositories;
+
+namespace BUTR.Site.NexusMods.Server.CrashReport.v14;
+
+public static class CrashReportV14
+{
+ private static ExceptionTypeId FromException(ExceptionModel exception)
+ {
+ var exc = exception;
+ while (exc.InnerException is not null)
+ exc = exc.InnerException;
+
+ return ExceptionTypeId.From(exc.Type);
+ }
+
+ private static string GetException(ExceptionModel? exception, bool inner = false) => exception is null ? string.Empty : $"""
+
+{(inner ? "Inner " : string.Empty)}Exception information
+Type: {exception.Type}
+Message: {exception.Message}
+CallStack:
+{exception.CallStack}
+
+{GetException(exception.InnerException, true)}
+""";
+
+ public static bool TryFromHtml(
+ IUnitOfWrite unitOfWrite,
+ TenantId tenant,
+ CrashReportFileId fileId,
+ CrashReportUrl url,
+ DateTime date,
+ byte version,
+ string content,
+ [NotNullWhen(true)] out CrashReportEntity? crashReportEntity,
+ [NotNullWhen(true)] out CrashReportToMetadataEntity? crashReportToMetadataEntity,
+ [NotNullWhen(true)] out IList? crashReportToModuleMetadataEntities)
+ {
+ var report = CrashReportParser.ParseLegacyHtml(version, content);
+ if (report is null)
+ {
+ crashReportEntity = null!;
+ crashReportToMetadataEntity = null!;
+ crashReportToModuleMetadataEntities = null!;
+ return false;
+ }
+ return TryFromModel(unitOfWrite, tenant, fileId, url, date, report, out crashReportEntity, out crashReportToMetadataEntity, out crashReportToModuleMetadataEntities);
+ }
+
+ public static bool TryFromJson(
+ IUnitOfWrite unitOfWrite,
+ TenantId tenant,
+ CrashReportFileId fileId,
+ CrashReportUrl url,
+ DateTime date,
+ byte version,
+ string content,
+ [NotNullWhen(true)] out CrashReportEntity? crashReportEntity,
+ [NotNullWhen(true)] out CrashReportToMetadataEntity? crashReportToMetadataEntity,
+ [NotNullWhen(true)] out IList? crashReportToModuleMetadataEntities)
+ {
+ if (version != 14)
+ {
+ crashReportEntity = null!;
+ crashReportToMetadataEntity = null!;
+ crashReportToModuleMetadataEntities = null!;
+ return false;
+ }
+
+ var report = JsonSerializer.Deserialize(content);
+ if (report is null)
+ {
+ crashReportEntity = null!;
+ crashReportToMetadataEntity = null!;
+ crashReportToModuleMetadataEntities = null!;
+ return false;
+ }
+
+ return TryFromModel(unitOfWrite, tenant, fileId, url, date, report, out crashReportEntity, out crashReportToMetadataEntity, out crashReportToModuleMetadataEntities);
+ }
+ public static bool TryFromModel(
+ IUnitOfWrite unitOfWrite,
+ TenantId tenant,
+ CrashReportFileId fileId,
+ CrashReportUrl url,
+ DateTime date,
+ CrashReportModel report,
+ [NotNullWhen(true)] out CrashReportEntity? crashReportEntity,
+ [NotNullWhen(true)] out CrashReportToMetadataEntity? crashReportToMetadataEntity,
+ [NotNullWhen(true)] out IList? crashReportToModuleMetadataEntities)
+ {
+ var butrLoaderVersion = report.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "BUTRLoaderVersion")?.Value;
+ var blseVersion = report.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "BLSEVersion")?.Value;
+ var launcherExVersion = report.Metadata.AdditionalMetadata.FirstOrDefault(x => x.Key == "LauncherExVersion")?.Value;
+
+ var crashReportId = CrashReportId.From(report.Id);
+ crashReportEntity = new CrashReportEntity
+ {
+ TenantId = tenant,
+ CrashReportId = crashReportId,
+ Url = url,
+ Version = CrashReportVersion.From(report.Version),
+ GameVersion = GameVersion.From(report.Metadata.GameVersion),
+ ExceptionTypeId = FromException(report.Exception),
+ ExceptionType = unitOfWrite.UpsertEntityFactory.GetOrCreateExceptionType(FromException(report.Exception)),
+ Exception = GetException(report.Exception),
+ CreatedAt = fileId.Value.Length == 8 ? DateTimeOffset.UnixEpoch.ToUniversalTime() : date.ToUniversalTime(),
+ };
+ crashReportToMetadataEntity = new CrashReportToMetadataEntity
+ {
+ TenantId = tenant,
+ CrashReportId = crashReportId,
+ LauncherType = string.IsNullOrEmpty(report.Metadata.LauncherType) ? null : report.Metadata.LauncherType,
+ LauncherVersion = string.IsNullOrEmpty(report.Metadata.LauncherVersion) ? null : report.Metadata.LauncherVersion,
+ Runtime = string.IsNullOrEmpty(report.Metadata.Runtime) ? null : report.Metadata.Runtime,
+ BUTRLoaderVersion = butrLoaderVersion,
+ BLSEVersion = blseVersion,
+ LauncherExVersion = launcherExVersion,
+ OperatingSystemType = report.Metadata.OperatingSystemType.ToString(),
+ OperatingSystemVersion = report.Metadata.OperatingSystemVersion,
+ };
+ crashReportToModuleMetadataEntities = report.Modules.DistinctBy(x => new { x.Id }).Select(x => new CrashReportToModuleMetadataEntity
+ {
+ TenantId = tenant,
+ CrashReportId = crashReportId,
+ ModuleId = ModuleId.From(x.Id),
+ Module = unitOfWrite.UpsertEntityFactory.GetOrCreateModule(ModuleId.From(x.Id)),
+ Version = ModuleVersion.From(x.Version),
+ NexusModsModId = NexusModsModId.TryParseUrl(x.Url, out var modId1) ? modId1 : null,
+ NexusModsMod = NexusModsModId.TryParseUrl(x.Url, out var modId2) ? unitOfWrite.UpsertEntityFactory.GetOrCreateNexusModsMod(modId2) : null,
+ InvolvedPosition = (byte) (report.InvolvedModules.IndexOf(y => y.ModuleOrLoaderPluginId == x.Id) + 1),
+ IsInvolved = report.InvolvedModules.Any(y => y.ModuleOrLoaderPluginId == x.Id),
+ }).ToArray();
+
+ return true;
+ }
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/BUTR.Site.NexusMods.Server.Persistence.csproj b/src/BUTR.Site.NexusMods.Server.Persistence/BUTR.Site.NexusMods.Server.Persistence.csproj
new file mode 100644
index 00000000..66b35fac
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/BUTR.Site.NexusMods.Server.Persistence.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net8.0
+ enable
+ latest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Filtering.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/Filtering.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Filtering.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/Filtering.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/FilteringType.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/FilteringType.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/FilteringType.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/FilteringType.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/API/PaginatedQuery.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/PaginatedQuery.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/API/PaginatedQuery.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/PaginatedQuery.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/API/PagingMetadata.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/PagingMetadata.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/API/PagingMetadata.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/PagingMetadata.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Sorting.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/Sorting.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Sorting.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/Sorting.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/SortingType.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/SortingType.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/SortingType.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/API/SortingType.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/AutocompleteEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/AutocompleteEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/AutocompleteEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/AutocompleteEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportIgnoredFileEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportIgnoredFileEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportIgnoredFileEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportIgnoredFileEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/CrashReportStatus.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportStatus.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/CrashReportStatus.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportStatus.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToFileIdEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportToFileIdEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToFileIdEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportToFileIdEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToMetadataEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportToMetadataEntity.cs
similarity index 86%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToMetadataEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportToMetadataEntity.cs
index fddcbd0e..fa9c9d96 100644
--- a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToMetadataEntity.cs
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportToMetadataEntity.cs
@@ -18,6 +18,10 @@ public sealed record CrashReportToMetadataEntity : IEntityWithTenant
public required string? BLSEVersion { get; init; }
public required string? LauncherExVersion { get; init; }
+
+ public required string? OperatingSystemType { get; init; }
+ public required string? OperatingSystemVersion { get; init; }
+
public override int GetHashCode() => HashCode.Combine(TenantId, CrashReportId, LauncherType, LauncherVersion, Runtime, BUTRLoaderVersion, BLSEVersion, LauncherExVersion);
}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToModuleMetadataEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportToModuleMetadataEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/CrashReportToModuleMetadataEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/CrashReportToModuleMetadataEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/ExceptionTypeEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/ExceptionTypeEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/ExceptionTypeEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/ExceptionTypeEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/IEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/IEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/IEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IEntityWithTenant.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/IEntityWithTenant.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/IEntityWithTenant.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/IEntityWithTenant.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationDiscordTokensEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationDiscordTokensEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationDiscordTokensEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationDiscordTokensEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGOGToOwnedTenantEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationGOGToOwnedTenantEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGOGToOwnedTenantEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationGOGToOwnedTenantEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGOGTokensEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationGOGTokensEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGOGTokensEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationGOGTokensEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGitHubTokensEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationGitHubTokensEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationGitHubTokensEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationGitHubTokensEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationSteamToOwnedTenantEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationSteamToOwnedTenantEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationSteamToOwnedTenantEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationSteamToOwnedTenantEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationSteamTokensEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationSteamTokensEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/IntegrationSteamTokensEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/IntegrationSteamTokensEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Json/ApplicationVersionRangeModel.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/Json/ApplicationVersionRangeModel.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/Json/ApplicationVersionRangeModel.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/Json/ApplicationVersionRangeModel.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Json/DependentModuleModel.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/Json/DependentModuleModel.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/Json/DependentModuleModel.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/Json/DependentModuleModel.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Json/ModuleInfoModel.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/Json/ModuleInfoModel.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/Json/ModuleInfoModel.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/Json/ModuleInfoModel.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoModel.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/Json/SubModuleInfoModel.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoModel.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/Json/SubModuleInfoModel.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoTagModel.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/Json/SubModuleInfoTagModel.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/Json/SubModuleInfoTagModel.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/Json/SubModuleInfoTagModel.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/ModuleEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/ModuleEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/ModuleEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/ModuleEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsArticleEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsArticleEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsArticleEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsArticleEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToFileUpdateEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToFileUpdateEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToFileUpdateEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToFileUpdateEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToModuleEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToModuleEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToModuleInfoHistoryEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToModuleInfoHistoryEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryGameVersionEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToModuleInfoHistoryGameVersionEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleInfoHistoryGameVersionEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToModuleInfoHistoryGameVersionEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleLinkType.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToModuleLinkType.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToModuleLinkType.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToModuleLinkType.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToNameEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToNameEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsModToNameEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsModToNameEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToCrashReportEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToCrashReportEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToCrashReportEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToCrashReportEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationDiscordEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToIntegrationDiscordEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationDiscordEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToIntegrationDiscordEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGOGEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToIntegrationGOGEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGOGEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToIntegrationGOGEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGitHubEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToIntegrationGitHubEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationGitHubEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToIntegrationGitHubEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationSteamEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToIntegrationSteamEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToIntegrationSteamEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToIntegrationSteamEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToModuleEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToModuleEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToModuleEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToModuleEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToModuleLinkType.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToModuleLinkType.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToModuleLinkType.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToModuleLinkType.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNameEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToNameEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNameEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToNameEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNexusModsModEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToNexusModsModEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNexusModsModEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToNexusModsModEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNexusModsModLinkType.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToNexusModsModLinkType.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToNexusModsModLinkType.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToNexusModsModLinkType.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToRoleEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToRoleEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/NexusModsUserToRoleEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/NexusModsUserToRoleEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/Paging.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/Paging.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/Paging.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/Paging.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/QuartzExecutionLogDetailEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/QuartzExecutionLogDetailEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/QuartzExecutionLogDetailEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/QuartzExecutionLogDetailEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/QuartzExecutionLogEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/QuartzExecutionLogEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/QuartzExecutionLogEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/QuartzExecutionLogEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsCrashScoreInvolvedEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/StatisticsCrashScoreInvolvedEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsCrashScoreInvolvedEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/StatisticsCrashScoreInvolvedEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsTopExceptionsTypeEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/StatisticsTopExceptionsTypeEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/StatisticsTopExceptionsTypeEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/StatisticsTopExceptionsTypeEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server/Models/Database/TenantEntity.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Entities/TenantEntity.cs
similarity index 100%
rename from src/BUTR.Site.NexusMods.Server/Models/Database/TenantEntity.cs
rename to src/BUTR.Site.NexusMods.Server.Persistence/Entities/TenantEntity.cs
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Extensions/IAsyncEnumerableExtensions.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Extensions/IAsyncEnumerableExtensions.cs
new file mode 100644
index 00000000..ce2f8955
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Extensions/IAsyncEnumerableExtensions.cs
@@ -0,0 +1,33 @@
+using JetBrains.Annotations;
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Extensions;
+
+///
+/// Provides extension methods for objects.
+///
+public static class IAsyncEnumerableExtensions
+{
+ public static int IndexOf(this IEnumerable source, Predicate predicate)
+ {
+ ArgumentNullException.ThrowIfNull(source);
+ ArgumentNullException.ThrowIfNull(predicate);
+
+ var index = 0;
+ foreach (var item in source)
+ {
+ if (predicate(item))
+ return index;
+
+ index += 1;
+ }
+
+ return -1;
+ }
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/IRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/IRepository.cs
new file mode 100644
index 00000000..2f5fcac7
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/IRepository.cs
@@ -0,0 +1,79 @@
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.API;
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+///
+/// Marker interface for repositories.
+///
+public interface IRepository;
+
+public interface IRepositoryRead : IRepository where TEntity : class, IEntity
+{
+ Task FirstOrDefaultAsync(
+ Expression>? filter,
+ Func, IOrderedQueryable>? orderBy,
+ CancellationToken ct);
+ Task FirstOrDefaultAsync(
+ Expression>? filter,
+ Func, IOrderedQueryable>? orderBy,
+ Expression> projection,
+ CancellationToken ct);
+
+ Task LastOrDefaultAsync(
+ Expression>? filter,
+ Func, IOrderedQueryable>? orderBy,
+ CancellationToken ct);
+ Task LastOrDefaultAsync(
+ Expression>? filter,
+ Func, IOrderedQueryable>? orderBy,
+ Expression> projection,
+ CancellationToken ct);
+
+ Task> GetAllAsync(
+ Expression>? filter,
+ Func, IOrderedQueryable>? orderBy,
+ CancellationToken ct);
+ Task> GetAllAsync(
+ Expression>? filter,
+ Func, IOrderedQueryable>? orderBy,
+ Expression> projection,
+ CancellationToken ct);
+
+ Task> PaginatedAsync(
+ PaginatedQuery query,
+ uint maxPageSize = 20,
+ Sorting? defaultSorting = default,
+ CancellationToken ct = default);
+
+ Task> PaginatedAsync(
+ Expression> projection,
+ PaginatedQuery query,
+ uint maxPageSize = 20,
+ Sorting? defaultSorting = default,
+ CancellationToken ct = default) where TProjection : class;
+}
+
+public interface IRepositoryWrite : IRepositoryRead where TEntity : class, IEntity
+{
+ void Add(TEntity entity);
+ void AddRange(IEnumerable entities);
+
+ void Update(TEntity originalEntity, TEntity currentEntity);
+
+ void Upsert(TEntity entity);
+ void UpsertRange(IEnumerable entities);
+
+ void Remove(TEntity entity);
+ void RemoveRange(IEnumerable entities);
+
+ int Remove(Expression> filter);
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/IUnitOfRead.cs b/src/BUTR.Site.NexusMods.Server.Persistence/IUnitOfRead.cs
new file mode 100644
index 00000000..10aaff99
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/IUnitOfRead.cs
@@ -0,0 +1,48 @@
+using System;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IUnitOfRead : IDisposable, IAsyncDisposable
+{
+ IAutocompleteEntityRepositoryRead Autocompletes { get; }
+
+ IQuartzExecutionLogEntityRepositoryRead QuartzExecutionLogs { get; }
+
+ IExceptionTypeRepositoryRead ExceptionTypes { get; }
+
+ ICrashReportEntityRepositoryRead CrashReports { get; }
+ ICrashReportToMetadataEntityRepositoryRead CrashReportToMetadatas { get; }
+ ICrashReportToModuleMetadataEntityRepositoryRead CrashReportModuleInfos { get; }
+ ICrashReportToFileIdEntityRepositoryRead CrashReportToFileIds { get; }
+ ICrashReportIgnoredFileEntityRepositoryRead CrashReportIgnoredFileIds { get; }
+
+ IStatisticsTopExceptionsTypeEntityRepositoryRead StatisticsTopExceptionsTypes { get; }
+ IStatisticsCrashScoreInvolvedEntityRepositoryRead StatisticsCrashScoreInvolveds { get; }
+
+ INexusModsArticleEntityRepositoryRead NexusModsArticles { get; }
+
+ INexusModsModToModuleEntityRepositoryRead NexusModsModModules { get; }
+ INexusModsModToNameEntityRepositoryRead NexusModsModName { get; }
+
+ INexusModsModToModuleInfoHistoryEntityRepositoryRead NexusModsModToModuleInfoHistory { get; }
+ INexusModsModToFileUpdateEntityRepositoryRead NexusModsModToFileUpdates { get; }
+
+ INexusModsUserRepositoryRead NexusModsUsers { get; }
+ INexusModsUserToNameEntityRepositoryRead NexusModsUserToName { get; }
+ INexusModsUserToCrashReportEntityRepositoryRead NexusModsUserToCrashReports { get; }
+ INexusModsUserToNexusModsModEntityRepositoryRead NexusModsUserToNexusModsMods { get; }
+ INexusModsUserToModuleEntityRepositoryRead NexusModsUserToModules { get; }
+
+
+ INexusModsUserToIntegrationGitHubEntityRepositoryRead NexusModsUserToGitHub { get; }
+ INexusModsUserToIntegrationDiscordEntityRepositoryRead NexusModsUserToDiscord { get; }
+ INexusModsUserToIntegrationGOGEntityRepositoryRead NexusModsUserToGOG { get; }
+ INexusModsUserToIntegrationSteamEntityRepositoryRead NexusModsUserToSteam { get; }
+
+ IIntegrationGitHubTokensEntityRepositoryRead IntegrationGitHubTokens { get; }
+ IIntegrationDiscordTokensEntityRepositoryRead IntegrationDiscordTokens { get; }
+ IIntegrationGOGTokensEntityRepositoryRead IntegrationGOGTokens { get; }
+ IIntegrationGOGToOwnedTenantEntityRepositoryRead IntegrationGOGToOwnedTenants { get; }
+ IIntegrationSteamTokensEntityRepositoryRead IntegrationSteamTokens { get; }
+ IIntegrationSteamToOwnedTenantEntityRepositoryRead IntegrationSteamToOwnedTenants { get; }
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/IUnitOfWorkFactory.cs b/src/BUTR.Site.NexusMods.Server.Persistence/IUnitOfWorkFactory.cs
new file mode 100644
index 00000000..7cb6a421
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/IUnitOfWorkFactory.cs
@@ -0,0 +1,12 @@
+using BUTR.Site.NexusMods.Server.Models;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IUnitOfWorkFactory
+{
+ IUnitOfRead CreateUnitOfRead();
+ IUnitOfRead CreateUnitOfRead(TenantId tenant);
+
+ IUnitOfWrite CreateUnitOfWrite();
+ IUnitOfWrite CreateUnitOfWrite(TenantId tenant);
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/IUnitOfWrite.cs b/src/BUTR.Site.NexusMods.Server.Persistence/IUnitOfWrite.cs
new file mode 100644
index 00000000..0f67a7d9
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/IUnitOfWrite.cs
@@ -0,0 +1,52 @@
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IUnitOfWrite : IDisposable, IAsyncDisposable
+{
+ IUpsertEntityFactory UpsertEntityFactory { get; }
+
+ IAutocompleteEntityRepositoryWrite Autocompletes { get; }
+
+ IQuartzExecutionLogEntityRepositoryWrite QuartzExecutionLogs { get; }
+
+ IExceptionTypeRepositoryWrite ExceptionTypes { get; }
+
+ ICrashReportEntityRepositoryWrite CrashReports { get; }
+ ICrashReportToMetadataEntityRepositoryWrite CrashReportToMetadatas { get; }
+ ICrashReportToModuleMetadataEntityRepositoryWrite CrashReportModuleInfos { get; }
+ ICrashReportToFileIdEntityRepositoryWrite CrashReportToFileIds { get; }
+ ICrashReportIgnoredFileEntityRepositoryWrite CrashReportIgnoredFileIds { get; }
+
+ IStatisticsTopExceptionsTypeEntityRepositoryWrite StatisticsTopExceptionsTypes { get; }
+ IStatisticsCrashScoreInvolvedEntityRepositoryWrite StatisticsCrashScoreInvolveds { get; }
+
+ INexusModsArticleEntityRepositoryWrite NexusModsArticles { get; }
+
+ INexusModsModToModuleEntityRepositoryWrite NexusModsModModules { get; }
+ INexusModsModToNameEntityRepositoryWrite NexusModsModName { get; }
+ INexusModsModToModuleInfoHistoryEntityRepositoryWrite NexusModsModToModuleInfoHistory { get; }
+ INexusModsModToFileUpdateEntityRepositoryWrite NexusModsModToFileUpdates { get; }
+
+ INexusModsUserRepositoryWrite NexusModsUsers { get; }
+ INexusModsUserToNameEntityRepositoryWrite NexusModsUserToName { get; }
+ INexusModsUserToCrashReportEntityRepositoryWrite NexusModsUserToCrashReports { get; }
+ INexusModsUserToNexusModsModEntityRepositoryWrite NexusModsUserToNexusModsMods { get; }
+ INexusModsUserToModuleEntityRepositoryWrite NexusModsUserToModules { get; }
+
+ INexusModsUserToIntegrationGitHubEntityRepositoryWrite NexusModsUserToGitHub { get; }
+ INexusModsUserToIntegrationDiscordEntityRepositoryWrite NexusModsUserToDiscord { get; }
+ INexusModsUserToIntegrationGOGEntityRepositoryWrite NexusModsUserToGOG { get; }
+ INexusModsUserToIntegrationSteamEntityRepositoryWrite NexusModsUserToSteam { get; }
+
+ IIntegrationGitHubTokensEntityRepositoryWrite IntegrationGitHubTokens { get; }
+ IIntegrationDiscordTokensEntityRepositoryWrite IntegrationDiscordTokens { get; }
+ IIntegrationGOGTokensEntityRepositoryWrite IntegrationGOGTokens { get; }
+ IIntegrationGOGToOwnedTenantEntityRepositoryWrite IntegrationGOGToOwnedTenants { get; }
+ IIntegrationSteamTokensEntityRepositoryWrite IntegrationSteamTokens { get; }
+ IIntegrationSteamToOwnedTenantEntityRepositoryWrite IntegrationSteamToOwnedTenants { get; }
+
+ Task SaveChangesAsync(CancellationToken ct);
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/IUpsertEntityFactory.cs b/src/BUTR.Site.NexusMods.Server.Persistence/IUpsertEntityFactory.cs
new file mode 100644
index 00000000..a19e2c72
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/IUpsertEntityFactory.cs
@@ -0,0 +1,16 @@
+using System.Threading;
+using System.Threading.Tasks;
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IUpsertEntityFactory
+{
+ NexusModsUserEntity GetOrCreateNexusModsUser(NexusModsUserId nexusModsUserId);
+ NexusModsUserEntity GetOrCreateNexusModsUserWithName(NexusModsUserId nexusModsUserId, NexusModsUserName nexusModsUserName);
+ NexusModsModEntity GetOrCreateNexusModsMod(NexusModsModId nexusModsModId);
+ ModuleEntity GetOrCreateModule(ModuleId moduleId);
+ ExceptionTypeEntity GetOrCreateExceptionType(ExceptionTypeId exception);
+ Task SaveCreatedAsync(CancellationToken ct);
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/AutocompleteEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/AutocompleteEntityRepository.cs
new file mode 100644
index 00000000..3f6d46a9
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/AutocompleteEntityRepository.cs
@@ -0,0 +1,16 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System;
+using System.Collections.Generic;
+using System.Linq.Expressions;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IAutocompleteEntityRepositoryRead : IRepositoryRead
+{
+ Task> AutocompleteStartsWithAsync(Expression> property, TParameter value, CancellationToken ct)
+ where TEntity : class, IEntity;
+}
+public interface IAutocompleteEntityRepositoryWrite : IRepositoryWrite, IAutocompleteEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportEntityRepository.cs
new file mode 100644
index 00000000..45bdb6db
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportEntityRepository.cs
@@ -0,0 +1,43 @@
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.API;
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public sealed record ModuleIdToVersionModel
+{
+ public required ModuleId ModuleId { get; init; }
+ public required ModuleVersion Version { get; init; }
+}
+public sealed record UserCrashReportModel
+{
+ public required CrashReportId Id { get; init; }
+ public required CrashReportVersion Version { get; init; }
+ public required GameVersion GameVersion { get; init; }
+ public required ExceptionTypeId ExceptionType { get; init; }
+ public required string Exception { get; init; }
+ public required DateTimeOffset CreatedAt { get; init; }
+ //public required ModuleId[] ModuleIds { get; init; }
+ //public required ModuleIdToVersionModel[] ModuleIdToVersion { get; init; }
+ public required ModuleId? TopInvolvedModuleId { get; init; } // Used for FE search
+ public required ModuleId[] InvolvedModuleIds { get; init; }
+ //public required NexusModsModId[] NexusModsModIds { get; init; }
+ public required CrashReportUrl Url { get; init; }
+
+ public required CrashReportStatus Status { get; init; }
+ public required string? Comment { get; init; }
+}
+
+public interface ICrashReportEntityRepositoryRead : IRepositoryRead
+{
+ Task> GetCrashReportsPaginatedAsync(NexusModsUserEntity user, PaginatedQuery query, ApplicationRole applicationRole, CancellationToken ct);
+}
+
+public interface ICrashReportEntityRepositoryWrite : IRepositoryWrite, ICrashReportEntityRepositoryRead
+{
+ Task GenerateAutoCompleteForGameVersionsAsync(CancellationToken ct);
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportIgnoredFileEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportIgnoredFileEntityRepository.cs
new file mode 100644
index 00000000..fded4cae
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportIgnoredFileEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface ICrashReportIgnoredFileEntityRepositoryRead : IRepositoryRead;
+public interface ICrashReportIgnoredFileEntityRepositoryWrite : IRepositoryWrite, ICrashReportIgnoredFileEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportToFileIdEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportToFileIdEntityRepository.cs
new file mode 100644
index 00000000..de6dd698
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportToFileIdEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface ICrashReportToFileIdEntityRepositoryRead : IRepositoryRead;
+public interface ICrashReportToFileIdEntityRepositoryWrite : IRepositoryWrite, ICrashReportToFileIdEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportToMetadataEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportToMetadataEntityRepository.cs
new file mode 100644
index 00000000..e4d80e56
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportToMetadataEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface ICrashReportToMetadataEntityRepositoryRead : IRepositoryRead;
+public interface ICrashReportToMetadataEntityRepositoryWrite : IRepositoryWrite, ICrashReportToMetadataEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportToModuleMetadataEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportToModuleMetadataEntityRepository.cs
new file mode 100644
index 00000000..4afc8e61
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/CrashReportToModuleMetadataEntityRepository.cs
@@ -0,0 +1,30 @@
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public sealed record StatisticsCrashReport
+{
+ public required GameVersion GameVersion { get; init; }
+ public required ModuleId ModuleId { get; init; }
+ public required ModuleVersion ModuleVersion { get; init; }
+ public required int InvolvedCount { get; init; }
+ public required int NotInvolvedCount { get; init; }
+ public required int TotalCount { get; init; }
+ public required int Value { get; init; }
+ public required double CrashScore { get; init; }
+}
+
+public interface ICrashReportToModuleMetadataEntityRepositoryRead : IRepositoryRead
+{
+ Task> GetAllStatisticsAsync(CancellationToken ct);
+}
+
+public interface ICrashReportToModuleMetadataEntityRepositoryWrite : IRepositoryWrite, ICrashReportToModuleMetadataEntityRepositoryRead
+{
+ Task GenerateAutoCompleteForModuleIdsAsync(CancellationToken ct);
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/ExceptionTypeRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/ExceptionTypeRepository.cs
new file mode 100644
index 00000000..7217e547
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/ExceptionTypeRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IExceptionTypeRepositoryRead : IRepositoryRead;
+public interface IExceptionTypeRepositoryWrite : IRepositoryWrite, IExceptionTypeRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationDiscordTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationDiscordTokensEntityRepository.cs
new file mode 100644
index 00000000..5cbf8fd6
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationDiscordTokensEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IIntegrationDiscordTokensEntityRepositoryRead : IRepositoryRead;
+public interface IIntegrationDiscordTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationDiscordTokensEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationGOGToOwnedTenantEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationGOGToOwnedTenantEntityRepository.cs
new file mode 100644
index 00000000..b45cdaa2
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationGOGToOwnedTenantEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IIntegrationGOGToOwnedTenantEntityRepositoryRead : IRepositoryRead;
+public interface IIntegrationGOGToOwnedTenantEntityRepositoryWrite : IRepositoryWrite, IIntegrationGOGToOwnedTenantEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationGOGTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationGOGTokensEntityRepository.cs
new file mode 100644
index 00000000..9c463923
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationGOGTokensEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IIntegrationGOGTokensEntityRepositoryRead : IRepositoryRead;
+public interface IIntegrationGOGTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationGOGTokensEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationGitHubTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationGitHubTokensEntityRepository.cs
new file mode 100644
index 00000000..8cd70012
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationGitHubTokensEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IIntegrationGitHubTokensEntityRepositoryRead : IRepositoryRead;
+public interface IIntegrationGitHubTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationGitHubTokensEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationSteamToOwnedTenantEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationSteamToOwnedTenantEntityRepository.cs
new file mode 100644
index 00000000..63483550
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationSteamToOwnedTenantEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IIntegrationSteamToOwnedTenantEntityRepositoryRead : IRepositoryRead;
+public interface IIntegrationSteamToOwnedTenantEntityRepositoryWrite : IRepositoryWrite, IIntegrationSteamToOwnedTenantEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationSteamTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationSteamTokensEntityRepository.cs
new file mode 100644
index 00000000..26972811
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/IntegrationSteamTokensEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IIntegrationSteamTokensEntityRepositoryRead : IRepositoryRead;
+public interface IIntegrationSteamTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationSteamTokensEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsArticleEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsArticleEntityRepository.cs
new file mode 100644
index 00000000..753da7e7
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsArticleEntityRepository.cs
@@ -0,0 +1,17 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface INexusModsArticleEntityRepositoryRead : IRepositoryRead
+{
+ Task> GetAllModuleIdsAsync(string authorName, CancellationToken ct);
+}
+
+public interface INexusModsArticleEntityRepositoryWrite : IRepositoryWrite, INexusModsArticleEntityRepositoryRead
+{
+ Task GenerateAutoCompleteForAuthorNameAsync(CancellationToken ct);
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToFileUpdateEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToFileUpdateEntityRepository.cs
new file mode 100644
index 00000000..eda4b687
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToFileUpdateEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface INexusModsModToFileUpdateEntityRepositoryRead : IRepositoryRead;
+public interface INexusModsModToFileUpdateEntityRepositoryWrite : IRepositoryWrite, INexusModsModToFileUpdateEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToModuleEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToModuleEntityRepository.cs
new file mode 100644
index 00000000..60ad0e85
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToModuleEntityRepository.cs
@@ -0,0 +1,41 @@
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.API;
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public sealed record LinkedByStaffNexusModsModModel
+{
+ public NexusModsModId NexusModsModId { get; init; }
+ public DateTimeOffset LastCheckedDate { get; init; }
+}
+
+public sealed record LinkedByStaffModuleNexusModsModsModel
+{
+ public required ModuleId ModuleId { get; init; }
+ public required LinkedByStaffNexusModsModModel[] NexusModsMods { get; init; }
+}
+
+public sealed record LinkedByExposureModuleModel
+{
+ public required ModuleId ModuleId { get; init; }
+ public required DateTimeOffset LastCheckedDate { get; init; }
+}
+
+public sealed record LinkedByExposureNexusModsModModelsModel
+{
+ public required NexusModsModId NexusModsModId { get; init; }
+ public required LinkedByExposureModuleModel[] Modules { get; init; }
+}
+
+public interface INexusModsModToModuleEntityRepositoryRead : IRepositoryRead
+{
+ Task> GetByStaffPaginatedAsync(PaginatedQuery query, CancellationToken ct);
+
+ Task> GetExposedPaginatedAsync(PaginatedQuery query, CancellationToken ct);
+}
+public interface INexusModsModToModuleEntityRepositoryWrite : IRepositoryWrite, INexusModsModToModuleEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToModuleInfoHistoryEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToModuleInfoHistoryEntityRepository.cs
new file mode 100644
index 00000000..6cd67ef6
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToModuleInfoHistoryEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface INexusModsModToModuleInfoHistoryEntityRepositoryRead : IRepositoryRead;
+public interface INexusModsModToModuleInfoHistoryEntityRepositoryWrite : IRepositoryWrite, INexusModsModToModuleInfoHistoryEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToNameEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToNameEntityRepository.cs
new file mode 100644
index 00000000..d788fc70
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsModToNameEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface INexusModsModToNameEntityRepositoryRead : IRepositoryRead;
+public interface INexusModsModToNameEntityRepositoryWrite : IRepositoryWrite, INexusModsModToNameEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserRepository.cs
new file mode 100644
index 00000000..9ace00a0
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserRepository.cs
@@ -0,0 +1,38 @@
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.API;
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public sealed record UserAvailableModModel
+{
+ public required NexusModsModId NexusModsModId { get; init; }
+ public required string Name { get; init; }
+}
+
+public sealed record UserLinkedModModel
+{
+ public required NexusModsModId NexusModsModId { get; init; }
+ public required string Name { get; init; }
+ public required NexusModsUserId[] OwnerNexusModsUserIds { get; init; }
+ public required NexusModsUserId[] AllowedNexusModsUserIds { get; init; }
+ public required NexusModsUserId[] ManuallyLinkedNexusModsUserIds { get; init; }
+ public required ModuleId[] ManuallyLinkedModuleIds { get; init; }
+ public required ModuleId[] KnownModuleIds { get; init; }
+}
+
+public interface INexusModsUserRepositoryRead : IRepositoryRead
+{
+ Task GetUserWithIntegrationsAsync(NexusModsUserId userId, CancellationToken ct);
+ Task GetUserAsync(NexusModsUserId userId, CancellationToken ct);
+
+ Task> GetNexusModsModsPaginatedAsync(NexusModsUserId userId, PaginatedQuery query, CancellationToken ct);
+
+ Task> GetAvailableModsPaginatedAsync(NexusModsUserId userId, PaginatedQuery query, CancellationToken ct);
+
+ Task GetLinkedModCountAsync(NexusModsUserId userId, CancellationToken ct);
+}
+public interface INexusModsUserRepositoryWrite : IRepositoryWrite, INexusModsUserRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToCrashReportEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToCrashReportEntityRepository.cs
new file mode 100644
index 00000000..1cce3e94
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToCrashReportEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface INexusModsUserToCrashReportEntityRepositoryRead : IRepositoryRead;
+public interface INexusModsUserToCrashReportEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToCrashReportEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationDiscordEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationDiscordEntityRepository.cs
new file mode 100644
index 00000000..8ab304d1
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationDiscordEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface INexusModsUserToIntegrationDiscordEntityRepositoryRead : IRepositoryRead;
+public interface INexusModsUserToIntegrationDiscordEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToIntegrationDiscordEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationGOGEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationGOGEntityRepository.cs
new file mode 100644
index 00000000..f0cd2ea3
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationGOGEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface INexusModsUserToIntegrationGOGEntityRepositoryRead : IRepositoryRead;
+public interface INexusModsUserToIntegrationGOGEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToIntegrationGOGEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationGitHubEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationGitHubEntityRepository.cs
new file mode 100644
index 00000000..ab2185ec
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationGitHubEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface INexusModsUserToIntegrationGitHubEntityRepositoryRead : IRepositoryRead;
+public interface INexusModsUserToIntegrationGitHubEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToIntegrationGitHubEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationSteamEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationSteamEntityRepository.cs
new file mode 100644
index 00000000..c8242ccd
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToIntegrationSteamEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface INexusModsUserToIntegrationSteamEntityRepositoryRead : IRepositoryRead;
+public interface INexusModsUserToIntegrationSteamEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToIntegrationSteamEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToModuleEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToModuleEntityRepository.cs
new file mode 100644
index 00000000..e13abe55
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToModuleEntityRepository.cs
@@ -0,0 +1,21 @@
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.API;
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public sealed record UserManuallyLinkedModuleModel
+{
+ public required NexusModsUserId NexusModsUserId { get; init; }
+ public required NexusModsUserName NexusModsUsername { get; init; }
+ public required ModuleId[] ModuleIds { get; init; }
+}
+
+public interface INexusModsUserToModuleEntityRepositoryRead : IRepositoryRead
+{
+ Task> GetManuallyLinkedModuleIdsPaginatedAsync(PaginatedQuery query, NexusModsUserToModuleLinkType linkType, CancellationToken ct);
+}
+public interface INexusModsUserToModuleEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToModuleEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToNameEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToNameEntityRepository.cs
new file mode 100644
index 00000000..9800e74f
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToNameEntityRepository.cs
@@ -0,0 +1,6 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface INexusModsUserToNameEntityRepositoryRead : IRepositoryRead;
+public interface INexusModsUserToNameEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToNameEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToNexusModsModEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToNexusModsModEntityRepository.cs
new file mode 100644
index 00000000..7ee04b7e
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/NexusModsUserToNexusModsModEntityRepository.cs
@@ -0,0 +1,25 @@
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.API;
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public sealed record UserManuallyLinkedModUserModel
+{
+ public required NexusModsUserId NexusModsUserId { get; init; }
+ public required NexusModsUserName NexusModsUsername { get; init; }
+}
+public sealed record UserManuallyLinkedModModel
+{
+ public required NexusModsModId NexusModsModId { get; init; }
+ public required UserManuallyLinkedModUserModel[] NexusModsUsers { get; init; }
+}
+
+public interface INexusModsUserToNexusModsModEntityRepositoryRead : IRepositoryRead
+{
+ Task> GetManuallyLinkedPaginatedAsync(NexusModsUserId userId, PaginatedQuery query, CancellationToken ct);
+}
+public interface INexusModsUserToNexusModsModEntityRepositoryWrite : IRepositoryWrite, INexusModsUserToNexusModsModEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/QuartzExecutionLogEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/QuartzExecutionLogEntityRepository.cs
new file mode 100644
index 00000000..65f16c3a
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/QuartzExecutionLogEntityRepository.cs
@@ -0,0 +1,13 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IQuartzExecutionLogEntityRepositoryRead : IRepositoryRead;
+
+public interface IQuartzExecutionLogEntityRepositoryWrite : IRepositoryWrite, IQuartzExecutionLogEntityRepositoryRead
+{
+ Task MarkIncompleteAsync(CancellationToken ct);
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/StatisticsCrashScoreInvolvedEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/StatisticsCrashScoreInvolvedEntityRepository.cs
new file mode 100644
index 00000000..102d1dae
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/StatisticsCrashScoreInvolvedEntityRepository.cs
@@ -0,0 +1,56 @@
+using BUTR.Site.NexusMods.Server.Models;
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public sealed record VersionScoreModel
+{
+ public required ModuleVersion Version { get; init; }
+ public required double Score { get; init; }
+ public required double Value { get; init; }
+ public required int CountStable { get; init; }
+ public required int CountUnstable { get; init; }
+ public double Count => CountStable + CountUnstable;
+}
+public sealed record VersionStorageModel
+{
+ public required ModuleVersion Version { get; init; }
+ public required VersionScoreModel[] Scores { get; init; }
+ public double MeanScore => Scores.Length == 0 ? 0 : 1 - (Scores.Sum(x => x.Value) / (double) Scores.Sum(x => x.Count));
+};
+public sealed record ModuleStorageModel
+{
+ public required ModuleId ModuleId { get; init; }
+ public required VersionStorageModel[] Versions { get; init; }
+};
+public sealed record StatisticsInvolvedModuleScoresForGameVersionModel
+{
+ public required GameVersion GameVersion { get; init; }
+ public required ModuleStorageModel[] Modules { get; init; }
+}
+
+public sealed record RawScoreForModuleVersionModel
+{
+ public required ModuleVersion ModuleVersion { get; init; }
+ public required double RawScore { get; init; }
+ public required int TotalCount { get; init; }
+}
+
+public sealed record StatisticsRawScoresForModuleModel
+{
+ public required ModuleId ModuleId { get; init; }
+ public required RawScoreForModuleVersionModel[] RawScores { get; init; }
+}
+
+public interface IStatisticsCrashScoreInvolvedEntityRepositoryRead : IRepositoryRead
+{
+ Task> GetAllInvolvedModuleScoresForGameVersionAsync(GameVersion[]? gameVersions, ModuleId[]? moduleIds, ModuleVersion[]? moduleVersions, CancellationToken ct);
+ Task> GetAllRawScoresForAllModulesAsync(GameVersion gameVersion, ModuleId[] moduleIds, CancellationToken ct);
+
+}
+public interface IStatisticsCrashScoreInvolvedEntityRepositoryWrite : IRepositoryWrite, IStatisticsCrashScoreInvolvedEntityRepositoryRead;
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/StatisticsTopExceptionsTypeEntityRepository.cs b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/StatisticsTopExceptionsTypeEntityRepository.cs
new file mode 100644
index 00000000..0173b8db
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server.Persistence/Repositories/StatisticsTopExceptionsTypeEntityRepository.cs
@@ -0,0 +1,12 @@
+using BUTR.Site.NexusMods.Server.Models.Database;
+
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace BUTR.Site.NexusMods.Server.Repositories;
+
+public interface IStatisticsTopExceptionsTypeEntityRepositoryRead : IRepositoryRead;
+public interface IStatisticsTopExceptionsTypeEntityRepositoryWrite : IRepositoryWrite, IStatisticsTopExceptionsTypeEntityRepositoryRead
+{
+ Task CalculateAsync(CancellationToken ct);
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/BUTR.Site.NexusMods.Server.ValueObjects.Vogen.csproj b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/BUTR.Site.NexusMods.Server.ValueObjects.Vogen.csproj
index 0ae53fc4..b79b3a36 100644
--- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/BUTR.Site.NexusMods.Server.ValueObjects.Vogen.csproj
+++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/BUTR.Site.NexusMods.Server.ValueObjects.Vogen.csproj
@@ -9,10 +9,9 @@
-
-
+
diff --git a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ExceptionTypeId.cs b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ExceptionTypeId.cs
index bb06d653..2ee6cdfb 100644
--- a/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ExceptionTypeId.cs
+++ b/src/BUTR.Site.NexusMods.Server.ValueObjects.Vogen/Models/ExceptionTypeId.cs
@@ -1,5 +1,3 @@
-using BUTR.CrashReport.Models;
-
namespace BUTR.Site.NexusMods.Server.Models;
using TType = ExceptionTypeId;
@@ -8,14 +6,6 @@ namespace BUTR.Site.NexusMods.Server.Models;
[ValueObject(conversions: Conversions.EfCoreValueConverter | Conversions.SystemTextJson | Conversions.TypeConverter)]
public readonly partial struct ExceptionTypeId
{
- public static TType FromException(ExceptionModel exception)
- {
- var exc = exception;
- while (exc.InnerException is not null)
- exc = exc.InnerException;
-
- return From(exc.Type);
- }
public static bool TryParseFromException(TValueType exception, out TType value)
{
Span dest = stackalloc Range[32];
diff --git a/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.csproj b/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.csproj
index a8a831a3..a01c9b4a 100644
--- a/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.csproj
+++ b/src/BUTR.Site.NexusMods.Server/BUTR.Site.NexusMods.Server.csproj
@@ -17,21 +17,16 @@
-
-
-
+
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
@@ -70,6 +65,9 @@
+
+
+
diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToMetadataEntityConfiguration.cs b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToMetadataEntityConfiguration.cs
index 276b57e4..173ec6e4 100644
--- a/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToMetadataEntityConfiguration.cs
+++ b/src/BUTR.Site.NexusMods.Server/Contexts/Configs/CrashReportToMetadataEntityConfiguration.cs
@@ -19,6 +19,8 @@ protected override void ConfigureModel(EntityTypeBuilder x.BUTRLoaderVersion).HasColumnName("butrloader_version");
builder.Property(x => x.BLSEVersion).HasColumnName("blse_version");
builder.Property(x => x.LauncherExVersion).HasColumnName("launcherex_version");
+ builder.Property(x => x.OperatingSystemType).HasColumnName("operating_system_type");
+ builder.Property(x => x.OperatingSystemVersion).HasColumnName("operating_system_version");
builder.ToTable("crash_report_metadata", "crashreport").HasKey(x => new
{
x.TenantId,
diff --git a/src/BUTR.Site.NexusMods.Server/Contexts/UpsertEntityFactory.cs b/src/BUTR.Site.NexusMods.Server/Contexts/UpsertEntityFactory.cs
index dbb83dcc..43ede89b 100644
--- a/src/BUTR.Site.NexusMods.Server/Contexts/UpsertEntityFactory.cs
+++ b/src/BUTR.Site.NexusMods.Server/Contexts/UpsertEntityFactory.cs
@@ -7,6 +7,7 @@
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
+using BUTR.Site.NexusMods.Server.Repositories;
namespace BUTR.Site.NexusMods.Server.Contexts;
@@ -14,7 +15,7 @@ namespace BUTR.Site.NexusMods.Server.Contexts;
/// Upsert is a pain in the ass, especially the graph inclusion.
/// Instead, we manually track such entities and save them manually
///
-public sealed class UpsertEntityFactory
+public sealed class UpsertEntityFactory : IUpsertEntityFactory
{
private readonly ITenantContextAccessor _tenantContextAccessor;
private readonly AppDbContextWrite _dbContextWrite;
diff --git a/src/BUTR.Site.NexusMods.Server/Controllers/AuthenticationController.cs b/src/BUTR.Site.NexusMods.Server/Controllers/AuthenticationController.cs
index ef8cd7fe..be42986b 100644
--- a/src/BUTR.Site.NexusMods.Server/Controllers/AuthenticationController.cs
+++ b/src/BUTR.Site.NexusMods.Server/Controllers/AuthenticationController.cs
@@ -189,7 +189,7 @@ public AuthenticationController(
UserId = (uint) nexusModsUserId.Value,
Name = userInfo.Name.Value,
EMail = userInfo.Email.Value,
- ProfileUrl = userInfo.AvatarUrl,
+ ProfileUrl = userInfo.AvatarUrl ?? "",
IsSupporter = userInfo.MembershipRoles.Contains("supporter"),
IsPremium = userInfo.MembershipRoles.Contains("premium"),
APIKey = null,
diff --git a/src/BUTR.Site.NexusMods.Server/Extensions/IAsyncEnumerableExtensions.cs b/src/BUTR.Site.NexusMods.Server/Extensions/IAsyncEnumerableExtensions.cs
index f620cc27..8bf1b0d5 100644
--- a/src/BUTR.Site.NexusMods.Server/Extensions/IAsyncEnumerableExtensions.cs
+++ b/src/BUTR.Site.NexusMods.Server/Extensions/IAsyncEnumerableExtensions.cs
@@ -14,23 +14,6 @@ namespace BUTR.Site.NexusMods.Server.Extensions;
///
public static class IAsyncEnumerableExtensions
{
- public static int IndexOf(this IEnumerable source, Predicate predicate)
- {
- ArgumentNullException.ThrowIfNull(source);
- ArgumentNullException.ThrowIfNull(predicate);
-
- var index = 0;
- foreach (var item in source)
- {
- if (predicate(item))
- return index;
-
- index += 1;
- }
-
- return -1;
- }
-
///
/// Configures an async enumerator with a cancellation token and a flag indicating whether to continue on a captured context.
///
diff --git a/src/BUTR.Site.NexusMods.Server/Options/OtlpOptions.cs b/src/BUTR.Site.NexusMods.Server/Options/OtlpOptions.cs
new file mode 100644
index 00000000..debb23ee
--- /dev/null
+++ b/src/BUTR.Site.NexusMods.Server/Options/OtlpOptions.cs
@@ -0,0 +1,25 @@
+using Aragas.Extensions.Options.FluentValidation.Extensions;
+using FluentValidation;
+using OpenTelemetry.Exporter;
+
+namespace BUTR.Site.NexusMods.Server.Options;
+
+public sealed class OtlpOptionsValidator : AbstractValidator
+{
+ public OtlpOptionsValidator()
+ {
+ RuleFor(static x => x.LoggingEndpoint).IsUri().IsUrlTcpEndpointAvailable().When(static x => !string.IsNullOrEmpty(x.LoggingEndpoint));
+ RuleFor(static x => x.TracingEndpoint).IsUri().IsUrlTcpEndpointAvailable().When(static x => !string.IsNullOrEmpty(x.TracingEndpoint));
+ RuleFor(static x => x.MetricsEndpoint).IsUri().IsUrlTcpEndpointAvailable().When(static x => !string.IsNullOrEmpty(x.MetricsEndpoint));
+ }
+}
+
+public sealed record OtlpOptions
+{
+ public required string LoggingEndpoint { get; init; } = default!;
+ public required OtlpExportProtocol LoggingProtocol { get; init; } = default!;
+ public required string TracingEndpoint { get; init; } = default!;
+ public required OtlpExportProtocol TracingProtocol { get; init; } = default!;
+ public required string MetricsEndpoint { get; init; } = default!;
+ public required OtlpExportProtocol MetricsProtocol { get; init; } = default!;
+}
\ No newline at end of file
diff --git a/src/BUTR.Site.NexusMods.Server/Program.cs b/src/BUTR.Site.NexusMods.Server/Program.cs
index da932569..d928d745 100644
--- a/src/BUTR.Site.NexusMods.Server/Program.cs
+++ b/src/BUTR.Site.NexusMods.Server/Program.cs
@@ -1,5 +1,6 @@
using BUTR.Site.NexusMods.Server.Contexts;
using BUTR.Site.NexusMods.Server.Extensions;
+using BUTR.Site.NexusMods.Server.Options;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
@@ -16,8 +17,6 @@
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
-using Quartz;
-
using Serilog;
using Serilog.Events;
@@ -28,6 +27,8 @@ namespace BUTR.Site.NexusMods.Server;
public static class Program
{
+ private const string OltpSectionName = "Oltp";
+
public static async Task Main(string[] args)
{
Log.Logger = new LoggerConfiguration()
@@ -60,9 +61,14 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host
.CreateDefaultBuilder(args)
.ConfigureServices((ctx, services) =>
{
- if (ctx.Configuration.GetSection("Oltp") is { } oltpSection)
+ var openTelemetry = services.AddOpenTelemetry()
+ .WithMetrics()
+ .WithTracing()
+ .WithLogging();
+
+ if (ctx.Configuration.GetSection(OltpSectionName) is { } oltpSection)
{
- var openTelemetry = services.AddOpenTelemetry()
+ openTelemetry
.ConfigureResource(builder =>
{
builder.AddDetector(new ContainerResourceDetector());
@@ -75,9 +81,9 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host
builder.AddTelemetrySdk();
});
- if (oltpSection.GetValue("MetricsEndpoint") is { } metricsEndpoint)
+ if (oltpSection.GetValue(nameof(OtlpOptions.MetricsEndpoint)) is { } metricsEndpoint)
{
- var metricsProtocol = oltpSection.GetValue("MetricsProtocol");
+ var metricsProtocol = oltpSection.GetValue(nameof(OtlpOptions.MetricsProtocol));
openTelemetry.WithMetrics(builder => builder
.AddProcessInstrumentation()
.AddRuntimeInstrumentation(instrumentationOptions =>
@@ -93,9 +99,9 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host
}));
}
- if (oltpSection.GetValue("TracingEndpoint") is { } tracingEndpoint)
+ if (oltpSection.GetValue(nameof(OtlpOptions.TracingEndpoint)) is { } tracingEndpoint)
{
- var tracingProtocol = oltpSection.GetValue("TracingProtocol");
+ var tracingProtocol = oltpSection.GetValue(nameof(OtlpOptions.TracingProtocol));
openTelemetry.WithTracing(builder => builder
.AddEntityFrameworkCoreInstrumentation(instrumentationOptions =>
{
@@ -103,11 +109,11 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host
})
.AddNpgsql(instrumentationOptions =>
{
-
+
})
.AddGrpcClientInstrumentation(instrumentationOptions =>
{
-
+
})
.AddHttpClientInstrumentation(instrumentationOptions =>
{
@@ -141,12 +147,12 @@ public static IHostBuilder CreateHostBuilder(string[] args) => Host
}, writeToProviders: true)
.ConfigureLogging((ctx, builder) =>
{
- var oltpSection = ctx.Configuration.GetSection("Oltp");
+ var oltpSection = ctx.Configuration.GetSection(OltpSectionName);
if (oltpSection == null!) return;
- var loggingEndpoint = oltpSection.GetValue("LoggingEndpoint");
+ var loggingEndpoint = oltpSection.GetValue(nameof(OtlpOptions.LoggingEndpoint));
if (loggingEndpoint is null) return;
- var loggingProtocol = oltpSection.GetValue("LoggingProtocol");
+ var loggingProtocol = oltpSection.GetValue(nameof(OtlpOptions.LoggingProtocol));
builder.AddOpenTelemetry(o =>
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/AutocompleteEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/AutocompleteEntityRepository.cs
index 96ecae16..fd6e0020 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/AutocompleteEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/AutocompleteEntityRepository.cs
@@ -14,13 +14,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IAutocompleteEntityRepositoryRead : IRepositoryRead
-{
- Task> AutocompleteStartsWithAsync(Expression> property, TParameter value, CancellationToken ct)
- where TEntity : class, IEntity;
-}
-public interface IAutocompleteEntityRepositoryWrite : IRepositoryWrite, IAutocompleteEntityRepositoryRead;
-
[ScopedService]
internal class AutocompleteEntityRepository : Repository, IAutocompleteEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportEntityRepository.cs
index 8650d980..a29ff0bb 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportEntityRepository.cs
@@ -19,40 +19,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public sealed record ModuleIdToVersionModel
-{
- public required ModuleId ModuleId { get; init; }
- public required ModuleVersion Version { get; init; }
-}
-public sealed record UserCrashReportModel
-{
- public required CrashReportId Id { get; init; }
- public required CrashReportVersion Version { get; init; }
- public required GameVersion GameVersion { get; init; }
- public required ExceptionTypeId ExceptionType { get; init; }
- public required string Exception { get; init; }
- public required DateTimeOffset CreatedAt { get; init; }
- //public required ModuleId[] ModuleIds { get; init; }
- //public required ModuleIdToVersionModel[] ModuleIdToVersion { get; init; }
- public required ModuleId? TopInvolvedModuleId { get; init; } // Used for FE search
- public required ModuleId[] InvolvedModuleIds { get; init; }
- //public required NexusModsModId[] NexusModsModIds { get; init; }
- public required CrashReportUrl Url { get; init; }
-
- public required CrashReportStatus Status { get; init; }
- public required string? Comment { get; init; }
-}
-
-public interface ICrashReportEntityRepositoryRead : IRepositoryRead
-{
- Task> GetCrashReportsPaginatedAsync(NexusModsUserEntity user, PaginatedQuery query, ApplicationRole applicationRole, CancellationToken ct);
-}
-
-public interface ICrashReportEntityRepositoryWrite : IRepositoryWrite, ICrashReportEntityRepositoryRead
-{
- Task GenerateAutoCompleteForGameVersionsAsync(CancellationToken ct);
-}
-
[ScopedService]
internal class CrashReportEntityRepository : Repository, ICrashReportEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportIgnoredFileEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportIgnoredFileEntityRepository.cs
index 3dda1b5d..80218d02 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportIgnoredFileEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportIgnoredFileEntityRepository.cs
@@ -6,9 +6,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface ICrashReportIgnoredFileEntityRepositoryRead : IRepositoryRead;
-public interface ICrashReportIgnoredFileEntityRepositoryWrite : IRepositoryWrite, ICrashReportIgnoredFileEntityRepositoryRead;
-
[ScopedService]
internal class CrashReportIgnoredFileEntityRepository : Repository, ICrashReportIgnoredFileEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToFileIdEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToFileIdEntityRepository.cs
index a6e97589..2562ee87 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToFileIdEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToFileIdEntityRepository.cs
@@ -8,9 +8,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface ICrashReportToFileIdEntityRepositoryRead : IRepositoryRead;
-public interface ICrashReportToFileIdEntityRepositoryWrite : IRepositoryWrite, ICrashReportToFileIdEntityRepositoryRead;
-
[ScopedService]
internal class CrashReportToFileIdEntityRepository : Repository, ICrashReportToFileIdEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToMetadataEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToMetadataEntityRepository.cs
index 28a8a919..d34a2595 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToMetadataEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToMetadataEntityRepository.cs
@@ -8,9 +8,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface ICrashReportToMetadataEntityRepositoryRead : IRepositoryRead;
-public interface ICrashReportToMetadataEntityRepositoryWrite : IRepositoryWrite, ICrashReportToMetadataEntityRepositoryRead;
-
[ScopedService]
internal class CrashReportToMetadataEntityRepository : Repository, ICrashReportToMetadataEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToModuleMetadataEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToModuleMetadataEntityRepository.cs
index 5e797d4b..34acabd0 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToModuleMetadataEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/CrashReportToModuleMetadataEntityRepository.cs
@@ -15,28 +15,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public sealed record StatisticsCrashReport
-{
- public required GameVersion GameVersion { get; init; }
- public required ModuleId ModuleId { get; init; }
- public required ModuleVersion ModuleVersion { get; init; }
- public required int InvolvedCount { get; init; }
- public required int NotInvolvedCount { get; init; }
- public required int TotalCount { get; init; }
- public required int Value { get; init; }
- public required double CrashScore { get; init; }
-}
-
-public interface ICrashReportToModuleMetadataEntityRepositoryRead : IRepositoryRead
-{
- Task> GetAllStatisticsAsync(CancellationToken ct);
-}
-
-public interface ICrashReportToModuleMetadataEntityRepositoryWrite : IRepositoryWrite, ICrashReportToModuleMetadataEntityRepositoryRead
-{
- Task GenerateAutoCompleteForModuleIdsAsync(CancellationToken ct);
-}
-
[ScopedService]
internal class CrashReportToModuleMetadataEntityRepository : Repository, ICrashReportToModuleMetadataEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/ExceptionTypeRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/ExceptionTypeRepository.cs
index 3792207e..3bcdf20a 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/ExceptionTypeRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/ExceptionTypeRepository.cs
@@ -6,9 +6,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IExceptionTypeRepositoryRead : IRepositoryRead;
-public interface IExceptionTypeRepositoryWrite : IRepositoryWrite, IExceptionTypeRepositoryRead;
-
[ScopedService]
internal class ExceptionTypeRepository : Repository, IExceptionTypeRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IRepository.cs
index a050cd3b..e4d5c701 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/IRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/IRepository.cs
@@ -17,73 +17,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-///
-/// Marker interface for repositories.
-///
-public interface IRepository;
-
-public interface IRepositoryRead : IRepository where TEntity : class, IEntity
-{
- Task FirstOrDefaultAsync(
- Expression>? filter,
- Func, IOrderedQueryable>? orderBy,
- CancellationToken ct);
- Task FirstOrDefaultAsync(
- Expression>? filter,
- Func, IOrderedQueryable>? orderBy,
- Expression> projection,
- CancellationToken ct);
-
- Task LastOrDefaultAsync(
- Expression>? filter,
- Func, IOrderedQueryable>? orderBy,
- CancellationToken ct);
- Task LastOrDefaultAsync(
- Expression>? filter,
- Func, IOrderedQueryable>? orderBy,
- Expression> projection,
- CancellationToken ct);
-
- Task> GetAllAsync(
- Expression>? filter,
- Func, IOrderedQueryable>? orderBy,
- CancellationToken ct);
- Task> GetAllAsync(
- Expression>? filter,
- Func, IOrderedQueryable>? orderBy,
- Expression> projection,
- CancellationToken ct);
-
- Task> PaginatedAsync(
- PaginatedQuery query,
- uint maxPageSize = 20,
- Sorting? defaultSorting = default,
- CancellationToken ct = default);
-
- Task> PaginatedAsync(
- Expression> projection,
- PaginatedQuery query,
- uint maxPageSize = 20,
- Sorting? defaultSorting = default,
- CancellationToken ct = default) where TProjection : class;
-}
-
-public interface IRepositoryWrite : IRepositoryRead where TEntity : class, IEntity
-{
- void Add(TEntity entity);
- void AddRange(IEnumerable entities);
-
- void Update(TEntity originalEntity, TEntity currentEntity);
-
- void Upsert(TEntity entity);
- void UpsertRange(IEnumerable entities);
-
- void Remove(TEntity entity);
- void RemoveRange(IEnumerable entities);
-
- int Remove(Expression> filter);
-}
-
internal abstract class Repository : IRepositoryWrite where TEntity : class, IEntity
{
protected readonly BaseAppDbContext _dbContext;
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfRead.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfRead.cs
index 41fa8d35..b5a29fe4 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfRead.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfRead.cs
@@ -9,51 +9,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IUnitOfRead : IDisposable, IAsyncDisposable
-{
- IAutocompleteEntityRepositoryRead Autocompletes { get; }
-
- IQuartzExecutionLogEntityRepositoryRead QuartzExecutionLogs { get; }
-
- IExceptionTypeRepositoryRead ExceptionTypes { get; }
-
- ICrashReportEntityRepositoryRead CrashReports { get; }
- ICrashReportToMetadataEntityRepositoryRead CrashReportToMetadatas { get; }
- ICrashReportToModuleMetadataEntityRepositoryRead CrashReportModuleInfos { get; }
- ICrashReportToFileIdEntityRepositoryRead CrashReportToFileIds { get; }
- ICrashReportIgnoredFileEntityRepositoryRead CrashReportIgnoredFileIds { get; }
-
- IStatisticsTopExceptionsTypeEntityRepositoryRead StatisticsTopExceptionsTypes { get; }
- IStatisticsCrashScoreInvolvedEntityRepositoryRead StatisticsCrashScoreInvolveds { get; }
-
- INexusModsArticleEntityRepositoryRead NexusModsArticles { get; }
-
- INexusModsModToModuleEntityRepositoryRead NexusModsModModules { get; }
- INexusModsModToNameEntityRepositoryRead NexusModsModName { get; }
-
- INexusModsModToModuleInfoHistoryEntityRepositoryRead NexusModsModToModuleInfoHistory { get; }
- INexusModsModToFileUpdateEntityRepositoryRead NexusModsModToFileUpdates { get; }
-
- INexusModsUserRepositoryRead NexusModsUsers { get; }
- INexusModsUserToNameEntityRepositoryRead NexusModsUserToName { get; }
- INexusModsUserToCrashReportEntityRepositoryRead NexusModsUserToCrashReports { get; }
- INexusModsUserToNexusModsModEntityRepositoryRead NexusModsUserToNexusModsMods { get; }
- INexusModsUserToModuleEntityRepositoryRead NexusModsUserToModules { get; }
-
-
- INexusModsUserToIntegrationGitHubEntityRepositoryRead NexusModsUserToGitHub { get; }
- INexusModsUserToIntegrationDiscordEntityRepositoryRead NexusModsUserToDiscord { get; }
- INexusModsUserToIntegrationGOGEntityRepositoryRead NexusModsUserToGOG { get; }
- INexusModsUserToIntegrationSteamEntityRepositoryRead NexusModsUserToSteam { get; }
-
- IIntegrationGitHubTokensEntityRepositoryRead IntegrationGitHubTokens { get; }
- IIntegrationDiscordTokensEntityRepositoryRead IntegrationDiscordTokens { get; }
- IIntegrationGOGTokensEntityRepositoryRead IntegrationGOGTokens { get; }
- IIntegrationGOGToOwnedTenantEntityRepositoryRead IntegrationGOGToOwnedTenants { get; }
- IIntegrationSteamTokensEntityRepositoryRead IntegrationSteamTokens { get; }
- IIntegrationSteamToOwnedTenantEntityRepositoryRead IntegrationSteamToOwnedTenants { get; }
-}
-
[TransientService]
internal class UnitOfRead : IUnitOfRead
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWorkFactory.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWorkFactory.cs
index a24e691a..cb30e57e 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWorkFactory.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWorkFactory.cs
@@ -8,15 +8,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IUnitOfWorkFactory
-{
- IUnitOfRead CreateUnitOfRead();
- IUnitOfRead CreateUnitOfRead(TenantId tenant);
-
- IUnitOfWrite CreateUnitOfWrite();
- IUnitOfWrite CreateUnitOfWrite(TenantId tenant);
-}
-
[ScopedService]
internal class UnitOfWorkFactory : IUnitOfWorkFactory
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWrite.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWrite.cs
index 3a42d3b6..73bc5422 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWrite.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/IUnitOfWrite.cs
@@ -11,53 +11,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IUnitOfWrite : IDisposable, IAsyncDisposable
-{
- UpsertEntityFactory UpsertEntityFactory { get; }
-
- IAutocompleteEntityRepositoryWrite Autocompletes { get; }
-
- IQuartzExecutionLogEntityRepositoryWrite QuartzExecutionLogs { get; }
-
- IExceptionTypeRepositoryWrite ExceptionTypes { get; }
-
- ICrashReportEntityRepositoryWrite CrashReports { get; }
- ICrashReportToMetadataEntityRepositoryWrite CrashReportToMetadatas { get; }
- ICrashReportToModuleMetadataEntityRepositoryWrite CrashReportModuleInfos { get; }
- ICrashReportToFileIdEntityRepositoryWrite CrashReportToFileIds { get; }
- ICrashReportIgnoredFileEntityRepositoryWrite CrashReportIgnoredFileIds { get; }
-
- IStatisticsTopExceptionsTypeEntityRepositoryWrite StatisticsTopExceptionsTypes { get; }
- IStatisticsCrashScoreInvolvedEntityRepositoryWrite StatisticsCrashScoreInvolveds { get; }
-
- INexusModsArticleEntityRepositoryWrite NexusModsArticles { get; }
-
- INexusModsModToModuleEntityRepositoryWrite NexusModsModModules { get; }
- INexusModsModToNameEntityRepositoryWrite NexusModsModName { get; }
- INexusModsModToModuleInfoHistoryEntityRepositoryWrite NexusModsModToModuleInfoHistory { get; }
- INexusModsModToFileUpdateEntityRepositoryWrite NexusModsModToFileUpdates { get; }
-
- INexusModsUserRepositoryWrite NexusModsUsers { get; }
- INexusModsUserToNameEntityRepositoryWrite NexusModsUserToName { get; }
- INexusModsUserToCrashReportEntityRepositoryWrite NexusModsUserToCrashReports { get; }
- INexusModsUserToNexusModsModEntityRepositoryWrite NexusModsUserToNexusModsMods { get; }
- INexusModsUserToModuleEntityRepositoryWrite NexusModsUserToModules { get; }
-
- INexusModsUserToIntegrationGitHubEntityRepositoryWrite NexusModsUserToGitHub { get; }
- INexusModsUserToIntegrationDiscordEntityRepositoryWrite NexusModsUserToDiscord { get; }
- INexusModsUserToIntegrationGOGEntityRepositoryWrite NexusModsUserToGOG { get; }
- INexusModsUserToIntegrationSteamEntityRepositoryWrite NexusModsUserToSteam { get; }
-
- IIntegrationGitHubTokensEntityRepositoryWrite IntegrationGitHubTokens { get; }
- IIntegrationDiscordTokensEntityRepositoryWrite IntegrationDiscordTokens { get; }
- IIntegrationGOGTokensEntityRepositoryWrite IntegrationGOGTokens { get; }
- IIntegrationGOGToOwnedTenantEntityRepositoryWrite IntegrationGOGToOwnedTenants { get; }
- IIntegrationSteamTokensEntityRepositoryWrite IntegrationSteamTokens { get; }
- IIntegrationSteamToOwnedTenantEntityRepositoryWrite IntegrationSteamToOwnedTenants { get; }
-
- Task SaveChangesAsync(CancellationToken ct);
-}
-
[TransientService]
internal class UnitOfWrite : IUnitOfWrite
{
@@ -66,7 +19,7 @@ internal class UnitOfWrite : IUnitOfWrite
private IDbContextTransaction _dbContextTransaction;
- public UpsertEntityFactory UpsertEntityFactory { get; private set; }
+ public IUpsertEntityFactory UpsertEntityFactory { get; private set; }
public IAutocompleteEntityRepositoryWrite Autocompletes { get; }
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationDiscordTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationDiscordTokensEntityRepository.cs
index 7e29fc3d..20f5a142 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationDiscordTokensEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationDiscordTokensEntityRepository.cs
@@ -8,9 +8,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IIntegrationDiscordTokensEntityRepositoryRead : IRepositoryRead;
-public interface IIntegrationDiscordTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationDiscordTokensEntityRepositoryRead;
-
[ScopedService]
internal class IntegrationDiscordTokensEntityRepository : Repository, IIntegrationDiscordTokensEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGToOwnedTenantEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGToOwnedTenantEntityRepository.cs
index 78fb19bb..6ee6af62 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGToOwnedTenantEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGToOwnedTenantEntityRepository.cs
@@ -6,9 +6,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IIntegrationGOGToOwnedTenantEntityRepositoryRead : IRepositoryRead;
-public interface IIntegrationGOGToOwnedTenantEntityRepositoryWrite : IRepositoryWrite, IIntegrationGOGToOwnedTenantEntityRepositoryRead;
-
[ScopedService]
internal class IntegrationGOGToOwnedTenantEntityRepository : Repository, IIntegrationGOGToOwnedTenantEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGTokensEntityRepository.cs
index 58887c93..2e36aa54 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGTokensEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGOGTokensEntityRepository.cs
@@ -8,9 +8,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IIntegrationGOGTokensEntityRepositoryRead : IRepositoryRead;
-public interface IIntegrationGOGTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationGOGTokensEntityRepositoryRead;
-
[ScopedService]
internal class IntegrationGOGTokensEntityRepository : Repository, IIntegrationGOGTokensEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGitHubTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGitHubTokensEntityRepository.cs
index 7ebdb6b8..7a4b90bb 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGitHubTokensEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationGitHubTokensEntityRepository.cs
@@ -8,9 +8,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IIntegrationGitHubTokensEntityRepositoryRead : IRepositoryRead;
-public interface IIntegrationGitHubTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationGitHubTokensEntityRepositoryRead;
-
[ScopedService]
internal class IntegrationGitHubTokensEntityRepository : Repository, IIntegrationGitHubTokensEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamToOwnedTenantEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamToOwnedTenantEntityRepository.cs
index b9755460..507fdd5a 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamToOwnedTenantEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamToOwnedTenantEntityRepository.cs
@@ -6,9 +6,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IIntegrationSteamToOwnedTenantEntityRepositoryRead : IRepositoryRead;
-public interface IIntegrationSteamToOwnedTenantEntityRepositoryWrite : IRepositoryWrite, IIntegrationSteamToOwnedTenantEntityRepositoryRead;
-
[ScopedService]
internal class IntegrationSteamToOwnedTenantEntityRepository : Repository, IIntegrationSteamToOwnedTenantEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamTokensEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamTokensEntityRepository.cs
index bf960da3..961ab31c 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamTokensEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/IntegrationSteamTokensEntityRepository.cs
@@ -8,9 +8,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface IIntegrationSteamTokensEntityRepositoryRead : IRepositoryRead;
-public interface IIntegrationSteamTokensEntityRepositoryWrite : IRepositoryWrite, IIntegrationSteamTokensEntityRepositoryRead;
-
[ScopedService]
internal class IntegrationSteamTokensEntityRepository : Repository, IIntegrationSteamTokensEntityRepositoryWrite
{
diff --git a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsArticleEntityRepository.cs b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsArticleEntityRepository.cs
index b3245b3b..6740dd79 100644
--- a/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsArticleEntityRepository.cs
+++ b/src/BUTR.Site.NexusMods.Server/Repositories/NexusModsArticleEntityRepository.cs
@@ -15,16 +15,6 @@
namespace BUTR.Site.NexusMods.Server.Repositories;
-public interface INexusModsArticleEntityRepositoryRead : IRepositoryRead
-{
- Task> GetAllModuleIdsAsync(string authorName, CancellationToken ct);
-}
-
-public interface INexusModsArticleEntityRepositoryWrite : IRepositoryWrite