From dec8b15278b73b7379568fd44c605139b3626831 Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Wed, 16 Sep 2026 17:31:21 -0300 Subject: [PATCH] fix(services): translate duplicate name races --- .../ServicesRegistryDataService.cs | 29 +++++++++++++++++-- .../Models/ServicesRegistryModels.cs | 14 +++++++++ .../Implementation/ServicesRegistryService.cs | 22 ++++++++++++-- 3 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 SeaHaven.DataServices/Models/ServicesRegistryModels.cs diff --git a/SeaHaven.DataServices/Implementation/ServicesRegistryDataService.cs b/SeaHaven.DataServices/Implementation/ServicesRegistryDataService.cs index b6acd06..c2e9c77 100644 --- a/SeaHaven.DataServices/Implementation/ServicesRegistryDataService.cs +++ b/SeaHaven.DataServices/Implementation/ServicesRegistryDataService.cs @@ -2,6 +2,7 @@ using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.EntityFrameworkCore; using SeaHaven.DataServices.Interfaces; +using SeaHaven.DataServices.Models; namespace SeaHaven.DataServices.Implementation { @@ -90,7 +91,15 @@ namespace SeaHaven.DataServices.Implementation public async Task AddServiceAsync(Service service, CancellationToken cancellationToken) { await _context.Services.AddAsync(service, cancellationToken); - await _context.SaveChangesAsync(cancellationToken); + try + { + await _context.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException ex) when (IsUniqueNameViolation(ex)) + { + throw new ServicesRegistryDuplicateNameException(ex); + } + return service; } @@ -112,7 +121,14 @@ namespace SeaHaven.DataServices.Implementation }); } - await _context.SaveChangesAsync(cancellationToken); + try + { + await _context.SaveChangesAsync(cancellationToken); + } + catch (DbUpdateException ex) when (IsUniqueNameViolation(ex)) + { + throw new ServicesRegistryDuplicateNameException(ex); + } } public async Task DeactivateServiceAsync(int id, CancellationToken cancellationToken) @@ -126,5 +142,14 @@ namespace SeaHaven.DataServices.Implementation await _context.SaveChangesAsync(cancellationToken); return true; } + + private static bool IsUniqueNameViolation(DbUpdateException exception) + { + var message = exception.InnerException?.Message ?? exception.Message; + return message.Contains("IX_Services_NormalizedName", StringComparison.OrdinalIgnoreCase) + || message.Contains("NormalizedName", StringComparison.OrdinalIgnoreCase) + && (message.Contains("duplicate", StringComparison.OrdinalIgnoreCase) + || message.Contains("unique", StringComparison.OrdinalIgnoreCase)); + } } } diff --git a/SeaHaven.DataServices/Models/ServicesRegistryModels.cs b/SeaHaven.DataServices/Models/ServicesRegistryModels.cs new file mode 100644 index 0000000..b34b25b --- /dev/null +++ b/SeaHaven.DataServices/Models/ServicesRegistryModels.cs @@ -0,0 +1,14 @@ +namespace SeaHaven.DataServices.Models +{ + /// + /// Raised by the registry data service when the database unique name index + /// wins a concurrent create or update race. + /// + public sealed class ServicesRegistryDuplicateNameException : Exception + { + public ServicesRegistryDuplicateNameException(Exception innerException) + : base("A service name is already in use.", innerException) + { + } + } +} diff --git a/SeaHaven.Services/Implementation/ServicesRegistryService.cs b/SeaHaven.Services/Implementation/ServicesRegistryService.cs index 79e2dd9..b6c7dd0 100644 --- a/SeaHaven.Services/Implementation/ServicesRegistryService.cs +++ b/SeaHaven.Services/Implementation/ServicesRegistryService.cs @@ -2,6 +2,7 @@ using System.Security.Claims; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using SeaHaven.DataServices.Interfaces; +using SeaHaven.DataServices.Models; using SeaHaven.Services.DTOs; using SeaHaven.Services.Exceptions; using SeaHaven.Services.Interfaces; @@ -71,7 +72,14 @@ namespace SeaHaven.Services.Implementation .ToList() }; - await _dataService.AddServiceAsync(service, cancellationToken); + try + { + await _dataService.AddServiceAsync(service, cancellationToken); + } + catch (ServicesRegistryDuplicateNameException) + { + throw DuplicateName(); + } var row = await _dataService.GetServiceRowByIdAsync(service.Id, cancellationToken); return MapToDto(row!); @@ -101,7 +109,14 @@ namespace SeaHaven.Services.Implementation service.RequiresCompletionDocument = input.RequiresCompletionDocument; service.CompletionDocTemplateId = template?.Id; - await _dataService.SaveServiceAsync(service, supportedTypes, cancellationToken); + try + { + await _dataService.SaveServiceAsync(service, supportedTypes, cancellationToken); + } + catch (ServicesRegistryDuplicateNameException) + { + throw DuplicateName(); + } var row = await _dataService.GetServiceRowByIdAsync(id, cancellationToken); return MapToDto(row!); @@ -123,6 +138,9 @@ namespace SeaHaven.Services.Implementation throw new ServicesRegistryValidationException("Forbidden", "Scheduler or Admin role is required."); } + private static ServicesRegistryValidationException DuplicateName() => + new("DuplicateName", "A service with this name already exists."); + private static string RequireValidName(string? name) { if (string.IsNullOrWhiteSpace(name))