refactor: restructure solution into -models/-data/-api project taxonomy

Phases 1-10 of the planned refactoring:

Phase 1: rename shared-models -> common
  - namespace Shared.Models -> Common throughout
  - remove stale AspNetCore.Http.Features 5.0 reference

Phase 2: create shared-data with abstract BaseEntity
  - BaseEntity: required string Id { get; init; } + DateTime CreatedAt { get; init; }

Phase 3: rename myai-models -> myai-data
  - namespace MyAi.Models -> MyAi.Data
  - MigrationsAssembly("myai-data")

Phase 4: rename cv-search-models -> cv-search-data
  - namespace CvSearch.Models -> CvSearch.Data
  - move JobSearchSettings to cv-matcher-api-models
  - JobSearch*Entity now inherits BaseEntity

Phase 5: extract rag-data from rag-api
  - new project: Apis/rag-data with RagDbContext + entities + migrations
  - RagDocumentEntity inherits BaseEntity; cache entities use CacheKey PK
  - fix duplicate AddHttpClient<RagAiClient>/AddScoped registrations in rag-api
  - MigrationsAssembly("rag-data")

Phase 6: extract cv-matcher-data from cv-matcher-api
  - new project: Apis/cv-matcher-data with CvMatcherDbContext + entities + migrations
  - CvMatchResultEntity inherits BaseEntity; CvMatcherChatCacheEntity uses CacheKey PK
  - MigrationsAssembly("cv-matcher-data")

Phase 7: create empty cv-cleanup-job-models and cv-search-job-models

Phase 8: update all 5 Dockerfiles for renamed/new projects

Phase 9: reorganise .sln virtual folders (Apis/Jobs/Models/Data/Helpers)
  - update root CLAUDE.md with new project taxonomy and migration commands
  - update cv-matcher-api/CLAUDE.md and cv-search-job/CLAUDE.md

Phase 10: add Directory.Packages.props for centralised NuGet versions
  - remove Version= from all PackageReference elements in active .csproj files

No database changes. No runtime behaviour changes.
All MigrationId strings in __EFMigrationsHistory are unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-27 15:26:03 +03:00
parent 9d8db59825
commit e95ed36647
105 changed files with 1770 additions and 296 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
using Shared.Models.Requests;
using Common.Requests;
namespace Models.Requests
{
+3 -3
View File
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
@@ -8,11 +8,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="5.0.17" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\shared-models\shared-models.csproj" />
<ProjectReference Include="..\common\common.csproj" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -4,7 +4,7 @@ using Microsoft.Extensions.Options;
using Models.Settings;
using Swashbuckle.AspNetCore.Annotations;
using Models.Requests;
using Shared.Models.Responses;
using Common.Responses;
namespace Api.Controllers
{
+1 -1
View File
@@ -7,7 +7,7 @@ using Microsoft.Extensions.Options;
using Models.Settings;
using Models.Requests;
using Swashbuckle.AspNetCore.Annotations;
using Shared.Models.Responses;
using Common.Responses;
namespace Api.Controllers
{
+2 -2
View File
@@ -9,8 +9,8 @@ using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.RateLimiting;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.Annotations;
using Shared.Models.Responses;
using MyAi.Models.Services;
using Common.Responses;
using MyAi.Data.Services;
namespace Api.Controllers;
@@ -6,7 +6,7 @@ using Microsoft.AspNetCore.StaticFiles;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Swashbuckle.AspNetCore.Annotations;
using Shared.Models.Responses;
using Common.Responses;
namespace Api.Controllers
{
+7 -5
View File
@@ -3,19 +3,21 @@ ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY Apis/api/api.csproj Apis/api/
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
COPY Apis/common/common.csproj Apis/common/
COPY Apis/api-models/api-models.csproj Apis/api-models/
COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/
COPY Apis/myai-models/myai-models.csproj Apis/myai-models/
COPY Apis/myai-data/myai-data.csproj Apis/myai-data/
COPY Apis/shared-data/shared-data.csproj Apis/shared-data/
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
RUN dotnet restore Apis/api/api.csproj
COPY Apis/api/ Apis/api/
COPY Apis/shared-models/ Apis/shared-models/
COPY Apis/common/ Apis/common/
COPY Apis/api-models/ Apis/api-models/
COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/
COPY Apis/myai-models/ Apis/myai-models/
COPY Apis/myai-data/ Apis/myai-data/
COPY Apis/shared-data/ Apis/shared-data/
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
RUN dotnet publish Apis/api/api.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
@@ -27,4 +29,4 @@ ENV ASPNETCORE_URLS=http://0.0.0.0:8080
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "api.dll"]
ENTRYPOINT ["dotnet", "api.dll"]
+4 -4
View File
@@ -3,11 +3,11 @@ using Api.Services;
using Api.Services.Contracts;
using Microsoft.EntityFrameworkCore;
using Models.Settings;
using MyAi.Models.Data;
using MyAi.Models.Services;
using MyAi.Data;
using MyAi.Data.Services;
using Refit;
using Serilog;
using Shared.Models.Settings;
using Common.Settings;
using StartupHelpers;
StartupExtensions.LoadDotEnvFile();
@@ -39,7 +39,7 @@ try
var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration);
options.UseSqlServer(connectionString, sql =>
{
sql.MigrationsAssembly("myai-models");
sql.MigrationsAssembly("myai-data");
sql.MigrationsHistoryTable(MyAiDbContext.MigrationTableName, MyAiDbContext.SchemaName);
});
});
+1 -1
View File
@@ -6,7 +6,7 @@ using MimeKit;
using Models.Settings;
using Models.Requests;
using CvMatcher.Models.Responses;
using MyAi.Models.Services;
using MyAi.Data.Services;
namespace Api.Services
{
+14 -14
View File
@@ -16,18 +16,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.5.1" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.2.0" />
<PackageReference Include="MailKit" Version="4.16.0" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Email" Version="4.2.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.7" />
<PackageReference Include="Refit.HttpClientFactory" Version="10.1.6" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="MailKit" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Enrichers.Environment" />
<PackageReference Include="Serilog.Sinks.Email" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
<PackageReference Include="Refit.HttpClientFactory" />
</ItemGroup>
<ItemGroup>
@@ -37,9 +37,9 @@
<ItemGroup>
<ProjectReference Include="..\api-models\api-models.csproj" />
<ProjectReference Include="..\cv-matcher-api-models\cv-matcher-api-models.csproj" />
<ProjectReference Include="..\shared-models\shared-models.csproj" />
<ProjectReference Include="..\common\common.csproj" />
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
<ProjectReference Include="..\myai-models\myai-models.csproj" />
<ProjectReference Include="..\myai-data\myai-data.csproj" />
</ItemGroup>
</Project>
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Http;
using System.ComponentModel.DataAnnotations;
namespace Shared.Models.Requests
namespace Common.Requests
{
public class UploadFileRequest
{
@@ -1,4 +1,4 @@
namespace Shared.Models.Responses;
namespace Common.Responses;
public sealed class ErrorResponse
{
@@ -1,4 +1,4 @@
namespace Shared.Models.Settings
namespace Common.Settings
{
public class AiSettings
{
@@ -1,4 +1,4 @@
namespace Shared.Models.Settings
namespace Common.Settings
{
public class DatabaseSettings
{
@@ -1,4 +1,4 @@
namespace Shared.Models.Settings
namespace Common.Settings
{
public class InternalApiSettings
{
@@ -1,4 +1,4 @@
namespace Shared.Models.Settings
namespace Common.Settings
{
public class OllamaSettings
{
@@ -1,4 +1,4 @@
namespace Shared.Models.Settings
namespace Common.Settings
{
public class OpenAiSettings
{
@@ -1,4 +1,4 @@
namespace Shared.Models.Settings
namespace Common.Settings
{
public class RateLimitingSettings
{
@@ -1,14 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>Shared.Models</RootNamespace>
<AssemblyName>common</AssemblyName>
<RootNamespace>Common</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Http.Features" Version="5.0.17" />
<FrameworkReference Include="Microsoft.AspNetCore.App" />
</ItemGroup>
</Project>
@@ -1,8 +1,8 @@
using Shared.Models.Settings;
using Common.Settings;
namespace CvMatcher.Models.Settings;
public sealed class AiSettings : Shared.Models.Settings.AiSettings
public sealed class AiSettings : Common.Settings.AiSettings
{
public OpenAiSettings OpenAI { get; set; } = new();
public OllamaSettings Ollama { get; set; } = new();
@@ -0,0 +1,21 @@
namespace CvMatcher.Models.Settings;
public sealed class JobSearchSettings
{
public bool Enabled { get; set; } = true;
public string JobSearchLinkBaseUrl { get; set; } = string.Empty;
public int TokenExpiryDays { get; set; } = 7;
public int MinMatchScore { get; set; } = 15;
public int MaxJobsToMatch { get; set; } = 15;
public List<JobProviderConfig> Providers { get; set; } = [];
}
public sealed class JobProviderConfig
{
public string Name { get; set; } = string.Empty;
public bool Enabled { get; set; } = true;
public string SearchUrlTemplate { get; set; } = string.Empty;
public string JobLinkContains { get; set; } = string.Empty;
public List<string> InitialKeywords { get; set; } = [];
public int MaxResults { get; set; } = 20;
}
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\shared-models\shared-models.csproj" />
<ProjectReference Include="..\common\common.csproj" />
</ItemGroup>
</Project>
+2 -2
View File
@@ -36,8 +36,8 @@ Default model: `gpt-4o-mini`. Timeout: 90 s.
Both contexts use the same SQL Server connection string (from `Database:*` settings).
- `CvMatcherDbContext` — schema `cvMatcher`; migrations in `cv-matcher-api` assembly
- `CvSearchDbContext` — schema `cvSearch`; migrations in `cv-search-models` assembly (MigrationsAssembly = "cv-search-models")
- `CvMatcherDbContext` — schema `cvMatcher`; migrations in `cv-matcher-data` assembly (`Apis/cv-matcher-data/`)
- `CvSearchDbContext` — schema `cvSearch`; migrations in `cv-search-data` assembly (`Apis/cv-search-data/`)
## Keyword extraction (JobTokenService.ExtractKeywords)
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Options;
using CvMatcher.Models.Settings;
using Api.Data.Repositories.Contracts;
using CvMatcher.Data.Repositories.Contracts;
using Api.Clients.Ai.Contracts;
using CommonHelpers;
@@ -3,7 +3,7 @@ using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Api.Clients.Ai.Contracts;
using Api.Data.Repositories.Contracts;
using CvMatcher.Data.Repositories.Contracts;
using CommonHelpers;
using CvMatcher.Models.Settings;
using Microsoft.Extensions.Options;
@@ -2,9 +2,9 @@ using CvMatcher.Models.Requests;
using Api.Services.Contracts;
using Microsoft.AspNetCore.Mvc;
using CvMatcher.Models.Responses;
using Shared.Models.Requests;
using Common.Requests;
using Swashbuckle.AspNetCore.Annotations;
using Shared.Models.Responses;
using Common.Responses;
namespace Api.Controllers;
@@ -2,7 +2,7 @@ using Api.Services.Contracts;
using CvMatcher.Models.Requests;
using CvMatcher.Models.Responses;
using Microsoft.AspNetCore.Mvc;
using Shared.Models.Responses;
using Common.Responses;
using Swashbuckle.AspNetCore.Annotations;
namespace Api.Controllers;
@@ -1,6 +1,6 @@
using CvMatcher.Models.Responses;
namespace Api.Data.Repositories.Contracts;
namespace CvMatcher.Data.Repositories.Contracts;
public interface IMatcherRepository
{
@@ -1,11 +1,11 @@
using System.Text.Json;
using Api.Data;
using Api.Data.Entities;
using Api.Data.Repositories.Contracts;
using CvMatcher.Data;
using CvMatcher.Data.Entities;
using CvMatcher.Data.Repositories.Contracts;
using CvMatcher.Models.Responses;
using Microsoft.EntityFrameworkCore;
namespace Api.Data.Repositories;
namespace CvMatcher.Data.Repositories;
public sealed class EfMatcherRepository : IMatcherRepository
{
+11 -7
View File
@@ -3,20 +3,24 @@ ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY Apis/cv-matcher-api/cv-matcher-api.csproj Apis/cv-matcher-api/
COPY Apis/cv-search-models/cv-search-models.csproj Apis/cv-search-models/
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
COPY Apis/cv-search-data/cv-search-data.csproj Apis/cv-search-data/
COPY Apis/cv-matcher-data/cv-matcher-data.csproj Apis/cv-matcher-data/
COPY Apis/common/common.csproj Apis/common/
COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/
COPY Apis/myai-models/myai-models.csproj Apis/myai-models/
COPY Apis/myai-data/myai-data.csproj Apis/myai-data/
COPY Apis/shared-data/shared-data.csproj Apis/shared-data/
COPY Helpers/common-helpers/common-helpers.csproj Helpers/common-helpers/
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
RUN dotnet restore Apis/cv-matcher-api/cv-matcher-api.csproj
COPY Apis/cv-matcher-api/ Apis/cv-matcher-api/
COPY Apis/cv-search-models/ Apis/cv-search-models/
COPY Apis/shared-models/ Apis/shared-models/
COPY Apis/cv-search-data/ Apis/cv-search-data/
COPY Apis/cv-matcher-data/ Apis/cv-matcher-data/
COPY Apis/common/ Apis/common/
COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/
COPY Apis/myai-models/ Apis/myai-models/
COPY Apis/myai-data/ Apis/myai-data/
COPY Apis/shared-data/ Apis/shared-data/
COPY Helpers/common-helpers/ Helpers/common-helpers/
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
@@ -29,4 +33,4 @@ ENV ASPNETCORE_URLS=http://0.0.0.0:8080
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "cv-matcher-api.dll"]
ENTRYPOINT ["dotnet", "cv-matcher-api.dll"]
+10 -10
View File
@@ -2,20 +2,19 @@ using Api.Clients.Ai;
using Api.Clients.Ai.Contracts;
using Api.Clients.Api;
using Api.Clients.Api.Contracts;
using Api.Data;
using Api.Data.Repositories;
using Api.Data.Repositories.Contracts;
using CvMatcher.Data;
using CvMatcher.Data.Repositories;
using CvMatcher.Data.Repositories.Contracts;
using Api.Services;
using Api.Services.Contracts;
using CvMatcher.Models.Settings;
using CvSearch.Models.Data;
using CvSearch.Models.Settings;
using CvSearch.Data;
using Microsoft.EntityFrameworkCore;
using MyAi.Models.Data;
using MyAi.Models.Services;
using MyAi.Data;
using MyAi.Data.Services;
using Refit;
using Serilog;
using Shared.Models.Settings;
using Common.Settings;
using StartupHelpers;
using System.Reflection;
@@ -63,6 +62,7 @@ try
options.UseSqlServer(connectionString, sql =>
{
sql.MigrationsHistoryTable(CvMatcherDbContext.MigrationTableName, CvMatcherDbContext.SchemaName);
sql.MigrationsAssembly("cv-matcher-data");
});
});
@@ -71,7 +71,7 @@ try
var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration);
options.UseSqlServer(connectionString, sql =>
{
sql.MigrationsAssembly("cv-search-models");
sql.MigrationsAssembly("cv-search-data");
sql.MigrationsHistoryTable(CvSearchDbContext.MigrationTableName, CvSearchDbContext.SchemaName);
});
});
@@ -81,7 +81,7 @@ try
var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration);
options.UseSqlServer(connectionString, sql =>
{
sql.MigrationsAssembly("myai-models");
sql.MigrationsAssembly("myai-data");
sql.MigrationsHistoryTable(MyAiDbContext.MigrationTableName, MyAiDbContext.SchemaName);
});
});
@@ -1,13 +1,13 @@
using System.Text.Json;
using Api.Clients.Ai.Contracts;
using Api.Clients.Api.Contracts;
using Api.Data.Repositories.Contracts;
using CvMatcher.Data.Repositories.Contracts;
using CvMatcher.Models.Requests;
using CvMatcher.Models.Responses;
using CvMatcher.Models.Settings;
using Api.Services.Contracts;
using Microsoft.Extensions.Options;
using MyAi.Models.Services;
using MyAi.Data.Services;
namespace Api.Services;
@@ -3,9 +3,9 @@ using System.Text.RegularExpressions;
using Api.Clients.Api.Contracts;
using Api.Services.Contracts;
using CvMatcher.Models.Responses;
using CvSearch.Models.Data;
using CvSearch.Models.Data.Entities;
using CvSearch.Models.Settings;
using CvSearch.Data;
using CvSearch.Data.Entities;
using CvMatcher.Models.Settings;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
+19 -18
View File
@@ -58,30 +58,31 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.5.1" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="MailKit" Version="4.16.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.Email" Version="4.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.7" />
<PackageReference Include="Refit.HttpClientFactory" Version="10.1.6" />
<PackageReference Include="MailKit" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Enrichers.Environment" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.Email" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
<PackageReference Include="Refit.HttpClientFactory" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Helpers\common-helpers\common-helpers.csproj" />
<ProjectReference Include="..\cv-matcher-api-models\cv-matcher-api-models.csproj" />
<ProjectReference Include="..\cv-search-models\cv-search-models.csproj" />
<ProjectReference Include="..\shared-models\shared-models.csproj" />
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
<ProjectReference Include="..\myai-models\myai-models.csproj" />
</ItemGroup>
<ProjectReference Include="..\cv-search-data\cv-search-data.csproj" />
<ProjectReference Include="..\cv-matcher-data\cv-matcher-data.csproj" />
<ProjectReference Include="..\common\common.csproj" />
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
<ProjectReference Include="..\myai-data\myai-data.csproj" />
</ItemGroup>
</Project>
@@ -1,8 +1,7 @@
using Api.Data.Entities;
using CvMatcher.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace Api.Data;
namespace CvMatcher.Data;
public sealed class CvMatcherDbContext : DbContext
{
@@ -1,12 +1,12 @@
namespace Api.Data.Entities;
using Shared.Data.Entities;
public sealed class CvMatchResultEntity
namespace CvMatcher.Data.Entities;
public sealed class CvMatchResultEntity : BaseEntity
{
public string Id { get; set; } = string.Empty;
public string CvDocumentId { get; set; } = string.Empty;
public string JobDocumentId { get; set; } = string.Empty;
public string Language { get; set; } = "en";
public string ResultJson { get; set; } = string.Empty;
public int Score { get; set; }
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
@@ -1,5 +1,6 @@
namespace Api.Data.Entities;
namespace CvMatcher.Data.Entities;
// CacheKey PK — BaseEntity not applicable
public sealed class CvMatcherChatCacheEntity
{
public string CacheKey { get; set; } = string.Empty;
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Api.Data;
using CvMatcher.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Api.Migrations
namespace CvMatcher.Data.Migrations
{
[DbContext(typeof(CvMatcherDbContext))]
[Migration("20260507140442_InitialCvMatcherSchema")]
@@ -26,7 +26,7 @@ namespace Api.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Api.Data.Entities.CvMatchResultEntity", b =>
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
@@ -62,7 +62,7 @@ namespace Api.Migrations
b.ToTable("Results", "cvMatcher");
});
modelBuilder.Entity("Api.Data.Entities.CvMatcherChatCacheEntity", b =>
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatcherChatCacheEntity", b =>
{
b.Property<string>("CacheKey")
.HasMaxLength(64)
@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Api.Migrations
namespace CvMatcher.Data.Migrations
{
/// <inheritdoc />
public partial class InitialCvMatcherSchema : Migration
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Api.Data;
using CvMatcher.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Api.Migrations
namespace CvMatcher.Data.Migrations
{
[DbContext(typeof(CvMatcherDbContext))]
[Migration("20260524140335_AddLanguageToCvMatchResult")]
@@ -26,7 +26,7 @@ namespace Api.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Api.Data.Entities.CvMatchResultEntity", b =>
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
@@ -66,7 +66,7 @@ namespace Api.Migrations
b.ToTable("Results", "cvMatcher");
});
modelBuilder.Entity("Api.Data.Entities.CvMatcherChatCacheEntity", b =>
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatcherChatCacheEntity", b =>
{
b.Property<string>("CacheKey")
.HasMaxLength(64)
@@ -1,8 +1,8 @@
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Api.Migrations
namespace CvMatcher.Data.Migrations
{
/// <inheritdoc />
public partial class AddLanguageToCvMatchResult : Migration
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Api.Data;
using CvMatcher.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Api.Migrations
namespace CvMatcher.Data.Migrations
{
[DbContext(typeof(CvMatcherDbContext))]
partial class CvMatcherDbContextModelSnapshot : ModelSnapshot
@@ -23,7 +23,7 @@ namespace Api.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Api.Data.Entities.CvMatchResultEntity", b =>
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatchResultEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
@@ -63,7 +63,7 @@ namespace Api.Migrations
b.ToTable("Results", "cvMatcher");
});
modelBuilder.Entity("Api.Data.Entities.CvMatcherChatCacheEntity", b =>
modelBuilder.Entity("CvMatcher.Data.Entities.CvMatcherChatCacheEntity", b =>
{
b.Property<string>("CacheKey")
.HasMaxLength(64)
@@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>cv-matcher-data</AssemblyName>
<RootNamespace>CvMatcher.Data</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\shared-data\shared-data.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,62 @@
using CvSearch.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace CvSearch.Data;
public sealed class CvSearchDbContext : DbContext
{
public const string SchemaName = "cvSearch";
public const string MigrationTableName = "_Migrations";
public CvSearchDbContext(DbContextOptions<CvSearchDbContext> options) : base(options) { }
public DbSet<JobSearchTokenEntity> JobSearchTokens => Set<JobSearchTokenEntity>();
public DbSet<JobSearchSessionEntity> JobSearchSessions => Set<JobSearchSessionEntity>();
public DbSet<JobSearchResultEntity> JobSearchResults => Set<JobSearchResultEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema(SchemaName);
modelBuilder.Entity<JobSearchTokenEntity>(entity =>
{
entity.ToTable("JobSearchTokens");
entity.HasKey(x => x.Id);
entity.Property(x => x.Id).HasMaxLength(64);
entity.Property(x => x.CvDocumentId).HasMaxLength(64).IsRequired();
entity.Property(x => x.Email).HasMaxLength(256).IsRequired();
entity.Property(x => x.Language).HasMaxLength(8).HasDefaultValue("en").IsRequired();
entity.Property(x => x.Used).HasDefaultValue(false);
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
});
modelBuilder.Entity<JobSearchSessionEntity>(entity =>
{
entity.ToTable("JobSearchSessions");
entity.HasKey(x => x.Id);
entity.Property(x => x.Id).HasMaxLength(64);
entity.Property(x => x.TokenId).HasMaxLength(64).IsRequired();
entity.Property(x => x.CvDocumentId).HasMaxLength(64).IsRequired();
entity.Property(x => x.Email).HasMaxLength(256).IsRequired();
entity.Property(x => x.Status).HasMaxLength(32).IsRequired();
entity.Property(x => x.Keywords).HasMaxLength(1000);
entity.Property(x => x.ProviderConfigJson).IsRequired(false);
entity.Property(x => x.Language).HasMaxLength(8).HasDefaultValue("en").IsRequired();
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
entity.HasIndex(x => x.Status);
});
modelBuilder.Entity<JobSearchResultEntity>(entity =>
{
entity.ToTable("JobSearchResults");
entity.HasKey(x => x.Id);
entity.Property(x => x.Id).HasMaxLength(64);
entity.Property(x => x.SessionId).HasMaxLength(64).IsRequired();
entity.Property(x => x.ProviderName).HasMaxLength(128);
entity.Property(x => x.JobUrl).HasMaxLength(2048);
entity.Property(x => x.JobTitle).HasMaxLength(512);
entity.Property(x => x.CreatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
entity.HasIndex(x => x.SessionId);
});
}
}
@@ -0,0 +1,14 @@
using Shared.Data.Entities;
namespace CvSearch.Data.Entities;
public sealed class JobSearchResultEntity : BaseEntity
{
public string SessionId { get; set; } = string.Empty;
public string ProviderName { get; set; } = string.Empty;
public string JobUrl { get; set; } = string.Empty;
public string JobTitle { get; set; } = string.Empty;
public string JobText { get; set; } = string.Empty;
public int Score { get; set; }
public string ResultJson { get; set; } = string.Empty;
}
@@ -0,0 +1,22 @@
using Shared.Data.Entities;
namespace CvSearch.Data.Entities;
public sealed class JobSearchSessionEntity : BaseEntity
{
public string TokenId { get; set; } = string.Empty;
public string CvDocumentId { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Status { get; set; } = JobSearchStatus.Pending;
public string Keywords { get; set; } = string.Empty;
public string? ProviderConfigJson { get; set; }
public string Language { get; set; } = "en";
}
public static class JobSearchStatus
{
public const string Pending = "Pending";
public const string Processing = "Processing";
public const string Done = "Done";
public const string Failed = "Failed";
}
@@ -0,0 +1,12 @@
using Shared.Data.Entities;
namespace CvSearch.Data.Entities;
public sealed class JobSearchTokenEntity : BaseEntity
{
public string CvDocumentId { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public string Language { get; set; } = "en";
public DateTime ExpiresAt { get; set; }
public bool Used { get; set; }
}
@@ -0,0 +1,160 @@
// <auto-generated />
using System;
using CvSearch.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CvSearch.Data.Migrations
{
[DbContext(typeof(CvSearchDbContext))]
[Migration("20260522093356_AddJobSearchTables")]
partial class AddJobSearchTables
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("cvSearch")
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchResultEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("JobText")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("JobTitle")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("JobUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<string>("ProviderName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ResultJson")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Score")
.HasColumnType("int");
b.Property<string>("SessionId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("SessionId");
b.ToTable("JobSearchResults", "cvSearch");
});
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchSessionEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("CvDocumentId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Keywords")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("ProviderConfigJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("Status");
b.ToTable("JobSearchSessions", "cvSearch");
});
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchTokenEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("CvDocumentId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime2");
b.Property<bool>("Used")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.HasKey("Id");
b.ToTable("JobSearchTokens", "cvSearch");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,102 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CvSearch.Data.Migrations
{
/// <inheritdoc />
public partial class AddJobSearchTables : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "cvSearch");
migrationBuilder.CreateTable(
name: "JobSearchResults",
schema: "cvSearch",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
SessionId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
ProviderName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
JobUrl = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: false),
JobTitle = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
JobText = table.Column<string>(type: "nvarchar(max)", nullable: false),
Score = table.Column<int>(type: "int", nullable: false),
ResultJson = table.Column<string>(type: "nvarchar(max)", nullable: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "SYSUTCDATETIME()")
},
constraints: table =>
{
table.PrimaryKey("PK_JobSearchResults", x => x.Id);
});
migrationBuilder.CreateTable(
name: "JobSearchSessions",
schema: "cvSearch",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
TokenId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
CvDocumentId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
Status = table.Column<string>(type: "nvarchar(32)", maxLength: 32, nullable: false),
Keywords = table.Column<string>(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
ProviderConfigJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "SYSUTCDATETIME()")
},
constraints: table =>
{
table.PrimaryKey("PK_JobSearchSessions", x => x.Id);
});
migrationBuilder.CreateTable(
name: "JobSearchTokens",
schema: "cvSearch",
columns: table => new
{
Id = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
CvDocumentId = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
Email = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
ExpiresAt = table.Column<DateTime>(type: "datetime2", nullable: false),
Used = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "SYSUTCDATETIME()")
},
constraints: table =>
{
table.PrimaryKey("PK_JobSearchTokens", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_JobSearchResults_SessionId",
schema: "cvSearch",
table: "JobSearchResults",
column: "SessionId");
migrationBuilder.CreateIndex(
name: "IX_JobSearchSessions_Status",
schema: "cvSearch",
table: "JobSearchSessions",
column: "Status");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "JobSearchResults",
schema: "cvSearch");
migrationBuilder.DropTable(
name: "JobSearchSessions",
schema: "cvSearch");
migrationBuilder.DropTable(
name: "JobSearchTokens",
schema: "cvSearch");
}
}
}
@@ -0,0 +1,174 @@
// <auto-generated />
using System;
using CvSearch.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CvSearch.Data.Migrations
{
[DbContext(typeof(CvSearchDbContext))]
[Migration("20260524145702_AddLanguageToJobSearchEntities")]
partial class AddLanguageToJobSearchEntities
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("cvSearch")
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchResultEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("JobText")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("JobTitle")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("JobUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<string>("ProviderName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ResultJson")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Score")
.HasColumnType("int");
b.Property<string>("SessionId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("SessionId");
b.ToTable("JobSearchResults", "cvSearch");
});
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchSessionEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("CvDocumentId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Keywords")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("Language")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(8)
.HasColumnType("nvarchar(8)")
.HasDefaultValue("en");
b.Property<string>("ProviderConfigJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("Status");
b.ToTable("JobSearchSessions", "cvSearch");
});
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchTokenEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("CvDocumentId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime2");
b.Property<string>("Language")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(8)
.HasColumnType("nvarchar(8)")
.HasDefaultValue("en");
b.Property<bool>("Used")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.HasKey("Id");
b.ToTable("JobSearchTokens", "cvSearch");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,46 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace CvSearch.Data.Migrations
{
/// <inheritdoc />
public partial class AddLanguageToJobSearchEntities : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Language",
schema: "cvSearch",
table: "JobSearchTokens",
type: "nvarchar(8)",
maxLength: 8,
nullable: false,
defaultValue: "en");
migrationBuilder.AddColumn<string>(
name: "Language",
schema: "cvSearch",
table: "JobSearchSessions",
type: "nvarchar(8)",
maxLength: 8,
nullable: false,
defaultValue: "en");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Language",
schema: "cvSearch",
table: "JobSearchTokens");
migrationBuilder.DropColumn(
name: "Language",
schema: "cvSearch",
table: "JobSearchSessions");
}
}
}
@@ -0,0 +1,171 @@
// <auto-generated />
using System;
using CvSearch.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace CvSearch.Data.Migrations
{
[DbContext(typeof(CvSearchDbContext))]
partial class CvSearchDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("cvSearch")
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchResultEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("JobText")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("JobTitle")
.IsRequired()
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("JobUrl")
.IsRequired()
.HasMaxLength(2048)
.HasColumnType("nvarchar(2048)");
b.Property<string>("ProviderName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ResultJson")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<int>("Score")
.HasColumnType("int");
b.Property<string>("SessionId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("SessionId");
b.ToTable("JobSearchResults", "cvSearch");
});
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchSessionEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("CvDocumentId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("Keywords")
.IsRequired()
.HasMaxLength(1000)
.HasColumnType("nvarchar(1000)");
b.Property<string>("Language")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(8)
.HasColumnType("nvarchar(8)")
.HasDefaultValue("en");
b.Property<string>("ProviderConfigJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(32)
.HasColumnType("nvarchar(32)");
b.Property<string>("TokenId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("Status");
b.ToTable("JobSearchSessions", "cvSearch");
});
modelBuilder.Entity("CvSearch.Data.Entities.JobSearchTokenEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<DateTime>("CreatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("CvDocumentId")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Email")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<DateTime>("ExpiresAt")
.HasColumnType("datetime2");
b.Property<string>("Language")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(8)
.HasColumnType("nvarchar(8)")
.HasDefaultValue("en");
b.Property<bool>("Used")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.HasKey("Id");
b.ToTable("JobSearchTokens", "cvSearch");
});
#pragma warning restore 612, 618
}
}
}
+23
View File
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>cv-search-data</AssemblyName>
<RootNamespace>CvSearch.Data</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\shared-data\shared-data.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>CvSearch.Models</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -0,0 +1,11 @@
namespace MyAi.Data.Entities;
// composite PK (Key + Language) — BaseEntity not applicable
public sealed class TemplateEntity
{
public string Key { get; set; } = string.Empty;
public string Language { get; set; } = string.Empty;
public string Value { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
public DateTime UpdatedAt { get; set; }
}
+30
View File
@@ -0,0 +1,30 @@
using MyAi.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace MyAi.Data;
public sealed class MyAiDbContext : DbContext
{
public const string SchemaName = "myAi";
public const string MigrationTableName = "_MyAiMigrations";
public MyAiDbContext(DbContextOptions<MyAiDbContext> options) : base(options) { }
public DbSet<TemplateEntity> Templates => Set<TemplateEntity>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.HasDefaultSchema(SchemaName);
modelBuilder.Entity<TemplateEntity>(entity =>
{
entity.ToTable("Templates");
entity.HasKey(x => new { x.Key, x.Language });
entity.Property(x => x.Key).HasMaxLength(128);
entity.Property(x => x.Language).HasMaxLength(8);
entity.Property(x => x.Value).IsRequired();
entity.Property(x => x.Description).HasMaxLength(500).HasDefaultValue(string.Empty);
entity.Property(x => x.UpdatedAt).HasDefaultValueSql("SYSUTCDATETIME()");
});
}
}
@@ -0,0 +1,62 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MyAi.Data;
#nullable disable
namespace MyAi.Data.Migrations
{
[DbContext(typeof(MyAiDbContext))]
[Migration("20260524145351_AddTemplates")]
partial class AddTemplates
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("myAi")
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("MyAi.Data.Entities.TemplateEntity", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Language")
.HasMaxLength(8)
.HasColumnType("nvarchar(8)");
b.Property<string>("Description")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)")
.HasDefaultValue("");
b.Property<DateTime>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Key", "Language");
b.ToTable("Templates", "myAi");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,113 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace MyAi.Data.Migrations
{
/// <inheritdoc />
public partial class AddTemplates : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "myAi");
migrationBuilder.CreateTable(
name: "Templates",
schema: "myAi",
columns: table => new
{
Key = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Language = table.Column<string>(type: "nvarchar(8)", maxLength: 8, nullable: false),
Value = table.Column<string>(type: "nvarchar(max)", nullable: false),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: false, defaultValue: ""),
UpdatedAt = table.Column<DateTime>(type: "datetime2", nullable: false, defaultValueSql: "SYSUTCDATETIME()")
},
constraints: table =>
{
table.PrimaryKey("PK_Templates", x => new { x.Key, x.Language });
});
Seed(migrationBuilder);
}
private static void Seed(MigrationBuilder m)
{
void Row(string key, string lang, string value, string description = "")
=> m.InsertData("Templates", ["Key", "Language", "Value", "Description"], [key, lang, value, description], "myAi");
// Match result email — subject
Row("email.match.subject", "en", "MyAi.ro CV Match: {{score}}% - {{jobLabel}}", "Subject for the CV match result email");
Row("email.match.subject", "ro", "MyAi.ro Potrivire CV: {{score}}% - {{jobLabel}}", "Subiect email rezultat potrivire CV");
// Match result email — body
Row("email.match.body", "en",
"CV Matcher result\n\nCV Document ID: {{cvDocumentId}}\nJob: {{jobLabel}}\nJob URL: {{jobUrl}}\nScore: {{score}}%\n\nSummary:\n{{summary}}\n\nStrengths:\n{{strengths}}\n\nGaps:\n{{gaps}}\n\nRecommendations:\n{{recommendations}}",
"Body for the CV match result email");
Row("email.match.body", "ro",
"Rezultat potrivire CV\n\nID document CV: {{cvDocumentId}}\nJob: {{jobLabel}}\nURL job: {{jobUrl}}\nScor: {{score}}%\n\nRezumat:\n{{summary}}\n\nPuncte forte:\n{{strengths}}\n\nLipsuri:\n{{gaps}}\n\nRecomandări:\n{{recommendations}}",
"Corpul emailului pentru rezultatul potrivirii CV");
// Match result email — job search CTA footer
Row("email.match.job-search-footer", "en",
"\n\n---\nWant to find more jobs matching your CV?\nClick: {{jobSearchLink}}\n(link valid for {{expiryDays}} days)",
"Job search CTA appended to match result email");
Row("email.match.job-search-footer", "ro",
"\n\n---\nVrei sa gasesti mai multe joburi potrivite CV-ului tau?\nClick: {{jobSearchLink}}\n(link valabil {{expiryDays}} zile)",
"CTA cautare joburi adaugat la emailul de potrivire CV");
// Job search results email — subject
Row("email.search-results.subject", "en", "MyAi.ro: {{count}} jobs matching your CV", "Subject for job search results email");
Row("email.search-results.subject", "ro", "MyAi.ro: {{count}} joburi potrivite CV-ului tau", "Subiect email rezultate cautare joburi");
// Job search results email — body preamble (items appended in code)
Row("email.search-results.body", "en", "MyAi.ro found {{count}} jobs matching your CV:\n\n{{items}}", "Body preamble for job search results email");
Row("email.search-results.body", "ro", "MyAi.ro a gasit {{count}} joburi potrivite CV-ului tau:\n\n{{items}}", "Corpul emailului de rezultate cautare joburi");
// Job search results email — no results found
Row("email.search-results.empty", "en", "MyAi.ro found no jobs matching your CV. Try again later or update your CV.", "No results message for job search results email");
Row("email.search-results.empty", "ro", "MyAi.ro nu a gasit joburi care sa corespunda CV-ului tau. Incercati mai tarziu sau ajustati CV-ul.", "Mesaj fara rezultate pentru emailul de cautare joburi");
// HTML job-search start page messages
Row("html.job-search.started.title", "en", "Job search started", "Title for job search started page");
Row("html.job-search.started.message", "en", "Your job search has started. Results will be sent to your email shortly.", "Message for job search started page");
Row("html.job-search.started.title", "ro", "Căutare joburi pornită", "Titlu pagina cautare joburi pornita");
Row("html.job-search.started.message", "ro", "Căutarea joburilor a început. Rezultatele vor fi trimise pe email în scurt timp.", "Mesaj pagina cautare joburi pornita");
Row("html.job-search.already-used.title", "en", "Link already used", "Title for already-used page");
Row("html.job-search.already-used.message", "en", "This job search link has already been used.", "Message for already-used page");
Row("html.job-search.already-used.title", "ro", "Link deja folosit", "Titlu pagina link deja folosit");
Row("html.job-search.already-used.message", "ro", "Acest link de cautare joburi a fost deja folosit.", "Mesaj pagina link deja folosit");
Row("html.job-search.expired.title", "en", "Link expired", "Title for expired link page");
Row("html.job-search.expired.message", "en", "This job search link has expired. Please request a new CV match to get a fresh link.", "Message for expired link page");
Row("html.job-search.expired.title", "ro", "Link expirat", "Titlu pagina link expirat");
Row("html.job-search.expired.message", "ro", "Acest link de cautare joburi a expirat. Solicita o noua potrivire CV pentru a primi un link nou.", "Mesaj pagina link expirat");
Row("html.job-search.invalid.title", "en", "Invalid link", "Title for invalid link page");
Row("html.job-search.invalid.message", "en", "This job search link is not valid.", "Message for invalid link page");
Row("html.job-search.invalid.title", "ro", "Link invalid", "Titlu pagina link invalid");
Row("html.job-search.invalid.message", "ro", "Acest link de cautare joburi nu este valid.", "Mesaj pagina link invalid");
Row("html.job-search.error.title", "en", "Error", "Title for error page");
Row("html.job-search.error.message", "en", "An error occurred. Please try again later.", "Message for error page");
Row("html.job-search.error.title", "ro", "Eroare", "Titlu pagina eroare");
Row("html.job-search.error.message", "ro", "A apărut o eroare. Te rugăm să încerci din nou mai târziu.", "Mesaj pagina eroare");
// AI system prompt for CV matching (language is a {{languageName}} variable inside it)
Row("ai.cv-match.system-prompt", "*",
"You are a strict CV-to-job matching engine. Return JSON only. Score realistically from 0 to 100.\nPenalize missing required skills. Do not invent experience. Use concise business language.\nRespond entirely in {{languageName}} — all text fields in the JSON must be in {{languageName}}.\nJSON shape: {\"score\":number,\"summary\":\"...\",\"strengths\":[\"...\"],\"gaps\":[\"...\"],\"recommendations\":[\"...\"],\"evidence\":[\"...\"]}",
"System prompt template for the CV-to-job LLM matching call. {{languageName}} is substituted at runtime.");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "Templates",
schema: "myAi");
}
}
}
@@ -0,0 +1,59 @@
// <auto-generated />
using System;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
using MyAi.Data;
#nullable disable
namespace MyAi.Data.Migrations
{
[DbContext(typeof(MyAiDbContext))]
partial class MyAiDbContextModelSnapshot : ModelSnapshot
{
protected override void BuildModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasDefaultSchema("myAi")
.HasAnnotation("ProductVersion", "10.0.7")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("MyAi.Data.Entities.TemplateEntity", b =>
{
b.Property<string>("Key")
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Language")
.HasMaxLength(8)
.HasColumnType("nvarchar(8)");
b.Property<string>("Description")
.IsRequired()
.ValueGeneratedOnAdd()
.HasMaxLength(500)
.HasColumnType("nvarchar(500)")
.HasDefaultValue("");
b.Property<DateTime>("UpdatedAt")
.ValueGeneratedOnAdd()
.HasColumnType("datetime2")
.HasDefaultValueSql("SYSUTCDATETIME()");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Key", "Language");
b.ToTable("Templates", "myAi");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,70 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using MyAi.Data;
using System.Collections.Concurrent;
namespace MyAi.Data.Services;
public sealed class DbTemplateService : ITemplateService
{
private readonly IServiceScopeFactory _scopeFactory;
private readonly ILogger<DbTemplateService> _logger;
private ConcurrentDictionary<string, string> _cache = new(StringComparer.OrdinalIgnoreCase);
private DateTime _loadedAt = DateTime.MinValue;
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);
public DbTemplateService(IServiceScopeFactory scopeFactory, ILogger<DbTemplateService> logger)
{
_scopeFactory = scopeFactory;
_logger = logger;
}
public string Get(string key, string language = "en")
{
EnsureCacheLoaded();
if (_cache.TryGetValue(CacheKey(key, language), out var value))
return value;
if (!string.Equals(language, "en", StringComparison.OrdinalIgnoreCase)
&& _cache.TryGetValue(CacheKey(key, "en"), out var fallback))
return fallback;
_logger.LogWarning("Template not found: key={Key}, language={Language}", key, language);
return key;
}
public string Render(string key, string language, params (string Key, string Value)[] placeholders)
{
var template = Get(key, language);
foreach (var (k, v) in placeholders)
template = template.Replace($"{{{{{k}}}}}", v, StringComparison.OrdinalIgnoreCase);
return template;
}
private void EnsureCacheLoaded()
{
if (DateTime.UtcNow - _loadedAt < CacheTtl) return;
try
{
using var scope = _scopeFactory.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<MyAiDbContext>();
var rows = db.Templates.AsNoTracking().ToList();
var fresh = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var row in rows)
fresh[CacheKey(row.Key, row.Language)] = row.Value;
_cache = fresh;
_loadedAt = DateTime.UtcNow;
_logger.LogDebug("Template cache refreshed. {Count} templates loaded.", rows.Count);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to refresh template cache. Serving stale cache.");
}
}
private static string CacheKey(string key, string language) => $"{key}::{language}";
}
@@ -0,0 +1,7 @@
namespace MyAi.Data.Services;
public interface ITemplateService
{
string Get(string key, string language = "en");
string Render(string key, string language, params (string Key, string Value)[] placeholders);
}
+23
View File
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>myai-data</AssemblyName>
<RootNamespace>MyAi.Data</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\shared-data\shared-data.csproj" />
</ItemGroup>
</Project>
+18
View File
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<RootNamespace>MyAi.Models</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
@@ -1,6 +1,6 @@
namespace Rag.Models.Settings;
public sealed class OllamaSettings : Shared.Models.Settings.OllamaSettings
public sealed class OllamaSettings : Common.Settings.OllamaSettings
{
public string EmbeddingModel { get; set; } = "nomic-embed-text";
}
@@ -1,6 +1,6 @@
namespace Rag.Models.Settings;
public sealed class OpenAiSettings: Shared.Models.Settings.OpenAiSettings
public sealed class OpenAiSettings : Common.Settings.OpenAiSettings
{
public string EmbeddingModel { get; set; } = "text-embedding-3-small";
}
+1 -1
View File
@@ -8,7 +8,7 @@
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\shared-models\shared-models.csproj" />
<ProjectReference Include="..\common\common.csproj" />
</ItemGroup>
</Project>
+1 -1
View File
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Options;
using Rag.Models.Settings;
using Api.Data.Repositories.Contracts;
using Rag.Data.Repositories.Contracts;
using Api.Clients.Ai.Contracts;
using CommonHelpers;
+1 -1
View File
@@ -3,7 +3,7 @@ using Api.Services.Contracts;
using Rag.Models.Requests;
using Rag.Models.Responses;
using Swashbuckle.AspNetCore.Annotations;
using Shared.Models.Responses;
using Common.Responses;
namespace Api.Controllers;
@@ -1,6 +1,6 @@
using Rag.Models;
namespace Api.Data.Repositories.Contracts;
namespace Rag.Data.Repositories.Contracts;
public interface IRagRepository
{
@@ -1,10 +1,10 @@
using Api.Data;
using Api.Data.Entities;
using Rag.Data;
using Rag.Data.Entities;
using Microsoft.EntityFrameworkCore;
using Api.Data.Repositories.Contracts;
using Rag.Data.Repositories.Contracts;
using Rag.Models;
namespace Api.Data.Repositories;
namespace Rag.Data.Repositories;
public sealed class EfRagRepository : IRagRepository
{
@@ -1,4 +1,4 @@
namespace Api.Data.Repositories;
namespace Rag.Data.Repositories;
public static class VectorSerializer
{
+7 -3
View File
@@ -3,16 +3,20 @@ ARG BUILD_CONFIGURATION=Release
WORKDIR /src
COPY Apis/rag-api/rag-api.csproj Apis/rag-api/
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
COPY Apis/rag-data/rag-data.csproj Apis/rag-data/
COPY Apis/common/common.csproj Apis/common/
COPY Apis/rag-api-models/rag-api-models.csproj Apis/rag-api-models/
COPY Apis/shared-data/shared-data.csproj Apis/shared-data/
COPY Helpers/common-helpers/common-helpers.csproj Helpers/common-helpers/
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
RUN dotnet restore Apis/rag-api/rag-api.csproj
COPY Apis/rag-api/ Apis/rag-api/
COPY Apis/shared-models/ Apis/shared-models/
COPY Apis/rag-data/ Apis/rag-data/
COPY Apis/common/ Apis/common/
COPY Apis/rag-api-models/ Apis/rag-api-models/
COPY Apis/shared-data/ Apis/shared-data/
COPY Helpers/common-helpers/ Helpers/common-helpers/
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
@@ -25,4 +29,4 @@ ENV ASPNETCORE_URLS=http://0.0.0.0:8080
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "rag-api.dll"]
ENTRYPOINT ["dotnet", "rag-api.dll"]
+5 -6
View File
@@ -1,15 +1,15 @@
using System.Reflection;
using Api.Clients.Ai;
using Api.Clients.Ai.Contracts;
using Api.Data;
using Api.Data.Repositories;
using Api.Data.Repositories.Contracts;
using Rag.Data;
using Rag.Data.Repositories;
using Rag.Data.Repositories.Contracts;
using Api.Services;
using Api.Services.Contracts;
using Microsoft.EntityFrameworkCore;
using Rag.Models.Settings;
using Serilog;
using Shared.Models.Settings;
using Common.Settings;
using StartupHelpers;
StartupExtensions.LoadDotEnvFile();
@@ -39,11 +39,10 @@ try
options.UseSqlServer(connectionString, sql =>
{
sql.MigrationsHistoryTable(RagDbContext.MigrationTableName, RagDbContext.SchemaName);
sql.MigrationsAssembly("rag-data");
});
});
builder.Services.AddHttpClient<RagAiClient>();
builder.Services.AddScoped<IRagRepository, EfRagRepository>();
builder.Services.AddHttpClient<RagAiClient>();
builder.Services.AddScoped<IRagRepository, EfRagRepository>();
builder.Services.AddScoped<IAiClient, CachedRagAiClient>();
+1 -1
View File
@@ -4,7 +4,7 @@ using Api.Services.Contracts;
using Rag.Models.Requests;
using Rag.Models.Responses;
using Rag.Models.Settings;
using Api.Data.Repositories.Contracts;
using Rag.Data.Repositories.Contracts;
using Api.Clients.Ai.Contracts;
using Rag.Models;
using CommonHelpers;
+17 -16
View File
@@ -58,28 +58,29 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.5.1" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.2.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7">
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="PdfPig" Version="0.1.14" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageReference Include="Serilog.Sinks.Email" Version="4.2.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.7" />
<PackageReference Include="Refit.HttpClientFactory" Version="10.1.6" />
<PackageReference Include="PdfPig" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Enrichers.Environment" />
<PackageReference Include="Serilog.Sinks.Console" />
<PackageReference Include="Serilog.Sinks.Email" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
<PackageReference Include="Refit.HttpClientFactory" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Helpers\common-helpers\common-helpers.csproj" />
<ProjectReference Include="..\rag-api-models\rag-api-models.csproj" />
<ProjectReference Include="..\shared-models\shared-models.csproj" />
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
</ItemGroup>
<ProjectReference Include="..\common\common.csproj" />
<ProjectReference Include="..\rag-data\rag-data.csproj" />
<ProjectReference Include="..\..\Helpers\startup-helpers\startup-helpers.csproj" />
</ItemGroup>
</Project>
@@ -1,5 +1,6 @@
namespace Api.Data.Entities;
namespace Rag.Data.Entities;
// CacheKey PK — BaseEntity not applicable
public sealed class RagChatCompletionCacheEntity
{
public string CacheKey { get; set; } = string.Empty;
@@ -1,5 +1,6 @@
namespace Api.Data.Entities;
namespace Rag.Data.Entities;
// no CreatedAt column in schema — BaseEntity not applicable
public sealed class RagChunkEntity
{
public string Id { get; set; } = string.Empty;
@@ -1,8 +1,9 @@
namespace Api.Data.Entities;
using Shared.Data.Entities;
public sealed class RagDocumentEntity
namespace Rag.Data.Entities;
public sealed class RagDocumentEntity : BaseEntity
{
public string Id { get; set; } = string.Empty;
public string DocumentType { get; set; } = string.Empty;
public string Title { get; set; } = string.Empty;
public string? SourceUrl { get; set; }
@@ -10,7 +11,6 @@ public sealed class RagDocumentEntity
public string TextHash { get; set; } = string.Empty;
public double TypeConfidence { get; set; }
public string MetadataJson { get; set; } = "{}";
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
public ICollection<RagChunkEntity> Chunks { get; set; } = [];
}
@@ -1,5 +1,6 @@
namespace Api.Data.Entities;
namespace Rag.Data.Entities;
// CacheKey PK — BaseEntity not applicable
public sealed class RagEmbeddingCacheEntity
{
public string CacheKey { get; set; } = string.Empty;
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Api.Data;
using Rag.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Api.Migrations
namespace Rag.Data.Migrations
{
[DbContext(typeof(RagDbContext))]
[Migration("20260507140305_InitialRagSchema")]
@@ -26,7 +26,7 @@ namespace Api.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Api.Data.Entities.RagChatCompletionCacheEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagChatCompletionCacheEntity", b =>
{
b.Property<string>("CacheKey")
.HasMaxLength(64)
@@ -54,7 +54,7 @@ namespace Api.Migrations
b.ToTable("ChatCompletionCache", "rag");
});
modelBuilder.Entity("Api.Data.Entities.RagChunkEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagChunkEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
@@ -83,7 +83,7 @@ namespace Api.Migrations
b.ToTable("Chunks", "rag");
});
modelBuilder.Entity("Api.Data.Entities.RagDocumentEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagDocumentEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
@@ -135,7 +135,7 @@ namespace Api.Migrations
b.ToTable("Documents", "rag");
});
modelBuilder.Entity("Api.Data.Entities.RagEmbeddingCacheEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagEmbeddingCacheEntity", b =>
{
b.Property<string>("CacheKey")
.HasMaxLength(64)
@@ -167,9 +167,9 @@ namespace Api.Migrations
b.ToTable("EmbeddingCache", "rag");
});
modelBuilder.Entity("Api.Data.Entities.RagChunkEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagChunkEntity", b =>
{
b.HasOne("Api.Data.Entities.RagDocumentEntity", "Document")
b.HasOne("Rag.Data.Entities.RagDocumentEntity", "Document")
.WithMany("Chunks")
.HasForeignKey("DocumentId")
.OnDelete(DeleteBehavior.Cascade)
@@ -178,7 +178,7 @@ namespace Api.Migrations
b.Navigation("Document");
});
modelBuilder.Entity("Api.Data.Entities.RagDocumentEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagDocumentEntity", b =>
{
b.Navigation("Chunks");
});
@@ -1,9 +1,9 @@
using System;
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Api.Migrations
namespace Rag.Data.Migrations
{
/// <inheritdoc />
public partial class InitialRagSchema : Migration
@@ -1,6 +1,6 @@
// <auto-generated />
// <auto-generated />
using System;
using Api.Data;
using Rag.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
@@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Api.Migrations
namespace Rag.Data.Migrations
{
[DbContext(typeof(RagDbContext))]
partial class RagDbContextModelSnapshot : ModelSnapshot
@@ -23,7 +23,7 @@ namespace Api.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Api.Data.Entities.RagChatCompletionCacheEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagChatCompletionCacheEntity", b =>
{
b.Property<string>("CacheKey")
.HasMaxLength(64)
@@ -51,7 +51,7 @@ namespace Api.Migrations
b.ToTable("ChatCompletionCache", "rag");
});
modelBuilder.Entity("Api.Data.Entities.RagChunkEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagChunkEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
@@ -80,7 +80,7 @@ namespace Api.Migrations
b.ToTable("Chunks", "rag");
});
modelBuilder.Entity("Api.Data.Entities.RagDocumentEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagDocumentEntity", b =>
{
b.Property<string>("Id")
.HasMaxLength(64)
@@ -132,7 +132,7 @@ namespace Api.Migrations
b.ToTable("Documents", "rag");
});
modelBuilder.Entity("Api.Data.Entities.RagEmbeddingCacheEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagEmbeddingCacheEntity", b =>
{
b.Property<string>("CacheKey")
.HasMaxLength(64)
@@ -164,9 +164,9 @@ namespace Api.Migrations
b.ToTable("EmbeddingCache", "rag");
});
modelBuilder.Entity("Api.Data.Entities.RagChunkEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagChunkEntity", b =>
{
b.HasOne("Api.Data.Entities.RagDocumentEntity", "Document")
b.HasOne("Rag.Data.Entities.RagDocumentEntity", "Document")
.WithMany("Chunks")
.HasForeignKey("DocumentId")
.OnDelete(DeleteBehavior.Cascade)
@@ -175,7 +175,7 @@ namespace Api.Migrations
b.Navigation("Document");
});
modelBuilder.Entity("Api.Data.Entities.RagDocumentEntity", b =>
modelBuilder.Entity("Rag.Data.Entities.RagDocumentEntity", b =>
{
b.Navigation("Chunks");
});
@@ -1,7 +1,7 @@
using Api.Data.Entities;
using Rag.Data.Entities;
using Microsoft.EntityFrameworkCore;
namespace Api.Data;
namespace Rag.Data;
public sealed class RagDbContext : DbContext
{
+23
View File
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>rag-data</AssemblyName>
<RootNamespace>Rag.Data</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Design">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\shared-data\shared-data.csproj" />
</ItemGroup>
</Project>
+12
View File
@@ -0,0 +1,12 @@
namespace Shared.Data.Entities;
/// <summary>
/// Abstract base for all EF entities that carry a surrogate string PK and an audit timestamp.
/// Entities with a composite PK or a non-<c>Id</c> primary key should NOT inherit this class;
/// document the exception with a brief comment on the entity.
/// </summary>
public abstract class BaseEntity
{
public required string Id { get; init; }
public DateTime CreatedAt { get; init; }
}
+9
View File
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<AssemblyName>shared-data</AssemblyName>
<RootNamespace>Shared.Data</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
+60 -20
View File
@@ -39,29 +39,55 @@ This applies to both the staging and production repos as appropriate.
- Docker Compose for local and production deployment
- Watchtower for automatic container updates in production
## Project taxonomy
| Category | Naming | Contains | EF dependency |
|----------|--------|----------|---------------|
| Executable | `{name}-api`, `{name}-job` | Controllers, Services, Program.cs | Via `ProjectReference` to a `-data` project |
| Domain contracts | `{name}-models`, `{name}-api-models`, `{name}-job-models` | DTOs, Refit interfaces, domain-specific Settings | No |
| Data layer | `{name}-data` | DbContext, EF entities, Migrations | Yes |
| Common contracts | `common` (no suffix) | Infrastructure/technical primitives — no domain ownership | No |
| Common base entities | `shared-data` | Abstract `BaseEntity` class (Id + CreatedAt). No DbContext. | No |
### The `common` project rule
`common` holds **only infrastructure/technical primitives** with no specific service domain ownership: `DatabaseSettings`, `InternalApiSettings`, `ErrorResponse`, `RateLimitingSettings`, `UploadFileRequest`, AI provider settings, etc. **Never put a business-domain type in `common`** — domain types belong in the owning service's `-models` project.
### Where migrations live
**Migrations always live in the `-data` project**, never in an API or Job project. EF CLI split: `--project` = `-data` project (owns the schema); `--startup-project` = whichever API supplies the DB connection string.
## Solution layout
```
Apis/
api/ Public-facing proxy API (port 8080). Handles CORS, rate limiting, captcha, email.
api-models/ DTOs and settings shared by api only.
cv-matcher-api/ Internal CV match engine (port 8082). Owns cvMatcher + cvSearch DB schemas.
cv-matcher-api-models/ DTOs shared between api and cv-matcher-api.
cv-search-models/ EF entities + DbContext for cvSearch schema. Shared by cv-matcher-api and cv-search-job.
rag-api/ Internal RAG/vector-search service (port 8081). Owns rag DB schema.
api-models/ DTOs and settings for api only.
cv-matcher-api/ Internal CV match engine (port 8082). Runs CvMatcher + CvSearch + MyAi DB migrations.
cv-matcher-api-models/ DTOs shared between api and cv-matcher-api (incl. JobSearchSettings).
rag-api/ Internal RAG/vector-search service (port 8081).
rag-api-models/ DTOs shared with rag-api.
shared-models/ Cross-service shared models (DatabaseSettings, etc.).
common/ Cross-service infrastructure primitives (DatabaseSettings, InternalApiSettings, etc.).
shared-data/ Abstract BaseEntity base class. No DbContext.
cv-matcher-data/ CvMatcherDbContext + entities + migrations (schema: cvMatcher).
cv-search-data/ CvSearchDbContext + entities + migrations (schema: cvSearch).
rag-data/ RagDbContext + entities + migrations (schema: rag).
myai-data/ MyAiDbContext + entities + migrations (schema: myAi).
Helpers/
startup-helpers/ Shared Program.cs bootstrap: Serilog, Swagger, .env loading, Azure Key Vault, middleware.
common-helpers/ Utility helpers.
Jobs/
job-scheduler/ IJobTask + JobSchedulerHostedService — the reusable scheduled-job engine.
cv-cleanup-job/ Worker: deletes old CVs from file storage. Runs hourly.
cv-cleanup-job-models/ Job-specific models for cv-cleanup-job (proactive; currently empty).
cv-search-job/ Worker: picks up pending job search sessions, scrapes providers, emails results.
web/ Razor Pages / Blazor front-end (port 5000).
cv-search-job-models/ Job-specific models for cv-search-job (proactive; currently empty).
web/ Razor Pages / Blazor front-end (port 5140).
docker-compose/ docker-compose.yml + .env file.
```
Virtual solution folders in `.sln`: `Apis` (executables + web), `Models` (DTOs/contracts), `Data` (data layers), `Jobs`, `Helpers`.
## Build & restore
```powershell
@@ -79,27 +105,41 @@ Config lives in `docker-compose/.env`. All env vars use `${VAR:-default}` fallba
## Database schemas
| Schema | Owner DbContext | Migrations assembly |
|-------------|----------------------|-----------------------|
| `cvMatcher` | `CvMatcherDbContext` | `cv-matcher-api` |
| `rag` | `RagDbContext` | `rag-api` |
| `cvSearch` | `CvSearchDbContext` | `cv-search-models` |
| Schema | Owner DbContext | Migrations project | Startup project |
|-------------|----------------------|-----------------------|-----------------------|
| `cvMatcher` | `CvMatcherDbContext` | `cv-matcher-data` | `cv-matcher-api` |
| `rag` | `RagDbContext` | `rag-data` | `rag-api` |
| `cvSearch` | `CvSearchDbContext` | `cv-search-data` | `cv-matcher-api` |
| `myAi` | `MyAiDbContext` | `myai-data` | `api` |
Both `cv-matcher-api` and `cv-search-job` register `CvSearchDbContext` and call `db.Database.Migrate()` on startup (idempotent — safe for both to run).
## EF Core migrations
```powershell
# Add a migration to cv-search-models
dotnet ef migrations add <MigrationName> \
--context CvSearchDbContext \
--project Apis/cv-search-models \
# cv-matcher-data (schema: cvMatcher)
dotnet ef migrations add <MigrationName> `
--context CvMatcherDbContext `
--project Apis/cv-matcher-data `
--startup-project Apis/cv-matcher-api
# Add a migration to cv-matcher-api
dotnet ef migrations add <MigrationName> \
--context CvMatcherDbContext \
--project Apis/cv-matcher-api
# rag-data (schema: rag)
dotnet ef migrations add <MigrationName> `
--context RagDbContext `
--project Apis/rag-data `
--startup-project Apis/rag-api
# cv-search-data (schema: cvSearch)
dotnet ef migrations add <MigrationName> `
--context CvSearchDbContext `
--project Apis/cv-search-data `
--startup-project Apis/cv-matcher-api
# myai-data (schema: myAi)
dotnet ef migrations add <MigrationName> `
--context MyAiDbContext `
--project Apis/myai-data `
--startup-project Apis/api
```
EF tools version warning ("older than runtime") is expected and harmless. The `HostAbortedException` output during migration scaffolding is normal — EF starts the host to discover DbContext then aborts it.
+38
View File
@@ -0,0 +1,38 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Azure -->
<PackageVersion Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.5.1" />
<PackageVersion Include="Azure.Identity" Version="1.21.0" />
<!-- EF Core -->
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="10.0.7" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="10.0.7" />
<!-- Extensions -->
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="10.0.0" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="10.0.0" />
<!-- Config -->
<PackageVersion Include="DotNetEnv" Version="3.2.0" />
<!-- HTTP / Refit -->
<PackageVersion Include="Refit.HttpClientFactory" Version="10.1.6" />
<!-- Serilog -->
<PackageVersion Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageVersion Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.1.1" />
<PackageVersion Include="Serilog.Sinks.Email" Version="4.2.1" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<!-- Swagger -->
<PackageVersion Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageVersion Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.7" />
<!-- Web -->
<PackageVersion Include="Yarp.ReverseProxy" Version="2.3.0" />
<PackageVersion Include="MailKit" Version="4.16.0" />
<PackageVersion Include="PdfPig" Version="0.1.14" />
<!-- Tooling -->
<PackageVersion Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
</ItemGroup>
</Project>
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Shared.Models.Settings;
using Common.Settings;
namespace StartupHelpers;
@@ -4,7 +4,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Shared.Models.Settings;
using Common.Settings;
namespace StartupHelpers;
+10 -10
View File
@@ -12,19 +12,19 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" Version="1.5.1" />
<PackageReference Include="Azure.Identity" Version="1.21.0" />
<PackageReference Include="DotNetEnv" Version="3.2.0" />
<PackageReference Include="Serilog.AspNetCore" Version="10.0.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Email" Version="4.2.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="10.1.7" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" Version="10.1.7" />
<PackageReference Include="Azure.Extensions.AspNetCore.Configuration.Secrets" />
<PackageReference Include="Azure.Identity" />
<PackageReference Include="DotNetEnv" />
<PackageReference Include="Serilog.AspNetCore" />
<PackageReference Include="Serilog.Enrichers.Environment" />
<PackageReference Include="Serilog.Sinks.Email" />
<PackageReference Include="Serilog.Sinks.File" />
<PackageReference Include="Swashbuckle.AspNetCore" />
<PackageReference Include="Swashbuckle.AspNetCore.Annotations" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Apis\shared-models\shared-models.csproj" />
<ProjectReference Include="..\..\Apis\common\common.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>cv-cleanup-job-models</AssemblyName>
<RootNamespace>CvCleanup.Job.Models</RootNamespace>
</PropertyGroup>
</Project>
+2 -2
View File
@@ -6,7 +6,7 @@ COPY Jobs/cv-cleanup-job/cv-cleanup-job.csproj Jobs/cv-cleanup-job/
COPY Jobs/job-scheduler/job-scheduler.csproj Jobs/job-scheduler/
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
COPY Apis/api-models/api-models.csproj Apis/api-models/
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
COPY Apis/common/common.csproj Apis/common/
RUN dotnet restore Jobs/cv-cleanup-job/cv-cleanup-job.csproj
@@ -14,7 +14,7 @@ COPY Jobs/cv-cleanup-job/ Jobs/cv-cleanup-job/
COPY Jobs/job-scheduler/ Jobs/job-scheduler/
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
COPY Apis/api-models/ Apis/api-models/
COPY Apis/shared-models/ Apis/shared-models/
COPY Apis/common/ Apis/common/
RUN dotnet publish Jobs/cv-cleanup-job/cv-cleanup-job.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+1 -1
View File
@@ -9,7 +9,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" />
</ItemGroup>
<ItemGroup>
@@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<AssemblyName>cv-search-job-models</AssemblyName>
<RootNamespace>CvSearch.Job.Models</RootNamespace>
</PropertyGroup>
</Project>
+1 -1
View File
@@ -86,5 +86,5 @@ Follows the same scheme as `cv-cleanup-job`:
## EF migrations
This project runs `CvSearchDbContext.Database.Migrate()` on startup.
Migrations live in `Apis/cv-search-models/Migrations/`.
Migrations live in `Apis/cv-search-data/Migrations/`.
To add a migration: see root CLAUDE.md.
+8 -6
View File
@@ -4,20 +4,22 @@ WORKDIR /src
COPY Jobs/cv-search-job/cv-search-job.csproj Jobs/cv-search-job/
COPY Jobs/job-scheduler/job-scheduler.csproj Jobs/job-scheduler/
COPY Apis/cv-search-models/cv-search-models.csproj Apis/cv-search-models/
COPY Apis/cv-search-data/cv-search-data.csproj Apis/cv-search-data/
COPY Apis/cv-matcher-api-models/cv-matcher-api-models.csproj Apis/cv-matcher-api-models/
COPY Apis/shared-models/shared-models.csproj Apis/shared-models/
COPY Apis/myai-models/myai-models.csproj Apis/myai-models/
COPY Apis/common/common.csproj Apis/common/
COPY Apis/myai-data/myai-data.csproj Apis/myai-data/
COPY Apis/shared-data/shared-data.csproj Apis/shared-data/
COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/
RUN dotnet restore Jobs/cv-search-job/cv-search-job.csproj
COPY Jobs/cv-search-job/ Jobs/cv-search-job/
COPY Jobs/job-scheduler/ Jobs/job-scheduler/
COPY Apis/cv-search-models/ Apis/cv-search-models/
COPY Apis/cv-search-data/ Apis/cv-search-data/
COPY Apis/cv-matcher-api-models/ Apis/cv-matcher-api-models/
COPY Apis/shared-models/ Apis/shared-models/
COPY Apis/myai-models/ Apis/myai-models/
COPY Apis/common/ Apis/common/
COPY Apis/myai-data/ Apis/myai-data/
COPY Apis/shared-data/ Apis/shared-data/
COPY Helpers/startup-helpers/ Helpers/startup-helpers/
RUN dotnet publish Jobs/cv-search-job/cv-search-job.csproj -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
+7 -7
View File
@@ -1,6 +1,6 @@
using System.Reflection;
using CvSearch.Models.Data;
using CvSearch.Models.Settings;
using CvMatcher.Models.Settings;
using CvSearch.Data;
using CvSearchJob.Clients;
using CvSearchJob.Services;
using CvSearchJob.Tasks;
@@ -9,11 +9,11 @@ using JobScheduler.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using MyAi.Models.Data;
using MyAi.Models.Services;
using MyAi.Data;
using MyAi.Data.Services;
using Refit;
using Serilog;
using Shared.Models.Settings;
using Common.Settings;
using StartupHelpers;
const string ServiceName = "cv-search-job";
@@ -36,7 +36,7 @@ try
var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration);
options.UseSqlServer(connectionString, sql =>
{
sql.MigrationsAssembly("cv-search-models");
sql.MigrationsAssembly("cv-search-data");
sql.MigrationsHistoryTable(CvSearchDbContext.MigrationTableName, CvSearchDbContext.SchemaName);
});
});
@@ -58,7 +58,7 @@ try
var connectionString = builder.Services.GetConfiguredDbConnectionString(builder.Configuration);
options.UseSqlServer(connectionString, sql =>
{
sql.MigrationsAssembly("myai-models");
sql.MigrationsAssembly("myai-data");
sql.MigrationsHistoryTable(MyAiDbContext.MigrationTableName, MyAiDbContext.SchemaName);
});
});
@@ -1,11 +1,11 @@
using CvMatcher.Models.Responses;
using CvSearch.Models.Data.Entities;
using CvSearch.Data.Entities;
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using MimeKit;
using MyAi.Models.Services;
using MyAi.Data.Services;
namespace CvSearchJob.Services;
@@ -1,6 +1,6 @@
using System.Text.RegularExpressions;
using System.Web;
using CvSearch.Models.Settings;
using CvMatcher.Models.Settings;
using Microsoft.Extensions.Logging;
namespace CvSearchJob.Services;

Some files were not shown because too many files have changed in this diff Show More