fix(external-system-gateway): resolve ExternalSystemGateway-011 — name-keyed repository lookups replace fetch-all-then-filter on the call hot path

This commit is contained in:
Joseph Doherty
2026-05-17 00:02:45 -04:00
parent 1e2e7d2e7c
commit a55502254e
10 changed files with 448 additions and 131 deletions

View File

@@ -6,6 +6,15 @@ public interface IExternalSystemRepository
{
// ExternalSystemDefinition
Task<ExternalSystemDefinition?> GetExternalSystemByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Returns the external system with the given name, or <c>null</c> if no such
/// system exists. A name-keyed lookup so hot-path resolution (e.g. a script's
/// <c>ExternalSystem.Call()</c>) does not have to fetch every system and filter
/// in memory on each call (ExternalSystemGateway-011).
/// </summary>
Task<ExternalSystemDefinition?> GetExternalSystemByNameAsync(string name, CancellationToken cancellationToken = default);
Task<IReadOnlyList<ExternalSystemDefinition>> GetAllExternalSystemsAsync(CancellationToken cancellationToken = default);
Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default);
Task UpdateExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default);
@@ -13,6 +22,15 @@ public interface IExternalSystemRepository
// ExternalSystemMethod
Task<ExternalSystemMethod?> GetExternalSystemMethodByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Returns the method with the given name belonging to the given external system,
/// or <c>null</c> if no such method exists. A name-keyed lookup so hot-path
/// resolution does not have to fetch every method of the system and filter in
/// memory on each call (ExternalSystemGateway-011).
/// </summary>
Task<ExternalSystemMethod?> GetMethodByNameAsync(int externalSystemId, string methodName, CancellationToken cancellationToken = default);
Task<IReadOnlyList<ExternalSystemMethod>> GetMethodsByExternalSystemIdAsync(int externalSystemId, CancellationToken cancellationToken = default);
Task AddExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default);
Task UpdateExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default);
@@ -20,6 +38,16 @@ public interface IExternalSystemRepository
// DatabaseConnectionDefinition
Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByIdAsync(int id, CancellationToken cancellationToken = default);
/// <summary>
/// Returns the database connection definition with the given name, or <c>null</c>
/// if no such connection exists. A name-keyed lookup so hot-path resolution (e.g.
/// a script's <c>Database.Connection()</c> / <c>Database.CachedWrite()</c>) does
/// not have to fetch every connection and filter in memory on each call
/// (ExternalSystemGateway-011).
/// </summary>
Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByNameAsync(string name, CancellationToken cancellationToken = default);
Task<IReadOnlyList<DatabaseConnectionDefinition>> GetAllDatabaseConnectionsAsync(CancellationToken cancellationToken = default);
Task AddDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default);
Task UpdateDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default);

View File

@@ -16,6 +16,12 @@ public class ExternalSystemRepository : IExternalSystemRepository
public async Task<ExternalSystemDefinition?> GetExternalSystemByIdAsync(int id, CancellationToken cancellationToken = default)
=> await _context.Set<ExternalSystemDefinition>().FindAsync(new object[] { id }, cancellationToken);
// ExternalSystemGateway-011: genuine name-keyed query (server-side WHERE) so the
// gateway's hot-path resolution does not fetch every system and filter in memory.
public async Task<ExternalSystemDefinition?> GetExternalSystemByNameAsync(string name, CancellationToken cancellationToken = default)
=> await _context.Set<ExternalSystemDefinition>()
.FirstOrDefaultAsync(s => s.Name == name, cancellationToken);
public async Task<IReadOnlyList<ExternalSystemDefinition>> GetAllExternalSystemsAsync(CancellationToken cancellationToken = default)
=> await _context.Set<ExternalSystemDefinition>().ToListAsync(cancellationToken);
@@ -34,6 +40,13 @@ public class ExternalSystemRepository : IExternalSystemRepository
public async Task<ExternalSystemMethod?> GetExternalSystemMethodByIdAsync(int id, CancellationToken cancellationToken = default)
=> await _context.Set<ExternalSystemMethod>().FindAsync(new object[] { id }, cancellationToken);
// ExternalSystemGateway-011: genuine name-keyed query scoped to the parent system.
public async Task<ExternalSystemMethod?> GetMethodByNameAsync(int externalSystemId, string methodName, CancellationToken cancellationToken = default)
=> await _context.Set<ExternalSystemMethod>()
.FirstOrDefaultAsync(
m => m.ExternalSystemDefinitionId == externalSystemId && m.Name == methodName,
cancellationToken);
public async Task<IReadOnlyList<ExternalSystemMethod>> GetMethodsByExternalSystemIdAsync(int externalSystemId, CancellationToken cancellationToken = default)
=> await _context.Set<ExternalSystemMethod>().Where(m => m.ExternalSystemDefinitionId == externalSystemId).ToListAsync(cancellationToken);
@@ -52,6 +65,11 @@ public class ExternalSystemRepository : IExternalSystemRepository
public async Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByIdAsync(int id, CancellationToken cancellationToken = default)
=> await _context.Set<DatabaseConnectionDefinition>().FindAsync(new object[] { id }, cancellationToken);
// ExternalSystemGateway-011: genuine name-keyed query (server-side WHERE).
public async Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByNameAsync(string name, CancellationToken cancellationToken = default)
=> await _context.Set<DatabaseConnectionDefinition>()
.FirstOrDefaultAsync(c => c.Name == name, cancellationToken);
public async Task<IReadOnlyList<DatabaseConnectionDefinition>> GetAllDatabaseConnectionsAsync(CancellationToken cancellationToken = default)
=> await _context.Set<DatabaseConnectionDefinition>().ToListAsync(cancellationToken);

View File

@@ -170,8 +170,10 @@ public class DatabaseGateway : IDatabaseGateway
string connectionName,
CancellationToken cancellationToken)
{
var connections = await _repository.GetAllDatabaseConnectionsAsync(cancellationToken);
return connections.FirstOrDefault(c =>
c.Name.Equals(connectionName, StringComparison.OrdinalIgnoreCase));
// ExternalSystemGateway-011: name-keyed repository lookup instead of
// fetch-all-then-filter — connection definitions are resolved on every
// cached write / connection request, so the repository performs an indexed
// query rather than loading every connection into memory.
return await _repository.GetDatabaseConnectionByNameAsync(connectionName, cancellationToken);
}
}

View File

@@ -373,13 +373,15 @@ public class ExternalSystemClient : IExternalSystemClient
string methodName,
CancellationToken cancellationToken)
{
var systems = await _repository.GetAllExternalSystemsAsync(cancellationToken);
var system = systems.FirstOrDefault(s => s.Name.Equals(systemName, StringComparison.OrdinalIgnoreCase));
// ExternalSystemGateway-011: name-keyed repository lookups instead of
// fetch-all-then-filter — definitions are resolved on every hot-path call
// (a script's ExternalSystem.Call()), so the repository performs an indexed
// query rather than loading every system / every method into memory.
var system = await _repository.GetExternalSystemByNameAsync(systemName, cancellationToken);
if (system == null)
return (null, null);
var methods = await _repository.GetMethodsByExternalSystemIdAsync(system.Id, cancellationToken);
var method = methods.FirstOrDefault(m => m.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase));
var method = await _repository.GetMethodByNameAsync(system.Id, methodName, cancellationToken);
return (system, method);
}

View File

@@ -53,6 +53,31 @@ public class SiteExternalSystemRepository : IExternalSystemRepository
return all.FirstOrDefault(e => e.Id == id);
}
/// <summary>
/// ExternalSystemGateway-011: genuine name-keyed lookup. The <c>external_systems</c>
/// table has <c>name</c> as its PRIMARY KEY, so this is a single indexed-row fetch
/// rather than the previous fetch-all-then-filter.
/// </summary>
public async Task<ExternalSystemDefinition?> GetExternalSystemByNameAsync(
string name, CancellationToken cancellationToken = default)
{
await using var connection = CreateConnection();
await connection.OpenAsync(cancellationToken);
await using var command = connection.CreateCommand();
command.CommandText = @"
SELECT name, endpoint_url, auth_type, auth_configuration
FROM external_systems
WHERE name = @name";
command.Parameters.AddWithValue("@name", name);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
if (!await reader.ReadAsync(cancellationToken))
return null;
return MapExternalSystem(reader);
}
// ── ExternalSystemMethod (read) ──
public async Task<IReadOnlyList<ExternalSystemMethod>> GetMethodsByExternalSystemIdAsync(
@@ -80,6 +105,23 @@ public class SiteExternalSystemRepository : IExternalSystemRepository
return ParseMethodDefinitions(json, externalSystemId);
}
/// <summary>
/// ExternalSystemGateway-011: name-keyed method lookup. The site store keeps a
/// system's methods in a single JSON column, so this fetches that one row and
/// matches the named method in memory. The parent system is resolved via
/// <see cref="GetMethodsByExternalSystemIdAsync"/>, which already performs a
/// single keyed read of the <c>method_definitions</c> column — this is no more
/// I/O than the gateway's previous fetch-all-methods-then-filter call, and on
/// the SQL-backed Central repository it is a genuine indexed query.
/// </summary>
public async Task<ExternalSystemMethod?> GetMethodByNameAsync(
int externalSystemId, string methodName, CancellationToken cancellationToken = default)
{
var methods = await GetMethodsByExternalSystemIdAsync(externalSystemId, cancellationToken);
return methods.FirstOrDefault(
m => m.Name.Equals(methodName, StringComparison.OrdinalIgnoreCase));
}
public async Task<ExternalSystemMethod?> GetExternalSystemMethodByIdAsync(
int id, CancellationToken cancellationToken = default)
{
@@ -134,6 +176,38 @@ public class SiteExternalSystemRepository : IExternalSystemRepository
return all.FirstOrDefault(d => d.Id == id);
}
/// <summary>
/// ExternalSystemGateway-011: genuine name-keyed lookup. The
/// <c>database_connections</c> table has <c>name</c> as its PRIMARY KEY, so this
/// is a single indexed-row fetch rather than fetch-all-then-filter.
/// </summary>
public async Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByNameAsync(
string name, CancellationToken cancellationToken = default)
{
await using var connection = CreateConnection();
await connection.OpenAsync(cancellationToken);
await using var command = connection.CreateCommand();
command.CommandText = @"
SELECT name, connection_string, max_retries, retry_delay_ms
FROM database_connections
WHERE name = @name";
command.Parameters.AddWithValue("@name", name);
await using var reader = await command.ExecuteReaderAsync(cancellationToken);
if (!await reader.ReadAsync(cancellationToken))
return null;
return new DatabaseConnectionDefinition(
name: reader.GetString(0),
connectionString: reader.GetString(1))
{
Id = GenerateSyntheticId(reader.GetString(0)),
MaxRetries = reader.GetInt32(2),
RetryDelay = TimeSpan.FromMilliseconds(reader.GetInt64(3))
};
}
// ── Write operations (not supported on site) ──
public Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default)