Add profiles domain, infra, API endpoints

This commit is contained in:
Holden
2026-06-18 12:05:54 -05:00
parent c7b8857208
commit e0171c3c82
36 changed files with 678 additions and 87 deletions

15
.idea/.idea.RecNet/.idea/dataSources.xml generated Normal file
View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="postgres" uuid="a92aa540-15b4-422d-869b-324493115cfc">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://localhost:43973/?password=4i6OtfQeYZ3qZ8OMOtak2acY&amp;user=postgres</jdbc-url>
<jdbc-additional-properties>
<property name="aspireResourceId" value="postgres-05c33d7f" />
</jdbc-additional-properties>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
</component>
</project>

View File

@@ -10,4 +10,9 @@
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RecNet.Infrastructure\RecNet.Infrastructure.csproj" />
<ProjectReference Include="..\RecNet.ServiceDefaults\RecNet.ServiceDefaults.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,6 +0,0 @@
@API_HostAddress = http://localhost:5155
GET {{API_HostAddress}}/weatherforecast/
Accept: application/json
###

View File

@@ -0,0 +1,6 @@
namespace API.Configurations;
public class RecNetConfiguration
{
public bool UseForwardedHeaders { get; set; } = true;
}

View File

@@ -0,0 +1,13 @@
using RecNet.Domain.Common;
namespace API.Contracts.Profiles.Requests;
public class LoginRequest
{
public required string AppVersion { get; set; }
public required string DeviceId { get; set; }
public required string PlatformAuthentication { get; set; }
public required string PlatformId { get; set; }
public PlatformType PlatformType { get; set; }
public required string Username { get; set; }
}

View File

@@ -0,0 +1,8 @@
using RecNet.Application.Profiles;
namespace API.Contracts.Profiles.Responses;
public class LoginResponse
{
public required ProfileDTO Profile { get; set; }
}

View File

@@ -0,0 +1,13 @@
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers.Config.V1;
[Route("api/[controller]/v1")]
[ApiController]
public class ConfigController : ControllerBase
{
// TODO: Resolve from Database
[HttpGet("motd")]
public ActionResult<string> GetMotd()
=> Ok("Hello World!");
}

View File

@@ -0,0 +1,64 @@
using API.Contracts.Profiles.Responses;
using AutoMapper;
using Microsoft.AspNetCore.Mvc;
using RecNet.Application.Profiles;
using RecNet.Domain.Repositories;
using LoginRequest = API.Contracts.Profiles.Requests.LoginRequest;
using Profile = RecNet.Domain.Entities.Profiles.Profile;
namespace API.Controllers.Profiles.V1;
[Route("api/[controller]/v1")]
[ApiController]
public class ProfilesController(
IProfileRepository profileRepository,
IMapper mapper) : ControllerBase
{
[HttpGet("{id:guid}")]
public async Task<ActionResult<ProfileDTO>> GetProfile(
Guid id,
CancellationToken ct)
{
var profile = await profileRepository.GetByIdAsync(id, ct);
if (profile == null)
return NotFound();
return mapper.Map<ProfileDTO>(profile);
}
[HttpGet("bulk")]
public async Task<IReadOnlyList<ProfileDTO>> GetBulkProfiles(
[FromQuery(Name = "id")] List<Guid> ids,
CancellationToken ct)
{
var profiles = await profileRepository.GetByIdsAsync(ids, ct);
return mapper.Map<List<ProfileDTO>>(profiles);
}
// TODO: Implement
[HttpPost("login")]
public async Task<ActionResult<LoginResponse>> Login(
[FromBody] LoginRequest request,
CancellationToken ct)
{
var profile = await profileRepository.GetByPlatform(request.PlatformType, request.PlatformId, ct);
if (profile is null)
{
profile = Profile.Create(request.Username, request.PlatformType, request.PlatformId);
await profileRepository.AddAsync(profile, ct);
}
profile.AddDeviceId(request.DeviceId);
if (request.Username != profile.Name)
profile.SetName(request.Username);
await profileRepository.SaveChangesAsync(ct);
return new LoginResponse
{
Profile = mapper.Map<ProfileDTO>(profile)
};
}
}

View File

@@ -1,25 +0,0 @@
using Microsoft.AspNetCore.Mvc;
namespace API.Controllers;
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries =
[
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
];
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
})
.ToArray();
}
}

View File

@@ -1,3 +1,9 @@
using API.Configurations;
using Microsoft.AspNetCore.HttpOverrides;
using RecNet.Application;
using RecNet.Infrastructure;
using RecNet.ServiceDefaults;
namespace API;
public class Program
@@ -6,26 +12,36 @@ public class Program
{
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var recNetOptions = builder.Configuration.GetSection("RecNet").Get<RecNetConfiguration>()
?? new RecNetConfiguration();
builder.AddServiceDefaults();
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders =
ForwardedHeaders.XForwardedFor |
ForwardedHeaders.XForwardedHost |
ForwardedHeaders.XForwardedProto;
options.KnownIPNetworks.Clear();
options.KnownProxies.Clear();
});
builder.Services.AddApplication();
builder.AddInfrastructure();
builder.Services.AddControllers();
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
builder.Services.AddOpenApi();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
if (recNetOptions.UseForwardedHeaders)
app.UseForwardedHeaders();
// app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapDefaultEndpoints();
app.Run();
}

View File

@@ -1,12 +0,0 @@
namespace API;
public class WeatherForecast
{
public DateOnly Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}

View File

@@ -1,8 +1,25 @@
using Aspire.Hosting;
var builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres");
var postgresPassword = builder.AddParameter("postgres-password", secret: true);
// Infra
var postgres = builder.AddPostgres("postgres")
.WithDataVolume("recnet-postgres-data")
.WithLifetime(ContainerLifetime.Persistent)
.WithPassword(postgresPassword)
.WithHostPort(5432)
.WithPgAdmin();
var db = postgres.AddDatabase("recnet");
// Services
var migrationService = builder.AddProject<Projects.RecNet_MigrationService>("migrationservice")
.WithReference(db)
.WaitFor(db);
builder.AddProject<Projects.API>("api")
.WithReference(db)
.WaitFor(db)
.WaitForCompletion(migrationService);
builder.Build().Run();

View File

@@ -12,4 +12,9 @@
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.4.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\API\API.csproj" />
<ProjectReference Include="..\RecNet.MigrationService\RecNet.MigrationService.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,5 +0,0 @@
namespace RecNet.Application;
public class Class1
{
}

View File

@@ -0,0 +1,12 @@
using AutoMapper;
using RecNet.Application.Profiles;
namespace RecNet.Application.Common.Mapping;
public class ApplicationMappingProfile : Profile
{
public ApplicationMappingProfile()
{
CreateMap<Domain.Entities.Profiles.Profile, ProfileDTO>();
}
}

View File

@@ -0,0 +1,13 @@
using Microsoft.Extensions.DependencyInjection;
namespace RecNet.Application;
public static class DependencyInjection
{
public static IServiceCollection AddApplication(this IServiceCollection services)
{
services.AddAutoMapper(_ => { }, typeof(DependencyInjection).Assembly);
return services;
}
}

View File

@@ -0,0 +1,20 @@
using System.Text.Json.Serialization;
using RecNet.Domain.Common;
namespace RecNet.Application.Profiles;
public class ProfileDTO
{
[JsonPropertyName("ProfileId")]
public Guid ProfileId { get; init; }
[JsonPropertyName("Name")]
public required string Name { get; init; }
[JsonPropertyName("Platform")]
public PlatformType Platform { get; init; }
[JsonPropertyName("CreatedAt")]
public DateTimeOffset CreatedAt { get; init; }
}

View File

@@ -6,4 +6,12 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\RecNet.Domain\RecNet.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="AutoMapper" Version="16.1.1" />
</ItemGroup>
</Project>

View File

@@ -1,5 +0,0 @@
namespace RecNet.Domain;
public class Class1
{
}

View File

@@ -0,0 +1,7 @@
namespace RecNet.Domain.Common;
public enum PlatformType
{
Steamworks = 0,
Meta = 1,
}

View File

@@ -0,0 +1,62 @@
using RecNet.Domain.Common;
using RecNet.Domain.Exceptions;
namespace RecNet.Domain.Entities.Profiles;
public class Profile
{
public const int MinNameLength = 3;
public const int MaxNameLength = 32;
private Profile(
string name,
PlatformType platform,
string platformId)
{
SetName(name);
Platform = platform;
PlatformId = RequireValue(platformId, nameof(platformId));
CreatedAt = DateTimeOffset.UtcNow;
}
public Guid ProfileId { get; } = Guid.NewGuid();
public string Name { get; private set; } = string.Empty;
public PlatformType Platform { get; private set; }
public string PlatformId { get; private set; }
public List<string> DeviceIds { get; private set; } = [];
public DateTimeOffset CreatedAt { get; private set; }
public static Profile Create(
string name,
PlatformType platform,
string platformId)
=> new(name, platform, platformId);
public void SetName(string name)
{
var requiredName = RequireValue(name, nameof(name));
if (requiredName.Length is < MinNameLength or > MaxNameLength)
throw new DomainException($"Username must be between {MinNameLength} and {MaxNameLength} characters long.");
Name = requiredName;
}
public void AddDeviceId(string deviceId)
{
deviceId = RequireValue(deviceId, nameof(deviceId));
if (DeviceIds.Contains(deviceId))
return;
DeviceIds.Add(deviceId);
}
private static string RequireValue(string value, string parameterName)
=> !string.IsNullOrWhiteSpace(value) ? value : throw new DomainException($"{parameterName} is required.");
}

View File

@@ -0,0 +1,15 @@
namespace RecNet.Domain.Exceptions;
public class DomainException : Exception
{
public DomainException()
{ }
public DomainException(string message)
: base(message)
{ }
public DomainException(string message, Exception innerException)
: base(message, innerException)
{ }
}

View File

@@ -6,4 +6,8 @@
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Folder Include="Entities\" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
using RecNet.Domain.Common;
using RecNet.Domain.Entities.Profiles;
namespace RecNet.Domain.Repositories;
public interface IProfileRepository
{
Task<Profile?> GetByIdAsync(Guid profileId, CancellationToken ct = default);
Task<IReadOnlyList<Profile>> GetByIdsAsync(List<Guid> profileIds, CancellationToken ct = default);
Task<Profile?> GetByPlatform(PlatformType platform, string platformId, CancellationToken ct = default);
Task AddAsync(Profile profile, CancellationToken ct = default);
Task SaveChangesAsync(CancellationToken ct = default);
}

View File

@@ -0,0 +1,20 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using RecNet.Domain.Repositories;
using RecNet.Infrastructure.Persistence;
using RecNet.Infrastructure.Persistence.Repositories;
namespace RecNet.Infrastructure;
public static class DependencyInjection
{
public static TBuilder AddInfrastructure<TBuilder>(this TBuilder builder)
where TBuilder : IHostApplicationBuilder
{
builder.AddNpgsqlDbContext<DatabaseContext>("recnet");
builder.Services.AddScoped<IProfileRepository, ProfileRepository>();
return builder;
}
}

View File

@@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using RecNet.Domain.Entities.Profiles;
namespace RecNet.Infrastructure.Persistence.Configurations;
public class ProfileConfiguration : IEntityTypeConfiguration<Profile>
{
public void Configure(EntityTypeBuilder<Profile> builder)
{
builder.HasKey(x => x.ProfileId);
builder.Property(x => x.Name)
.HasMaxLength(Profile.MaxNameLength)
.IsRequired();
builder.Property(x => x.Platform)
.HasConversion<string>()
.HasMaxLength(50)
.IsRequired();
builder.Property(x => x.PlatformId)
.HasMaxLength(50)
.IsRequired();
builder.Property(x => x.DeviceIds)
.HasColumnType("jsonb");
builder.Property(x => x.CreatedAt)
.IsRequired();
builder.HasIndex(x => new { x.Platform, x.PlatformId })
.IsUnique();
}
}

View File

@@ -1,8 +1,14 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using RecNet.Domain.Entities.Profiles;
namespace RecNet.Infrastructure.Persistence;
public class DatabaseContext : DbContext
public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbContext(options)
{
public DbSet<Profile> Profiles => Set<Profile>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfigurationsFromAssembly(typeof(DatabaseContext).Assembly);
}
}

View File

@@ -0,0 +1,66 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using RecNet.Infrastructure.Persistence;
#nullable disable
namespace RecNet.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(DatabaseContext))]
[Migration("20260618180145_Initial")]
partial class Initial
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("RecNet.Domain.Entities.Profiles.Profile", b =>
{
b.Property<Guid>("ProfileId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<string>("DeviceIds")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Platform")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("PlatformId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("ProfileId");
b.HasIndex("Platform", "PlatformId")
.IsUnique();
b.ToTable("Profiles");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace RecNet.Infrastructure.Persistence.Migrations
{
/// <inheritdoc />
public partial class Initial : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Profiles",
columns: table => new
{
ProfileId = table.Column<Guid>(type: "uuid", nullable: false),
Name = table.Column<string>(type: "character varying(32)", maxLength: 32, nullable: false),
Platform = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
PlatformId = table.Column<string>(type: "character varying(50)", maxLength: 50, nullable: false),
DeviceIds = table.Column<string>(type: "jsonb", nullable: false),
CreatedAt = table.Column<DateTimeOffset>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Profiles", x => x.ProfileId);
});
migrationBuilder.CreateIndex(
name: "IX_Profiles_Platform_PlatformId",
table: "Profiles",
columns: new[] { "Platform", "PlatformId" },
unique: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Profiles");
}
}
}

View File

@@ -0,0 +1,63 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
using RecNet.Infrastructure.Persistence;
#nullable disable
namespace RecNet.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(DatabaseContext))]
partial class DatabaseContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.8")
.HasAnnotation("Relational:MaxIdentifierLength", 63);
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("RecNet.Domain.Entities.Profiles.Profile", b =>
{
b.Property<Guid>("ProfileId")
.ValueGeneratedOnAdd()
.HasColumnType("uuid");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("timestamp with time zone");
b.PrimitiveCollection<string>("DeviceIds")
.IsRequired()
.HasColumnType("jsonb");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("character varying(32)");
b.Property<string>("Platform")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.Property<string>("PlatformId")
.IsRequired()
.HasMaxLength(50)
.HasColumnType("character varying(50)");
b.HasKey("ProfileId");
b.HasIndex("Platform", "PlatformId")
.IsUnique();
b.ToTable("Profiles");
});
#pragma warning restore 612, 618
}
}
}

View File

@@ -0,0 +1,37 @@
using Microsoft.EntityFrameworkCore;
using RecNet.Domain.Common;
using RecNet.Domain.Entities.Profiles;
using RecNet.Domain.Repositories;
namespace RecNet.Infrastructure.Persistence.Repositories;
public class ProfileRepository(DatabaseContext dbContext) : IProfileRepository
{
public Task<Profile?> GetByIdAsync(
Guid profileId,
CancellationToken ct = default)
=> dbContext.Profiles
.FirstOrDefaultAsync(x => x.ProfileId == profileId, ct);
public async Task<IReadOnlyList<Profile>> GetByIdsAsync(
List<Guid> profileIds,
CancellationToken ct = default)
=> await dbContext.Profiles
.Where(x => profileIds.Contains(x.ProfileId))
.ToListAsync(ct);
public Task<Profile?> GetByPlatform(PlatformType platform, string platformId, CancellationToken ct = default)
=> dbContext.Profiles
.FirstOrDefaultAsync(x =>
x.Platform == platform &&
x.PlatformId == platformId,
ct);
public async Task AddAsync(
Profile profile,
CancellationToken ct = default)
=> await dbContext.Profiles.AddAsync(profile, ct);
public Task SaveChangesAsync(CancellationToken ct = default)
=> dbContext.SaveChangesAsync(ct);
}

View File

@@ -11,7 +11,12 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Persistence\Configurations\" />
<ProjectReference Include="..\RecNet.Application\RecNet.Application.csproj" />
<ProjectReference Include="..\RecNet.Domain\RecNet.Domain.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Persistence\Migrations\" />
</ItemGroup>
</Project>

View File

@@ -1,7 +1,16 @@
using RecNet.MigrationService;
using RecNet.Infrastructure.Persistence;
using RecNet.ServiceDefaults;
var builder = Host.CreateApplicationBuilder(args);
builder.AddServiceDefaults();
builder.Services.AddHostedService<Worker>();
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing.AddSource(Worker.ActivitySourceName));
builder.AddNpgsqlDbContext<DatabaseContext>("recnet");
var host = builder.Build();
host.Run();

View File

@@ -8,6 +8,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.8"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\RecNet.Infrastructure\RecNet.Infrastructure.csproj" />
<ProjectReference Include="..\RecNet.ServiceDefaults\RecNet.ServiceDefaults.csproj" />
</ItemGroup>
</Project>

View File

@@ -1,17 +1,48 @@
using System.Diagnostics;
using Microsoft.EntityFrameworkCore;
using RecNet.Infrastructure.Persistence;
namespace RecNet.MigrationService;
public class Worker(ILogger<Worker> logger) : BackgroundService
public class Worker(
IServiceProvider serviceProvider,
IHostApplicationLifetime hostApplicationLifetime) : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
public const string ActivitySourceName = "Migrations";
private static readonly ActivitySource ActivitySource = new(ActivitySourceName);
protected override async Task ExecuteAsync(
CancellationToken cancellationToken)
{
while (!stoppingToken.IsCancellationRequested)
using var activity = ActivitySource.StartActivity(
"Migrating database", ActivityKind.Client);
try
{
if (logger.IsEnabled(LogLevel.Information))
using var scope = serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
await RunMigrationAsync(dbContext, cancellationToken);
}
catch (Exception ex)
{
logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
activity?.AddException(ex);
throw;
}
await Task.Delay(1000, stoppingToken);
hostApplicationLifetime.StopApplication();
}
private static async Task RunMigrationAsync(
DatabaseContext dbContext, CancellationToken cancellationToken)
{
var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{
// Run migration in a transaction to avoid partial migration if it fails.
await dbContext.Database.MigrateAsync(cancellationToken);
});
}
}

View File

@@ -2,13 +2,13 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.ServiceDiscovery;
using OpenTelemetry;
using OpenTelemetry.Metrics;
using OpenTelemetry.Trace;
namespace Microsoft.Extensions.Hosting;
namespace RecNet.ServiceDefaults;
// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
// This project should be referenced by each service project in your solution.