Add profiles domain, infra, API endpoints
This commit is contained in:
15
.idea/.idea.RecNet/.idea/dataSources.xml
generated
Normal file
15
.idea/.idea.RecNet/.idea/dataSources.xml
generated
Normal 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&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>
|
||||
@@ -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>
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
@API_HostAddress = http://localhost:5155
|
||||
|
||||
GET {{API_HostAddress}}/weatherforecast/
|
||||
Accept: application/json
|
||||
|
||||
###
|
||||
6
API/Configurations/RecNetConfiguration.cs
Normal file
6
API/Configurations/RecNetConfiguration.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace API.Configurations;
|
||||
|
||||
public class RecNetConfiguration
|
||||
{
|
||||
public bool UseForwardedHeaders { get; set; } = true;
|
||||
}
|
||||
13
API/Contracts/Profiles/Requests/LoginRequest.cs
Normal file
13
API/Contracts/Profiles/Requests/LoginRequest.cs
Normal 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; }
|
||||
}
|
||||
8
API/Contracts/Profiles/Responses/LoginResponse.cs
Normal file
8
API/Contracts/Profiles/Responses/LoginResponse.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using RecNet.Application.Profiles;
|
||||
|
||||
namespace API.Contracts.Profiles.Responses;
|
||||
|
||||
public class LoginResponse
|
||||
{
|
||||
public required ProfileDTO Profile { get; set; }
|
||||
}
|
||||
13
API/Controllers/Config/V1/ConfigController.cs
Normal file
13
API/Controllers/Config/V1/ConfigController.cs
Normal 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!");
|
||||
}
|
||||
64
API/Controllers/Profiles/V1/ProfilesController.cs
Normal file
64
API/Controllers/Profiles/V1/ProfilesController.cs
Normal 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)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace RecNet.Application;
|
||||
|
||||
public class Class1
|
||||
{
|
||||
}
|
||||
@@ -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>();
|
||||
}
|
||||
}
|
||||
13
RecNet.Application/DependencyInjection.cs
Normal file
13
RecNet.Application/DependencyInjection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
20
RecNet.Application/Profiles/ProfileDto.cs
Normal file
20
RecNet.Application/Profiles/ProfileDto.cs
Normal 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; }
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
namespace RecNet.Domain;
|
||||
|
||||
public class Class1
|
||||
{
|
||||
}
|
||||
7
RecNet.Domain/Common/PlatformType.cs
Normal file
7
RecNet.Domain/Common/PlatformType.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace RecNet.Domain.Common;
|
||||
|
||||
public enum PlatformType
|
||||
{
|
||||
Steamworks = 0,
|
||||
Meta = 1,
|
||||
}
|
||||
62
RecNet.Domain/Entities/Profiles/Profile.cs
Normal file
62
RecNet.Domain/Entities/Profiles/Profile.cs
Normal 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.");
|
||||
}
|
||||
15
RecNet.Domain/Exceptions/DomainException.cs
Normal file
15
RecNet.Domain/Exceptions/DomainException.cs
Normal 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)
|
||||
{ }
|
||||
}
|
||||
@@ -6,4 +6,8 @@
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Entities\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
16
RecNet.Domain/Repositories/IProfileRepository.cs
Normal file
16
RecNet.Domain/Repositories/IProfileRepository.cs
Normal 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);
|
||||
}
|
||||
20
RecNet.Infrastructure/DependencyInjection.cs
Normal file
20
RecNet.Infrastructure/DependencyInjection.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
66
RecNet.Infrastructure/Persistence/Migrations/20260618180145_Initial.Designer.cs
generated
Normal file
66
RecNet.Infrastructure/Persistence/Migrations/20260618180145_Initial.Designer.cs
generated
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
while (!stoppingToken.IsCancellationRequested)
|
||||
{
|
||||
if (logger.IsEnabled(LogLevel.Information))
|
||||
{
|
||||
logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
|
||||
}
|
||||
public const string ActivitySourceName = "Migrations";
|
||||
private static readonly ActivitySource ActivitySource = new(ActivitySourceName);
|
||||
|
||||
await Task.Delay(1000, stoppingToken);
|
||||
protected override async Task ExecuteAsync(
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
using var activity = ActivitySource.StartActivity(
|
||||
"Migrating database", ActivityKind.Client);
|
||||
|
||||
try
|
||||
{
|
||||
using var scope = serviceProvider.CreateScope();
|
||||
var dbContext = scope.ServiceProvider.GetRequiredService<DatabaseContext>();
|
||||
|
||||
await RunMigrationAsync(dbContext, cancellationToken);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
activity?.AddException(ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user