diff --git a/server/Tingle.Dependabot/AppSetup.cs b/server/Tingle.Dependabot/AppSetup.cs index 20f07832..27670313 100644 --- a/server/Tingle.Dependabot/AppSetup.cs +++ b/server/Tingle.Dependabot/AppSetup.cs @@ -10,7 +10,7 @@ internal static class AppSetup { private class ProjectSetupInfo { - public required Uri Url { get; set; } + public required AzureDevOpsProjectUrl Url { get; set; } public required string Token { get; set; } public bool AutoComplete { get; set; } public List? AutoCompleteIgnoreConfigs { get; set; } @@ -50,8 +50,8 @@ public static async Task SetupAsync(WebApplication app, CancellationToken cancel var projects = await context.Projects.ToListAsync(cancellationToken); foreach (var setup in setups) { - var url = (AzureDevOpsProjectUrl)setup.Url; - var project = projects.SingleOrDefault(p => new Uri(p.Url!) == setup.Url); + var url = setup.Url; + var project = projects.SingleOrDefault(p => p.Url == setup.Url); if (project is null) { project = new Models.Management.Project diff --git a/server/Tingle.Dependabot/Models/MainDbContext.cs b/server/Tingle.Dependabot/Models/MainDbContext.cs index a56bb19c..455ba3b0 100644 --- a/server/Tingle.Dependabot/Models/MainDbContext.cs +++ b/server/Tingle.Dependabot/Models/MainDbContext.cs @@ -1,6 +1,9 @@ using Microsoft.AspNetCore.DataProtection.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.ChangeTracking; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; using Tingle.Dependabot.Models.Management; +using Tingle.Dependabot.Workflow; namespace Tingle.Dependabot.Models; @@ -14,6 +17,13 @@ public MainDbContext(DbContextOptions options) : base(options) { public DbSet DataProtectionKeys => Set(); + protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder) + { + base.ConfigureConventions(configurationBuilder); + + configurationBuilder.Properties().HaveConversion(); + } + protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); @@ -60,4 +70,18 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) builder.OwnsOne(j => j.Resources); }); } + + private class AzureDevOpsProjectUrlConverter : ValueConverter + { + public AzureDevOpsProjectUrlConverter() : base(convertToProviderExpression: v => v.ToString(), + convertFromProviderExpression: v => v == null ? default : new AzureDevOpsProjectUrl(v)) + { } + } + private class AzureDevOpsProjectUrlComparer : ValueComparer + { + public AzureDevOpsProjectUrlComparer() : base(equalsExpression: (l, r) => l == r, + hashCodeExpression: v => v.GetHashCode(), + snapshotExpression: v => new AzureDevOpsProjectUrl(v.ToString())) + { } + } } diff --git a/server/Tingle.Dependabot/Models/Management/Project.cs b/server/Tingle.Dependabot/Models/Management/Project.cs index 00ad2345..31bfeec0 100644 --- a/server/Tingle.Dependabot/Models/Management/Project.cs +++ b/server/Tingle.Dependabot/Models/Management/Project.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; +using Tingle.Dependabot.Workflow; namespace Tingle.Dependabot.Models.Management; @@ -29,8 +30,7 @@ public class Project /// URL for the project. /// https://dev.azure.com/tingle/dependabot [Url] - [Required] - public string? Url { get; set; } // TODO: change this to AzureDevOpsProjectUrl when we have converters for JSON and EfCore hence reduce the conversions all over the code + public AzureDevOpsProjectUrl Url { get; set; } /// /// Token for accessing the project with permissions for repositories, pull requests, and service hooks. diff --git a/server/Tingle.Dependabot/Workflow/AzureDevOpsProjectUrl.cs b/server/Tingle.Dependabot/Workflow/AzureDevOpsProjectUrl.cs index f765573d..272da2bb 100644 --- a/server/Tingle.Dependabot/Workflow/AzureDevOpsProjectUrl.cs +++ b/server/Tingle.Dependabot/Workflow/AzureDevOpsProjectUrl.cs @@ -1,9 +1,12 @@ using System.ComponentModel; using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; namespace Tingle.Dependabot.Workflow; /// Easier manager and parser for URLs of projects on Azure DevOps. +[JsonConverter(typeof(AzureDevOpsProjectUrlJsonConverter))] [TypeConverter(typeof(AzureDevOpsProjectUrlTypeConverter))] public readonly struct AzureDevOpsProjectUrl : IEquatable { @@ -112,4 +115,24 @@ private class AzureDevOpsProjectUrlTypeConverter : TypeConverter return base.ConvertTo(context, culture, value, destinationType); } } + + private class AzureDevOpsProjectUrlJsonConverter : JsonConverter + { + public override AzureDevOpsProjectUrl Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) return default; + if (reader.TokenType != JsonTokenType.String) + { + throw new InvalidOperationException("Only strings are supported"); + } + + var str = reader.GetString(); + return new AzureDevOpsProjectUrl(str!); + } + + public override void Write(Utf8JsonWriter writer, AzureDevOpsProjectUrl value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } } diff --git a/server/Tingle.Dependabot/Workflow/AzureDevOpsProvider.cs b/server/Tingle.Dependabot/Workflow/AzureDevOpsProvider.cs index 8f876f41..4a1dbb71 100644 --- a/server/Tingle.Dependabot/Workflow/AzureDevOpsProvider.cs +++ b/server/Tingle.Dependabot/Workflow/AzureDevOpsProvider.cs @@ -63,7 +63,7 @@ public async Task> CreateOrUpdateSubscriptionsAsync(Project project }; // fetch the subscriptions - var url = (AzureDevOpsProjectUrl)project.Url!; + var url = project.Url; var uri = new UriBuilder { Scheme = url.Scheme, @@ -133,7 +133,7 @@ public async Task> CreateOrUpdateSubscriptionsAsync(Project project public async Task GetProjectAsync(Project project, CancellationToken cancellationToken) { - var url = (AzureDevOpsProjectUrl)project.Url!; + var url = project.Url; var uri = new UriBuilder { Scheme = url.Scheme, @@ -148,7 +148,7 @@ public async Task GetProjectAsync(Project project, CancellationToke public async Task> GetRepositoriesAsync(Project project, CancellationToken cancellationToken) { - var url = (AzureDevOpsProjectUrl)project.Url!; + var url = project.Url; var uri = new UriBuilder { Scheme = url.Scheme, @@ -164,7 +164,7 @@ public async Task> GetRepositoriesAsync(Project project, Ca public async Task GetRepositoryAsync(Project project, string repositoryIdOrName, CancellationToken cancellationToken) { - var url = (AzureDevOpsProjectUrl)project.Url!; + var url = project.Url; var uri = new UriBuilder { Scheme = url.Scheme, @@ -179,7 +179,7 @@ public async Task GetRepositoryAsync(Project project, string rep public async Task GetConfigurationFileAsync(Project project, string repositoryIdOrName, CancellationToken cancellationToken = default) { - var url = (AzureDevOpsProjectUrl)project.Url!; + var url = project.Url; // Try all known paths foreach (var path in ConfigurationFilePaths) diff --git a/server/Tingle.Dependabot/Workflow/Synchronizer.cs b/server/Tingle.Dependabot/Workflow/Synchronizer.cs index aaed3b4a..ad7add13 100644 --- a/server/Tingle.Dependabot/Workflow/Synchronizer.cs +++ b/server/Tingle.Dependabot/Workflow/Synchronizer.cs @@ -84,7 +84,7 @@ public async Task SynchronizeAsync(Project project, bool trigger, CancellationTo cancellationToken: cancellationToken); // Track for further synchronization - var sci = new SynchronizerConfigurationItem(((AzureDevOpsProjectUrl)project.Url!).MakeRepositorySlug(adoRepo.Name), adoRepo, item); + var sci = new SynchronizerConfigurationItem(project.Url.MakeRepositorySlug(adoRepo.Name), adoRepo, item); syncPairs.Add((sci, repository)); } @@ -126,7 +126,7 @@ public async Task SynchronizeAsync(Project project, Repository repository, bool cancellationToken: cancellationToken); // perform synchronization - var sci = new SynchronizerConfigurationItem(((AzureDevOpsProjectUrl)project.Url!).MakeRepositorySlug(adoRepo.Name), adoRepo, item); + var sci = new SynchronizerConfigurationItem(project.Url.MakeRepositorySlug(adoRepo.Name), adoRepo, item); await SynchronizeAsync(project, repository, sci, trigger, cancellationToken); } @@ -154,7 +154,7 @@ public async Task SynchronizeAsync(Project project, string? repositoryProviderId select r).SingleOrDefaultAsync(cancellationToken); // perform synchronization - var sci = new SynchronizerConfigurationItem(((AzureDevOpsProjectUrl)project.Url!).MakeRepositorySlug(adoRepo.Name), adoRepo, item); + var sci = new SynchronizerConfigurationItem(project.Url.MakeRepositorySlug(adoRepo.Name), adoRepo, item); await SynchronizeAsync(project, repository, sci, trigger, cancellationToken); } diff --git a/server/Tingle.Dependabot/Workflow/UpdateRunner.cs b/server/Tingle.Dependabot/Workflow/UpdateRunner.cs index 12cf6233..8e3016b0 100644 --- a/server/Tingle.Dependabot/Workflow/UpdateRunner.cs +++ b/server/Tingle.Dependabot/Workflow/UpdateRunner.cs @@ -276,7 +276,7 @@ internal async Task> CreateEnvironmentVariables(Proj .AddIfNotDefault("DEPENDABOT_MILESTONE", update.Milestone?.ToString()); // Add values for Azure DevOps - var url = (AzureDevOpsProjectUrl)project.Url!; + var url = project.Url; values.AddIfNotDefault("AZURE_HOSTNAME", url.Hostname) .AddIfNotDefault("AZURE_ORGANIZATION", url.OrganizationName) .AddIfNotDefault("AZURE_PROJECT", url.ProjectName) @@ -300,7 +300,7 @@ internal async Task WriteJobDefinitionAsync(Project project, [return: NotNullIfNotNull(nameof(value))] static JsonNode? ToJsonNode(T? value) => value is null ? null : JsonSerializer.SerializeToNode(value, serializerOptions); // null ensures we do not add to the values - var url = (AzureDevOpsProjectUrl)project.Url!; + var url = project.Url; var credentialsMetadata = MakeCredentialsMetadata(credentials); // check if debug is enabled for the project via Feature Management