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"/>
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.8"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\RecNet.Infrastructure\RecNet.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\RecNet.ServiceDefaults\RecNet.ServiceDefaults.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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;
|
namespace API;
|
||||||
|
|
||||||
public class Program
|
public class Program
|
||||||
@@ -6,26 +12,36 @@ public class Program
|
|||||||
{
|
{
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
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();
|
builder.Services.AddControllers();
|
||||||
// Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi
|
|
||||||
builder.Services.AddOpenApi();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Configure the HTTP request pipeline.
|
if (recNetOptions.UseForwardedHeaders)
|
||||||
if (app.Environment.IsDevelopment())
|
app.UseForwardedHeaders();
|
||||||
{
|
|
||||||
app.MapOpenApi();
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseHttpsRedirection();
|
|
||||||
|
|
||||||
|
// app.UseAuthentication();
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
|
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
app.MapDefaultEndpoints();
|
||||||
|
|
||||||
app.Run();
|
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 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");
|
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();
|
builder.Build().Run();
|
||||||
@@ -12,4 +12,9 @@
|
|||||||
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.4.5" />
|
<PackageReference Include="Aspire.Hosting.PostgreSQL" Version="13.4.5" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\API\API.csproj" />
|
||||||
|
<ProjectReference Include="..\RecNet.MigrationService\RecNet.MigrationService.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\RecNet.Domain\RecNet.Domain.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AutoMapper" Version="16.1.1" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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>
|
<Nullable>enable</Nullable>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Entities\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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;
|
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>
|
||||||
|
|
||||||
<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>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
using RecNet.MigrationService;
|
using RecNet.MigrationService;
|
||||||
|
using RecNet.Infrastructure.Persistence;
|
||||||
|
using RecNet.ServiceDefaults;
|
||||||
|
|
||||||
var builder = Host.CreateApplicationBuilder(args);
|
var builder = Host.CreateApplicationBuilder(args);
|
||||||
|
|
||||||
|
builder.AddServiceDefaults();
|
||||||
builder.Services.AddHostedService<Worker>();
|
builder.Services.AddHostedService<Worker>();
|
||||||
|
|
||||||
|
builder.Services.AddOpenTelemetry()
|
||||||
|
.WithTracing(tracing => tracing.AddSource(Worker.ActivitySourceName));
|
||||||
|
|
||||||
|
builder.AddNpgsqlDbContext<DatabaseContext>("recnet");
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
host.Run();
|
host.Run();
|
||||||
@@ -8,6 +8,15 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<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"/>
|
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.8"/>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\RecNet.Infrastructure\RecNet.Infrastructure.csproj" />
|
||||||
|
<ProjectReference Include="..\RecNet.ServiceDefaults\RecNet.ServiceDefaults.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,17 +1,48 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
|
using RecNet.Infrastructure.Persistence;
|
||||||
|
|
||||||
namespace RecNet.MigrationService;
|
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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,13 +2,13 @@ using Microsoft.AspNetCore.Builder;
|
|||||||
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
using Microsoft.Extensions.Diagnostics.HealthChecks;
|
||||||
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.ServiceDiscovery;
|
|
||||||
using OpenTelemetry;
|
using OpenTelemetry;
|
||||||
using OpenTelemetry.Metrics;
|
using OpenTelemetry.Metrics;
|
||||||
using OpenTelemetry.Trace;
|
using OpenTelemetry.Trace;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.Hosting;
|
namespace RecNet.ServiceDefaults;
|
||||||
|
|
||||||
// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
|
// Adds common Aspire services: service discovery, resilience, health checks, and OpenTelemetry.
|
||||||
// This project should be referenced by each service project in your solution.
|
// This project should be referenced by each service project in your solution.
|
||||||
|
|||||||
Reference in New Issue
Block a user