Skip to content

Commit

Permalink
feat: declaratively create namespaces at startup via config (#52)
Browse files Browse the repository at this point in the history
* chore(deps): updated dependencies

* feat: declaratively create namespaces at startup via config
  • Loading branch information
chgl authored Mar 26, 2023
1 parent 778e92e commit 2e684ca
Show file tree
Hide file tree
Showing 21 changed files with 243 additions and 93 deletions.
2 changes: 1 addition & 1 deletion src/Vfps.Benchmarks/Vfps.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.13.2" />
<PackageReference Include="BenchmarkDotNet" Version="0.13.5" />
</ItemGroup>

<ItemGroup>
Expand Down
4 changes: 2 additions & 2 deletions src/Vfps.IntegrationTests/MigrationsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ public class MigrationsTests : IAsyncLifetime, IClassFixture<NetworkFixture>

private readonly string migrationsImage;

private readonly ITestcontainersBuilder<TestcontainersContainer> migrationsContainerBuilder;
private readonly ContainerBuilder<TestcontainersContainer> migrationsContainerBuilder;

public MigrationsTests(ITestOutputHelper output, NetworkFixture networkFixture)
{
this.output = output;

postgresqlContainer = new TestcontainersBuilder<PostgreSqlTestcontainer>()
postgresqlContainer = new ContainerBuilder<PostgreSqlTestcontainer>()
.WithDatabase(
new PostgreSqlTestcontainerConfiguration(
"docker.io/bitnami/postgresql:14.5.0-debian-11-r17"
Expand Down
4 changes: 2 additions & 2 deletions src/Vfps.IntegrationTests/NetworkFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ namespace Vfps.IntegrationTests;

public sealed class NetworkFixture : IAsyncLifetime
{
public IDockerNetwork Network { get; } =
new TestcontainersNetworkBuilder()
public INetwork Network { get; } =
new NetworkBuilder()
.WithDriver(NetworkDriver.Bridge)
.WithName(Guid.NewGuid().ToString("D"))
.Build();
Expand Down
6 changes: 3 additions & 3 deletions src/Vfps.IntegrationTests/Vfps.IntegrationTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="Testcontainers" Version="2.2.0" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="Testcontainers" Version="2.4.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
4 changes: 2 additions & 2 deletions src/Vfps.StressTests/Vfps.StressTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="NBomber" Version="3.3.0" />
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
Expand Down
3 changes: 2 additions & 1 deletion src/Vfps.Tests/ServiceTests/NamespaceServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ public class NamespaceServiceTests : ServiceTestBase

public NamespaceServiceTests()
{
sut = new Services.NamespaceService(InMemoryPseudonymContext);
var namespaceRepository = new NamespaceRepository(InMemoryPseudonymContext);
sut = new Services.NamespaceService(InMemoryPseudonymContext, namespaceRepository);
}

[Fact]
Expand Down
12 changes: 6 additions & 6 deletions src/Vfps.Tests/Vfps.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@
</PackageReference>
<PackageReference Include="EntityFrameworkCore.Exceptions.Sqlite" Version="6.0.3" />
<PackageReference Include="FakeItEasy" Version="7.3.1" />
<PackageReference Include="FakeItEasy.Analyzer.CSharp" Version="6.1.0">
<PackageReference Include="FakeItEasy.Analyzer.CSharp" Version="6.1.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.8.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.0" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="7.0.4" />
<PackageReference Include="Microsoft.Data.Sqlite.Core" Version="7.0.4" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="xunit" Version="2.4.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
6 changes: 4 additions & 2 deletions src/Vfps.Tests/WebAppTests/TestFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ protected override void ConfigureWebHost(IWebHostBuilder builder)

internal static class ServiceCollectionExtensions
{
public static void RemoveDbContext<T>(this IServiceCollection services) where T : DbContext
public static void RemoveDbContext<T>(this IServiceCollection services)
where T : DbContext
{
var descriptor = services.SingleOrDefault(
d => d.ServiceType == typeof(DbContextOptions<T>)
Expand All @@ -41,7 +42,8 @@ public static void RemoveDbContext<T>(this IServiceCollection services) where T
services.Remove(descriptor);
}

public static void EnsureDbCreated<T>(this IServiceCollection services) where T : DbContext
public static void EnsureDbCreated<T>(this IServiceCollection services)
where T : DbContext
{
var serviceProvider = services.BuildServiceProvider();

Expand Down
16 changes: 14 additions & 2 deletions src/Vfps/Data/CachingNamespaceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,19 @@ CacheConfig cacheConfig
private NamespaceRepository NamespaceRepository { get; }

/// <inheritdoc/>
public async Task<Namespace?> FindAsync(string namespaceName)
public async Task<Namespace> CreateAsync(
Namespace @namespace,
CancellationToken cancellationToken
)
{
return await NamespaceRepository.CreateAsync(@namespace, cancellationToken);
}

/// <inheritdoc/>
public async Task<Namespace?> FindAsync(
string namespaceName,
CancellationToken cancellationToken
)
{
var cacheKey = $"namespaces.{namespaceName}";

Expand All @@ -32,7 +44,7 @@ CacheConfig cacheConfig
{
entry.SetSize(1).SetAbsoluteExpiration(CacheConfig.AbsoluteExpiration);

return await NamespaceRepository.FindAsync(namespaceName);
return await NamespaceRepository.FindAsync(namespaceName, cancellationToken);
}
);
}
Expand Down
14 changes: 13 additions & 1 deletion src/Vfps/Data/INamespaceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,18 @@ public interface INamespaceRepository
/// Finds a namespace by its name.
/// </summary>
/// <param name="namespaceName">The name of the namespace to get.</param>
/// <param name="cancellationToken">A cancellation token to abort the action</param>
/// <returns>The namespace if it exists or null if it doesn't.</returns>
Task<Models.Namespace?> FindAsync(string namespaceName);
Task<Models.Namespace?> FindAsync(string namespaceName, CancellationToken cancellationToken);

/// <summary>
/// Create a namespace.
/// </summary>
/// <param name="namespace">The namespace object</param>
/// <param name="cancellationToken">A a cancellation token to abort the action</param>
/// <returns>The created namespace</returns>
Task<Models.Namespace> CreateAsync(
Models.Namespace @namespace,
CancellationToken cancellationToken
);
}
4 changes: 2 additions & 2 deletions src/Vfps/Data/Models/Namespace.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ public class Namespace : TracksCreationAndUpdates
public string? Description { get; set; }
public PseudonymGenerationMethod PseudonymGenerationMethod { get; set; }
public uint PseudonymLength { get; set; }
public string? PseudonymPrefix { get; set; }
public string? PseudonymSuffix { get; set; }
public string? PseudonymPrefix { get; set; } = string.Empty;
public string? PseudonymSuffix { get; set; } = string.Empty;

public ICollection<Pseudonym> Pseudonyms { get; set; }
}
18 changes: 16 additions & 2 deletions src/Vfps/Data/NamespaceRepository.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,22 @@ public NamespaceRepository(PseudonymContext context)
private PseudonymContext Context { get; }

/// <inheritdoc/>
public async Task<Namespace?> FindAsync(string namespaceName)
public async Task<Namespace> CreateAsync(
Namespace @namespace,
CancellationToken cancellationToken
)
{
return await Context.Namespaces.FindAsync(namespaceName);
Context.Add(@namespace);
await Context.SaveChangesAsync(cancellationToken);
return @namespace;
}

/// <inheritdoc/>
public async Task<Namespace?> FindAsync(
string namespaceName,
CancellationToken cancellationToken
)
{
return await Context.Namespaces.FindAsync(namespaceName, cancellationToken);
}
}
3 changes: 2 additions & 1 deletion src/Vfps/Data/PseudonymContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ namespace Vfps.Data;

public class PseudonymContext : DbContext
{
public PseudonymContext(DbContextOptions<PseudonymContext> options) : base(options) { }
public PseudonymContext(DbContextOptions<PseudonymContext> options)
: base(options) { }

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
Expand Down
8 changes: 6 additions & 2 deletions src/Vfps/Fhir/FhirController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,17 @@ IPseudonymRepository pseudonymRepository
/// Create a pseudonym for an original value in the given namespace.
/// </summary>
/// <param name="parametersResource">A FHIR Parameters resource</param>
/// <param name="cancellationToken">A cancellation token to abort the request</param>
/// <returns>Either a FHIR Parameters resource containing the created pseudonym or a FHIR OperationOutcome in case of errors.</returns>
[HttpPost("$create-pseudonym")]
[ProducesResponseType(typeof(Parameters), 200)]
[ProducesResponseType(typeof(OperationOutcome), 400)]
[ProducesResponseType(typeof(OperationOutcome), 404)]
[ProducesResponseType(typeof(OperationOutcome), 500)]
public async Task<ObjectResult> CreatePseudonym([FromBody] Parameters? parametersResource)
public async Task<ObjectResult> CreatePseudonym(
[FromBody] Parameters? parametersResource,
CancellationToken cancellationToken = default
)
{
if (parametersResource is null)
{
Expand Down Expand Up @@ -77,7 +81,7 @@ public async Task<ObjectResult> CreatePseudonym([FromBody] Parameters? parameter
return BadRequest(outcome);
}

var @namespace = await NamespaceRepository.FindAsync(namespaceName);
var @namespace = await NamespaceRepository.FindAsync(namespaceName, cancellationToken);
if (@namespace is null)
{
var outcome = new OperationOutcome();
Expand Down
84 changes: 84 additions & 0 deletions src/Vfps/InitNamespacesBackgroundService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
using EntityFramework.Exceptions.Common;
using Vfps.Data;
using Vfps.Protos;
using Vfps.Services;
using NamespaceService = Vfps.Services.NamespaceService;

namespace Vfps;

public class InitNamespacesBackgroundService : BackgroundService
{
public InitNamespacesBackgroundService(
IServiceProvider serviceProvider,
IConfiguration configuration,
ILogger<InitNamespacesBackgroundService> logger
)
{
ServiceProvider = serviceProvider;
Configuration = configuration;
Logger = logger;
}

private IConfiguration Configuration { get; }
private ILogger<InitNamespacesBackgroundService> Logger { get; }
private IServiceProvider ServiceProvider { get; }

protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
var namespaces = Configuration
.GetSection("Init:v1:Namespaces")
.Get<List<Data.Models.Namespace>>();
if (namespaces is null || namespaces.Count == 0)
{
Logger.LogInformation("No namespaces configured to create during startup.");
return;
}

using var scope = ServiceProvider.CreateScope();
var namespaceRepository = scope.ServiceProvider.GetRequiredService<INamespaceRepository>();

foreach (var @namespace in namespaces)
{
Logger.LogInformation(
"Attempting to create namespace {NamespaceName}",
@namespace.Name
);

@namespace.LastUpdatedAt = DateTime.UtcNow;
@namespace.CreatedAt = DateTime.UtcNow;

var maybeExistsNamespace = await namespaceRepository.FindAsync(
@namespace.Name,
stoppingToken
);
if (maybeExistsNamespace is not null)
{
Logger.LogInformation(
"A namespace with the same name {NamespaceName} already exists. Will not be overridden.",
@namespace.Name
);
return;
}

Logger.LogInformation(
"Namespace {NamespaceName} doesn't seem to exist yet, attempting to create.",
@namespace.Name
);
try
{
await namespaceRepository.CreateAsync(@namespace, stoppingToken);
Logger.LogInformation(
"Successfully created namespace {NamespaceName}.",
@namespace.Name
);
}
catch (UniqueConstraintException)
{
Logger.LogInformation(
"A namespace with the same name {NamespaceName} already exists. Will not be overridden.",
@namespace.Name
);
}
}
}
}
4 changes: 3 additions & 1 deletion src/Vfps/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,8 @@
builder.Services.AddHostedService<MemoryCacheMetricsBackgroundService>();
}

builder.Services.AddHostedService<InitNamespacesBackgroundService>();

builder.Services.AddControllers(options =>
{
options.InputFormatters.Insert(0, new FhirInputFormatter());
Expand Down Expand Up @@ -161,7 +163,7 @@
"/readyz",
new HealthCheckOptions
{
// there's currently now readiness probes depending on external state,
// there's currently no readiness probes depending on external state,
// but in case we ever add one, this prepares the code for it.
// see https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks?view=aspnetcore-7.0#separate-readiness-and-liveness-probes
Predicate = healthCheck => healthCheck.Tags.Contains("ready")
Expand Down
11 changes: 5 additions & 6 deletions src/Vfps/Services/NamespaceService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Microsoft.EntityFrameworkCore;
using System.Reflection.Metadata.Ecma335;
using Vfps.Data;
using Vfps.Protos;

Expand All @@ -12,11 +11,13 @@ namespace Vfps.Services;
public class NamespaceService : Protos.NamespaceService.NamespaceServiceBase
{
/// <inheritdoc/>
public NamespaceService(PseudonymContext context)
public NamespaceService(PseudonymContext context, INamespaceRepository namespaceRepository)
{
NamespaceRepository = namespaceRepository;
Context = context;
}

private INamespaceRepository NamespaceRepository { get; }
private PseudonymContext Context { get; }

/// <inheritdoc/>
Expand Down Expand Up @@ -46,11 +47,9 @@ ServerCallContext context
LastUpdatedAt = now,
};

Context.Add(@namespace);

try
{
await Context.SaveChangesAsync(context.CancellationToken);
await NamespaceRepository.CreateAsync(@namespace, context.CancellationToken);
}
catch (UniqueConstraintException)
{
Expand Down Expand Up @@ -90,7 +89,7 @@ public override async Task<NamespaceServiceGetResponse> Get(
ServerCallContext context
)
{
var @namespace = await Context.Namespaces.FindAsync(
var @namespace = await NamespaceRepository.FindAsync(
request.Name,
context.CancellationToken
);
Expand Down
5 changes: 4 additions & 1 deletion src/Vfps/Services/PseudonymService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ ServerCallContext context
{
var now = DateTimeOffset.UtcNow;

var @namespace = await NamespaceRepository.FindAsync(request.Namespace);
var @namespace = await NamespaceRepository.FindAsync(
request.Namespace,
context.CancellationToken
);
if (@namespace is null)
{
var metadata = new Metadata { { "Namespace", request.Namespace } };
Expand Down
Loading

0 comments on commit 2e684ca

Please sign in to comment.