fix(services): translate duplicate name races

This commit is contained in:
Alexandre Brandizzi 2026-09-16 17:31:21 -03:00
parent 45f98e8154
commit dec8b15278
3 changed files with 61 additions and 4 deletions

View file

@ -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<Service> 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<bool> 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));
}
}
}

View file

@ -0,0 +1,14 @@
namespace SeaHaven.DataServices.Models
{
/// <summary>
/// Raised by the registry data service when the database unique name index
/// wins a concurrent create or update race.
/// </summary>
public sealed class ServicesRegistryDuplicateNameException : Exception
{
public ServicesRegistryDuplicateNameException(Exception innerException)
: base("A service name is already in use.", innerException)
{
}
}
}

View file

@ -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))