diff --git a/Apis/api-models/Requests/UploadCvRequest.cs b/Apis/api-models/Requests/UploadCvRequest.cs
index 18bd5db..31349d0 100644
--- a/Apis/api-models/Requests/UploadCvRequest.cs
+++ b/Apis/api-models/Requests/UploadCvRequest.cs
@@ -1,4 +1,4 @@
-using Shared.Models.Requests;
+using Common.Requests;
namespace Models.Requests
{
diff --git a/Apis/api-models/api-models.csproj b/Apis/api-models/api-models.csproj
index f9c95a3..5e0613b 100644
--- a/Apis/api-models/api-models.csproj
+++ b/Apis/api-models/api-models.csproj
@@ -1,4 +1,4 @@
-
+
net10.0
@@ -8,11 +8,11 @@
-
+
-
+
diff --git a/Apis/api/Controllers/CaptchaController.cs b/Apis/api/Controllers/CaptchaController.cs
index 53a715d..b050523 100644
--- a/Apis/api/Controllers/CaptchaController.cs
+++ b/Apis/api/Controllers/CaptchaController.cs
@@ -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
{
diff --git a/Apis/api/Controllers/ContactController.cs b/Apis/api/Controllers/ContactController.cs
index 920b594..b95e2ea 100644
--- a/Apis/api/Controllers/ContactController.cs
+++ b/Apis/api/Controllers/ContactController.cs
@@ -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
{
diff --git a/Apis/api/Controllers/CvMatcherController.cs b/Apis/api/Controllers/CvMatcherController.cs
index 76b07a9..fe734a8 100644
--- a/Apis/api/Controllers/CvMatcherController.cs
+++ b/Apis/api/Controllers/CvMatcherController.cs
@@ -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;
diff --git a/Apis/api/Controllers/FileDownloadController.cs b/Apis/api/Controllers/FileDownloadController.cs
index 58765de..f12a5fb 100644
--- a/Apis/api/Controllers/FileDownloadController.cs
+++ b/Apis/api/Controllers/FileDownloadController.cs
@@ -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
{
diff --git a/Apis/api/Dockerfile b/Apis/api/Dockerfile
index d344070..e3d147b 100644
--- a/Apis/api/Dockerfile
+++ b/Apis/api/Dockerfile
@@ -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"]
\ No newline at end of file
+ENTRYPOINT ["dotnet", "api.dll"]
diff --git a/Apis/api/Program.cs b/Apis/api/Program.cs
index 4b92fbb..ae73a22 100644
--- a/Apis/api/Program.cs
+++ b/Apis/api/Program.cs
@@ -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);
});
});
diff --git a/Apis/api/Services/SmtpEmailSender.cs b/Apis/api/Services/SmtpEmailSender.cs
index d17ffe7..ee564b4 100644
--- a/Apis/api/Services/SmtpEmailSender.cs
+++ b/Apis/api/Services/SmtpEmailSender.cs
@@ -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
{
diff --git a/Apis/api/api.csproj b/Apis/api/api.csproj
index d369786..4a628cc 100644
--- a/Apis/api/api.csproj
+++ b/Apis/api/api.csproj
@@ -16,18 +16,18 @@
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
@@ -37,9 +37,9 @@
-
+
-
+
diff --git a/Apis/shared-models/Requests/UploadFileRequest.cs b/Apis/common/Requests/UploadFileRequest.cs
similarity index 86%
rename from Apis/shared-models/Requests/UploadFileRequest.cs
rename to Apis/common/Requests/UploadFileRequest.cs
index c9ed0b2..99a24dc 100644
--- a/Apis/shared-models/Requests/UploadFileRequest.cs
+++ b/Apis/common/Requests/UploadFileRequest.cs
@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Http;
using System.ComponentModel.DataAnnotations;
-namespace Shared.Models.Requests
+namespace Common.Requests
{
public class UploadFileRequest
{
diff --git a/Apis/shared-models/Responses/ErrorResponse.cs b/Apis/common/Responses/ErrorResponse.cs
similarity index 85%
rename from Apis/shared-models/Responses/ErrorResponse.cs
rename to Apis/common/Responses/ErrorResponse.cs
index c688715..a1262d8 100644
--- a/Apis/shared-models/Responses/ErrorResponse.cs
+++ b/Apis/common/Responses/ErrorResponse.cs
@@ -1,4 +1,4 @@
-namespace Shared.Models.Responses;
+namespace Common.Responses;
public sealed class ErrorResponse
{
diff --git a/Apis/shared-models/Settings/AiSettings.cs b/Apis/common/Settings/AiSettings.cs
similarity index 73%
rename from Apis/shared-models/Settings/AiSettings.cs
rename to Apis/common/Settings/AiSettings.cs
index 56ff34c..f6fef72 100644
--- a/Apis/shared-models/Settings/AiSettings.cs
+++ b/Apis/common/Settings/AiSettings.cs
@@ -1,4 +1,4 @@
-namespace Shared.Models.Settings
+namespace Common.Settings
{
public class AiSettings
{
diff --git a/Apis/shared-models/Settings/DatabaseSettings.cs b/Apis/common/Settings/DatabaseSettings.cs
similarity index 90%
rename from Apis/shared-models/Settings/DatabaseSettings.cs
rename to Apis/common/Settings/DatabaseSettings.cs
index d5f89e9..4a3961a 100644
--- a/Apis/shared-models/Settings/DatabaseSettings.cs
+++ b/Apis/common/Settings/DatabaseSettings.cs
@@ -1,4 +1,4 @@
-namespace Shared.Models.Settings
+namespace Common.Settings
{
public class DatabaseSettings
{
diff --git a/Apis/shared-models/Settings/InternalApiSettings.cs b/Apis/common/Settings/InternalApiSettings.cs
similarity index 82%
rename from Apis/shared-models/Settings/InternalApiSettings.cs
rename to Apis/common/Settings/InternalApiSettings.cs
index 14b0637..d988232 100644
--- a/Apis/shared-models/Settings/InternalApiSettings.cs
+++ b/Apis/common/Settings/InternalApiSettings.cs
@@ -1,4 +1,4 @@
-namespace Shared.Models.Settings
+namespace Common.Settings
{
public class InternalApiSettings
{
diff --git a/Apis/shared-models/Settings/OllamaSettings.cs b/Apis/common/Settings/OllamaSettings.cs
similarity index 86%
rename from Apis/shared-models/Settings/OllamaSettings.cs
rename to Apis/common/Settings/OllamaSettings.cs
index 6cb4584..2b3a11f 100644
--- a/Apis/shared-models/Settings/OllamaSettings.cs
+++ b/Apis/common/Settings/OllamaSettings.cs
@@ -1,4 +1,4 @@
-namespace Shared.Models.Settings
+namespace Common.Settings
{
public class OllamaSettings
{
diff --git a/Apis/shared-models/Settings/OpenAiSettings.cs b/Apis/common/Settings/OpenAiSettings.cs
similarity index 86%
rename from Apis/shared-models/Settings/OpenAiSettings.cs
rename to Apis/common/Settings/OpenAiSettings.cs
index 603a55f..e280784 100644
--- a/Apis/shared-models/Settings/OpenAiSettings.cs
+++ b/Apis/common/Settings/OpenAiSettings.cs
@@ -1,4 +1,4 @@
-namespace Shared.Models.Settings
+namespace Common.Settings
{
public class OpenAiSettings
{
diff --git a/Apis/shared-models/Settings/RateLimitingSettings.cs b/Apis/common/Settings/RateLimitingSettings.cs
similarity index 94%
rename from Apis/shared-models/Settings/RateLimitingSettings.cs
rename to Apis/common/Settings/RateLimitingSettings.cs
index 2f1a730..38bb837 100644
--- a/Apis/shared-models/Settings/RateLimitingSettings.cs
+++ b/Apis/common/Settings/RateLimitingSettings.cs
@@ -1,4 +1,4 @@
-namespace Shared.Models.Settings
+namespace Common.Settings
{
public class RateLimitingSettings
{
diff --git a/Apis/shared-models/shared-models.csproj b/Apis/common/common.csproj
similarity index 53%
rename from Apis/shared-models/shared-models.csproj
rename to Apis/common/common.csproj
index 0cea718..3762a2e 100644
--- a/Apis/shared-models/shared-models.csproj
+++ b/Apis/common/common.csproj
@@ -1,14 +1,15 @@
-
+
net10.0
- Shared.Models
+ common
+ Common
enable
enable
-
+
diff --git a/Apis/cv-matcher-api-models/Settings/AiSettings.cs b/Apis/cv-matcher-api-models/Settings/AiSettings.cs
index 839ddb8..0f1e7d5 100644
--- a/Apis/cv-matcher-api-models/Settings/AiSettings.cs
+++ b/Apis/cv-matcher-api-models/Settings/AiSettings.cs
@@ -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();
diff --git a/Apis/cv-matcher-api-models/Settings/JobSearchSettings.cs b/Apis/cv-matcher-api-models/Settings/JobSearchSettings.cs
new file mode 100644
index 0000000..5afaa9d
--- /dev/null
+++ b/Apis/cv-matcher-api-models/Settings/JobSearchSettings.cs
@@ -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 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 InitialKeywords { get; set; } = [];
+ public int MaxResults { get; set; } = 20;
+}
diff --git a/Apis/cv-matcher-api-models/cv-matcher-api-models.csproj b/Apis/cv-matcher-api-models/cv-matcher-api-models.csproj
index aeeb632..9dddb25 100644
--- a/Apis/cv-matcher-api-models/cv-matcher-api-models.csproj
+++ b/Apis/cv-matcher-api-models/cv-matcher-api-models.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/Apis/cv-matcher-api/CLAUDE.md b/Apis/cv-matcher-api/CLAUDE.md
index a310eb4..4867458 100644
--- a/Apis/cv-matcher-api/CLAUDE.md
+++ b/Apis/cv-matcher-api/CLAUDE.md
@@ -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)
diff --git a/Apis/cv-matcher-api/Clients/Ai/CachedMatcherAiClient.cs b/Apis/cv-matcher-api/Clients/Ai/CachedMatcherAiClient.cs
index 05014e6..8fd03e6 100644
--- a/Apis/cv-matcher-api/Clients/Ai/CachedMatcherAiClient.cs
+++ b/Apis/cv-matcher-api/Clients/Ai/CachedMatcherAiClient.cs
@@ -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;
diff --git a/Apis/cv-matcher-api/Clients/Ai/MatcherAiClient.cs b/Apis/cv-matcher-api/Clients/Ai/MatcherAiClient.cs
index ae8f9cb..3fe3968 100644
--- a/Apis/cv-matcher-api/Clients/Ai/MatcherAiClient.cs
+++ b/Apis/cv-matcher-api/Clients/Ai/MatcherAiClient.cs
@@ -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;
diff --git a/Apis/cv-matcher-api/Controllers/CvController.cs b/Apis/cv-matcher-api/Controllers/CvController.cs
index 7e54f97..c0d0ee4 100644
--- a/Apis/cv-matcher-api/Controllers/CvController.cs
+++ b/Apis/cv-matcher-api/Controllers/CvController.cs
@@ -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;
diff --git a/Apis/cv-matcher-api/Controllers/JobSearchController.cs b/Apis/cv-matcher-api/Controllers/JobSearchController.cs
index 95d832c..65bd1eb 100644
--- a/Apis/cv-matcher-api/Controllers/JobSearchController.cs
+++ b/Apis/cv-matcher-api/Controllers/JobSearchController.cs
@@ -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;
diff --git a/Apis/cv-matcher-api/Data/Repositories/Contracts/IMatcherRepository.cs b/Apis/cv-matcher-api/Data/Repositories/Contracts/IMatcherRepository.cs
index e128a69..6241862 100644
--- a/Apis/cv-matcher-api/Data/Repositories/Contracts/IMatcherRepository.cs
+++ b/Apis/cv-matcher-api/Data/Repositories/Contracts/IMatcherRepository.cs
@@ -1,6 +1,6 @@
using CvMatcher.Models.Responses;
-namespace Api.Data.Repositories.Contracts;
+namespace CvMatcher.Data.Repositories.Contracts;
public interface IMatcherRepository
{
diff --git a/Apis/cv-matcher-api/Data/Repositories/EfMatcherRepository.cs b/Apis/cv-matcher-api/Data/Repositories/EfMatcherRepository.cs
index 5ed9b0b..85ada91 100644
--- a/Apis/cv-matcher-api/Data/Repositories/EfMatcherRepository.cs
+++ b/Apis/cv-matcher-api/Data/Repositories/EfMatcherRepository.cs
@@ -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
{
diff --git a/Apis/cv-matcher-api/Dockerfile b/Apis/cv-matcher-api/Dockerfile
index 5803a8e..aa636ce 100644
--- a/Apis/cv-matcher-api/Dockerfile
+++ b/Apis/cv-matcher-api/Dockerfile
@@ -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"]
\ No newline at end of file
+ENTRYPOINT ["dotnet", "cv-matcher-api.dll"]
diff --git a/Apis/cv-matcher-api/Program.cs b/Apis/cv-matcher-api/Program.cs
index c0b0f94..0913aec 100644
--- a/Apis/cv-matcher-api/Program.cs
+++ b/Apis/cv-matcher-api/Program.cs
@@ -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);
});
});
diff --git a/Apis/cv-matcher-api/Services/CvMatcherService.cs b/Apis/cv-matcher-api/Services/CvMatcherService.cs
index 5d34651..5f699fd 100644
--- a/Apis/cv-matcher-api/Services/CvMatcherService.cs
+++ b/Apis/cv-matcher-api/Services/CvMatcherService.cs
@@ -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;
diff --git a/Apis/cv-matcher-api/Services/JobTokenService.cs b/Apis/cv-matcher-api/Services/JobTokenService.cs
index 4640438..8b1f2d8 100644
--- a/Apis/cv-matcher-api/Services/JobTokenService.cs
+++ b/Apis/cv-matcher-api/Services/JobTokenService.cs
@@ -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;
diff --git a/Apis/cv-matcher-api/cv-matcher-api.csproj b/Apis/cv-matcher-api/cv-matcher-api.csproj
index 5bed5f5..561c0ea 100644
--- a/Apis/cv-matcher-api/cv-matcher-api.csproj
+++ b/Apis/cv-matcher-api/cv-matcher-api.csproj
@@ -58,30 +58,31 @@
-
-
-
-
-
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
diff --git a/Apis/cv-matcher-api/Data/CvMatcherDbContext.cs b/Apis/cv-matcher-data/CvMatcherDbContext.cs
similarity index 96%
rename from Apis/cv-matcher-api/Data/CvMatcherDbContext.cs
rename to Apis/cv-matcher-data/CvMatcherDbContext.cs
index 5889116..52e12ce 100644
--- a/Apis/cv-matcher-api/Data/CvMatcherDbContext.cs
+++ b/Apis/cv-matcher-data/CvMatcherDbContext.cs
@@ -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
{
diff --git a/Apis/cv-matcher-api/Data/Entities/CvMatchResultEntity.cs b/Apis/cv-matcher-data/Entities/CvMatchResultEntity.cs
similarity index 59%
rename from Apis/cv-matcher-api/Data/Entities/CvMatchResultEntity.cs
rename to Apis/cv-matcher-data/Entities/CvMatchResultEntity.cs
index d86a1b0..ac0a9c5 100644
--- a/Apis/cv-matcher-api/Data/Entities/CvMatchResultEntity.cs
+++ b/Apis/cv-matcher-data/Entities/CvMatchResultEntity.cs
@@ -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;
}
diff --git a/Apis/cv-matcher-api/Data/Entities/CvMatcherChatCacheEntity.cs b/Apis/cv-matcher-data/Entities/CvMatcherChatCacheEntity.cs
similarity index 80%
rename from Apis/cv-matcher-api/Data/Entities/CvMatcherChatCacheEntity.cs
rename to Apis/cv-matcher-data/Entities/CvMatcherChatCacheEntity.cs
index 4ad845a..2bb669f 100644
--- a/Apis/cv-matcher-api/Data/Entities/CvMatcherChatCacheEntity.cs
+++ b/Apis/cv-matcher-data/Entities/CvMatcherChatCacheEntity.cs
@@ -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;
diff --git a/Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs b/Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs
similarity index 92%
rename from Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs
rename to Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs
index 1ef6774..42ef9a7 100644
--- a/Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs
+++ b/Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.Designer.cs
@@ -1,6 +1,6 @@
-//
+//
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("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("CacheKey")
.HasMaxLength(64)
diff --git a/Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.cs b/Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.cs
similarity index 98%
rename from Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.cs
rename to Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.cs
index 4c62b0f..9e9c62f 100644
--- a/Apis/cv-matcher-api/Migrations/20260507140442_InitialCvMatcherSchema.cs
+++ b/Apis/cv-matcher-data/Migrations/20260507140442_InitialCvMatcherSchema.cs
@@ -1,9 +1,9 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
-namespace Api.Migrations
+namespace CvMatcher.Data.Migrations
{
///
public partial class InitialCvMatcherSchema : Migration
diff --git a/Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs b/Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs
similarity index 92%
rename from Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs
rename to Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs
index bf50633..74d8293 100644
--- a/Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs
+++ b/Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.Designer.cs
@@ -1,6 +1,6 @@
-//
+//
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("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("CacheKey")
.HasMaxLength(64)
diff --git a/Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.cs b/Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.cs
similarity index 90%
rename from Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.cs
rename to Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.cs
index c711c23..0c58014 100644
--- a/Apis/cv-matcher-api/Migrations/20260524140335_AddLanguageToCvMatchResult.cs
+++ b/Apis/cv-matcher-data/Migrations/20260524140335_AddLanguageToCvMatchResult.cs
@@ -1,8 +1,8 @@
-using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
-namespace Api.Migrations
+namespace CvMatcher.Data.Migrations
{
///
public partial class AddLanguageToCvMatchResult : Migration
diff --git a/Apis/cv-matcher-api/Migrations/CvMatcherDbContextModelSnapshot.cs b/Apis/cv-matcher-data/Migrations/CvMatcherDbContextModelSnapshot.cs
similarity index 92%
rename from Apis/cv-matcher-api/Migrations/CvMatcherDbContextModelSnapshot.cs
rename to Apis/cv-matcher-data/Migrations/CvMatcherDbContextModelSnapshot.cs
index af0e900..8e7ffff 100644
--- a/Apis/cv-matcher-api/Migrations/CvMatcherDbContextModelSnapshot.cs
+++ b/Apis/cv-matcher-data/Migrations/CvMatcherDbContextModelSnapshot.cs
@@ -1,6 +1,6 @@
-//
+//
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("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("CacheKey")
.HasMaxLength(64)
diff --git a/Apis/cv-matcher-data/cv-matcher-data.csproj b/Apis/cv-matcher-data/cv-matcher-data.csproj
new file mode 100644
index 0000000..3e627b1
--- /dev/null
+++ b/Apis/cv-matcher-data/cv-matcher-data.csproj
@@ -0,0 +1,19 @@
+
+
+ net10.0
+ enable
+ enable
+ cv-matcher-data
+ CvMatcher.Data
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
diff --git a/Apis/cv-search-data/Data/CvSearchDbContext.cs b/Apis/cv-search-data/Data/CvSearchDbContext.cs
new file mode 100644
index 0000000..0c66516
--- /dev/null
+++ b/Apis/cv-search-data/Data/CvSearchDbContext.cs
@@ -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 options) : base(options) { }
+
+ public DbSet JobSearchTokens => Set();
+ public DbSet JobSearchSessions => Set();
+ public DbSet JobSearchResults => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.HasDefaultSchema(SchemaName);
+
+ modelBuilder.Entity(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(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(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);
+ });
+ }
+}
diff --git a/Apis/cv-search-data/Data/Entities/JobSearchResultEntity.cs b/Apis/cv-search-data/Data/Entities/JobSearchResultEntity.cs
new file mode 100644
index 0000000..f64f4ec
--- /dev/null
+++ b/Apis/cv-search-data/Data/Entities/JobSearchResultEntity.cs
@@ -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;
+}
diff --git a/Apis/cv-search-data/Data/Entities/JobSearchSessionEntity.cs b/Apis/cv-search-data/Data/Entities/JobSearchSessionEntity.cs
new file mode 100644
index 0000000..70102e4
--- /dev/null
+++ b/Apis/cv-search-data/Data/Entities/JobSearchSessionEntity.cs
@@ -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";
+}
diff --git a/Apis/cv-search-data/Data/Entities/JobSearchTokenEntity.cs b/Apis/cv-search-data/Data/Entities/JobSearchTokenEntity.cs
new file mode 100644
index 0000000..e3d768c
--- /dev/null
+++ b/Apis/cv-search-data/Data/Entities/JobSearchTokenEntity.cs
@@ -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; }
+}
diff --git a/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.Designer.cs b/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.Designer.cs
new file mode 100644
index 0000000..26141b2
--- /dev/null
+++ b/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.Designer.cs
@@ -0,0 +1,160 @@
+//
+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
+ {
+ ///
+ 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("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("JobText")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("JobTitle")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("JobUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("nvarchar(2048)");
+
+ b.Property("ProviderName")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("ResultJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Score")
+ .HasColumnType("int");
+
+ b.Property("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("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CvDocumentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Keywords")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("ProviderConfigJson")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("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("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CvDocumentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Used")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(false);
+
+ b.HasKey("Id");
+
+ b.ToTable("JobSearchTokens", "cvSearch");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.cs b/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.cs
new file mode 100644
index 0000000..d57fc57
--- /dev/null
+++ b/Apis/cv-search-data/Migrations/20260522093356_AddJobSearchTables.cs
@@ -0,0 +1,102 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace CvSearch.Data.Migrations
+{
+ ///
+ public partial class AddJobSearchTables : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.EnsureSchema(
+ name: "cvSearch");
+
+ migrationBuilder.CreateTable(
+ name: "JobSearchResults",
+ schema: "cvSearch",
+ columns: table => new
+ {
+ Id = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ SessionId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ ProviderName = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false),
+ JobUrl = table.Column(type: "nvarchar(2048)", maxLength: 2048, nullable: false),
+ JobTitle = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: false),
+ JobText = table.Column(type: "nvarchar(max)", nullable: false),
+ Score = table.Column(type: "int", nullable: false),
+ ResultJson = table.Column(type: "nvarchar(max)", nullable: false),
+ CreatedAt = table.Column(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(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ TokenId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ CvDocumentId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false),
+ Status = table.Column(type: "nvarchar(32)", maxLength: 32, nullable: false),
+ Keywords = table.Column(type: "nvarchar(1000)", maxLength: 1000, nullable: false),
+ ProviderConfigJson = table.Column(type: "nvarchar(max)", nullable: true),
+ CreatedAt = table.Column(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(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ CvDocumentId = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false),
+ Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: false),
+ ExpiresAt = table.Column(type: "datetime2", nullable: false),
+ Used = table.Column(type: "bit", nullable: false, defaultValue: false),
+ CreatedAt = table.Column(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");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "JobSearchResults",
+ schema: "cvSearch");
+
+ migrationBuilder.DropTable(
+ name: "JobSearchSessions",
+ schema: "cvSearch");
+
+ migrationBuilder.DropTable(
+ name: "JobSearchTokens",
+ schema: "cvSearch");
+ }
+ }
+}
diff --git a/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.Designer.cs b/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.Designer.cs
new file mode 100644
index 0000000..847c985
--- /dev/null
+++ b/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.Designer.cs
@@ -0,0 +1,174 @@
+//
+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
+ {
+ ///
+ 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("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("JobText")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("JobTitle")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("JobUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("nvarchar(2048)");
+
+ b.Property("ProviderName")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("ResultJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Score")
+ .HasColumnType("int");
+
+ b.Property("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("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CvDocumentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Keywords")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Language")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)")
+ .HasDefaultValue("en");
+
+ b.Property("ProviderConfigJson")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("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("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CvDocumentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Language")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)")
+ .HasDefaultValue("en");
+
+ b.Property("Used")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(false);
+
+ b.HasKey("Id");
+
+ b.ToTable("JobSearchTokens", "cvSearch");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.cs b/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.cs
new file mode 100644
index 0000000..ef76ef8
--- /dev/null
+++ b/Apis/cv-search-data/Migrations/20260524145702_AddLanguageToJobSearchEntities.cs
@@ -0,0 +1,46 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace CvSearch.Data.Migrations
+{
+ ///
+ public partial class AddLanguageToJobSearchEntities : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Language",
+ schema: "cvSearch",
+ table: "JobSearchTokens",
+ type: "nvarchar(8)",
+ maxLength: 8,
+ nullable: false,
+ defaultValue: "en");
+
+ migrationBuilder.AddColumn(
+ name: "Language",
+ schema: "cvSearch",
+ table: "JobSearchSessions",
+ type: "nvarchar(8)",
+ maxLength: 8,
+ nullable: false,
+ defaultValue: "en");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Language",
+ schema: "cvSearch",
+ table: "JobSearchTokens");
+
+ migrationBuilder.DropColumn(
+ name: "Language",
+ schema: "cvSearch",
+ table: "JobSearchSessions");
+ }
+ }
+}
diff --git a/Apis/cv-search-data/Migrations/CvSearchDbContextModelSnapshot.cs b/Apis/cv-search-data/Migrations/CvSearchDbContextModelSnapshot.cs
new file mode 100644
index 0000000..1cb9f20
--- /dev/null
+++ b/Apis/cv-search-data/Migrations/CvSearchDbContextModelSnapshot.cs
@@ -0,0 +1,171 @@
+//
+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("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("JobText")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("JobTitle")
+ .IsRequired()
+ .HasMaxLength(512)
+ .HasColumnType("nvarchar(512)");
+
+ b.Property("JobUrl")
+ .IsRequired()
+ .HasMaxLength(2048)
+ .HasColumnType("nvarchar(2048)");
+
+ b.Property("ProviderName")
+ .IsRequired()
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("ResultJson")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Score")
+ .HasColumnType("int");
+
+ b.Property("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("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CvDocumentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("Keywords")
+ .IsRequired()
+ .HasMaxLength(1000)
+ .HasColumnType("nvarchar(1000)");
+
+ b.Property("Language")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)")
+ .HasDefaultValue("en");
+
+ b.Property("ProviderConfigJson")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("Status")
+ .IsRequired()
+ .HasMaxLength(32)
+ .HasColumnType("nvarchar(32)");
+
+ b.Property("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("Id")
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("CreatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("CvDocumentId")
+ .IsRequired()
+ .HasMaxLength(64)
+ .HasColumnType("nvarchar(64)");
+
+ b.Property("Email")
+ .IsRequired()
+ .HasMaxLength(256)
+ .HasColumnType("nvarchar(256)");
+
+ b.Property("ExpiresAt")
+ .HasColumnType("datetime2");
+
+ b.Property("Language")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)")
+ .HasDefaultValue("en");
+
+ b.Property("Used")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("bit")
+ .HasDefaultValue(false);
+
+ b.HasKey("Id");
+
+ b.ToTable("JobSearchTokens", "cvSearch");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Apis/cv-search-data/cv-search-data.csproj b/Apis/cv-search-data/cv-search-data.csproj
new file mode 100644
index 0000000..7209708
--- /dev/null
+++ b/Apis/cv-search-data/cv-search-data.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ cv-search-data
+ CvSearch.Data
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/Apis/cv-search-data/cv-search-models.csproj b/Apis/cv-search-data/cv-search-models.csproj
new file mode 100644
index 0000000..310b3cf
--- /dev/null
+++ b/Apis/cv-search-data/cv-search-models.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net10.0
+ CvSearch.Models
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/Apis/myai-data/Data/Entities/TemplateEntity.cs b/Apis/myai-data/Data/Entities/TemplateEntity.cs
new file mode 100644
index 0000000..154ad43
--- /dev/null
+++ b/Apis/myai-data/Data/Entities/TemplateEntity.cs
@@ -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; }
+}
diff --git a/Apis/myai-data/Data/MyAiDbContext.cs b/Apis/myai-data/Data/MyAiDbContext.cs
new file mode 100644
index 0000000..6ceb60b
--- /dev/null
+++ b/Apis/myai-data/Data/MyAiDbContext.cs
@@ -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 options) : base(options) { }
+
+ public DbSet Templates => Set();
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.HasDefaultSchema(SchemaName);
+
+ modelBuilder.Entity(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()");
+ });
+ }
+}
diff --git a/Apis/myai-data/Migrations/20260524145351_AddTemplates.Designer.cs b/Apis/myai-data/Migrations/20260524145351_AddTemplates.Designer.cs
new file mode 100644
index 0000000..63cf0c0
--- /dev/null
+++ b/Apis/myai-data/Migrations/20260524145351_AddTemplates.Designer.cs
@@ -0,0 +1,62 @@
+//
+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
+ {
+ ///
+ 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("Key")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Language")
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)");
+
+ b.Property("Description")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)")
+ .HasDefaultValue("");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Key", "Language");
+
+ b.ToTable("Templates", "myAi");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Apis/myai-data/Migrations/20260524145351_AddTemplates.cs b/Apis/myai-data/Migrations/20260524145351_AddTemplates.cs
new file mode 100644
index 0000000..36f47f1
--- /dev/null
+++ b/Apis/myai-data/Migrations/20260524145351_AddTemplates.cs
@@ -0,0 +1,113 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace MyAi.Data.Migrations
+{
+ ///
+ public partial class AddTemplates : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.EnsureSchema(
+ name: "myAi");
+
+ migrationBuilder.CreateTable(
+ name: "Templates",
+ schema: "myAi",
+ columns: table => new
+ {
+ Key = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false),
+ Language = table.Column(type: "nvarchar(8)", maxLength: 8, nullable: false),
+ Value = table.Column(type: "nvarchar(max)", nullable: false),
+ Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false, defaultValue: ""),
+ UpdatedAt = table.Column(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.");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Templates",
+ schema: "myAi");
+ }
+ }
+}
diff --git a/Apis/myai-data/Migrations/MyAiDbContextModelSnapshot.cs b/Apis/myai-data/Migrations/MyAiDbContextModelSnapshot.cs
new file mode 100644
index 0000000..71e85d6
--- /dev/null
+++ b/Apis/myai-data/Migrations/MyAiDbContextModelSnapshot.cs
@@ -0,0 +1,59 @@
+//
+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("Key")
+ .HasMaxLength(128)
+ .HasColumnType("nvarchar(128)");
+
+ b.Property("Language")
+ .HasMaxLength(8)
+ .HasColumnType("nvarchar(8)");
+
+ b.Property("Description")
+ .IsRequired()
+ .ValueGeneratedOnAdd()
+ .HasMaxLength(500)
+ .HasColumnType("nvarchar(500)")
+ .HasDefaultValue("");
+
+ b.Property("UpdatedAt")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("datetime2")
+ .HasDefaultValueSql("SYSUTCDATETIME()");
+
+ b.Property("Value")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Key", "Language");
+
+ b.ToTable("Templates", "myAi");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/Apis/myai-data/Services/DbTemplateService.cs b/Apis/myai-data/Services/DbTemplateService.cs
new file mode 100644
index 0000000..aa9bd50
--- /dev/null
+++ b/Apis/myai-data/Services/DbTemplateService.cs
@@ -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 _logger;
+ private ConcurrentDictionary _cache = new(StringComparer.OrdinalIgnoreCase);
+ private DateTime _loadedAt = DateTime.MinValue;
+ private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);
+
+ public DbTemplateService(IServiceScopeFactory scopeFactory, ILogger 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();
+ var rows = db.Templates.AsNoTracking().ToList();
+ var fresh = new ConcurrentDictionary(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}";
+}
diff --git a/Apis/myai-data/Services/ITemplateService.cs b/Apis/myai-data/Services/ITemplateService.cs
new file mode 100644
index 0000000..1c4f239
--- /dev/null
+++ b/Apis/myai-data/Services/ITemplateService.cs
@@ -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);
+}
diff --git a/Apis/myai-data/myai-data.csproj b/Apis/myai-data/myai-data.csproj
new file mode 100644
index 0000000..4095db1
--- /dev/null
+++ b/Apis/myai-data/myai-data.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ myai-data
+ MyAi.Data
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/Apis/myai-data/myai-models.csproj b/Apis/myai-data/myai-models.csproj
new file mode 100644
index 0000000..cf8d4c5
--- /dev/null
+++ b/Apis/myai-data/myai-models.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net10.0
+ MyAi.Models
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
diff --git a/Apis/rag-api-models/Settings/OllamaSettings.cs b/Apis/rag-api-models/Settings/OllamaSettings.cs
index b75ffe2..e99b845 100644
--- a/Apis/rag-api-models/Settings/OllamaSettings.cs
+++ b/Apis/rag-api-models/Settings/OllamaSettings.cs
@@ -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";
}
diff --git a/Apis/rag-api-models/Settings/OpenAiSettings.cs b/Apis/rag-api-models/Settings/OpenAiSettings.cs
index b1c7f36..80eccbc 100644
--- a/Apis/rag-api-models/Settings/OpenAiSettings.cs
+++ b/Apis/rag-api-models/Settings/OpenAiSettings.cs
@@ -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";
}
diff --git a/Apis/rag-api-models/rag-api-models.csproj b/Apis/rag-api-models/rag-api-models.csproj
index b19eedd..d5098be 100644
--- a/Apis/rag-api-models/rag-api-models.csproj
+++ b/Apis/rag-api-models/rag-api-models.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/Apis/rag-api/Clients/Ai/CachedRagAiClient.cs b/Apis/rag-api/Clients/Ai/CachedRagAiClient.cs
index 4821285..0aa29b2 100644
--- a/Apis/rag-api/Clients/Ai/CachedRagAiClient.cs
+++ b/Apis/rag-api/Clients/Ai/CachedRagAiClient.cs
@@ -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;
diff --git a/Apis/rag-api/Controllers/RagController.cs b/Apis/rag-api/Controllers/RagController.cs
index ecf07c6..d3e8cfc 100644
--- a/Apis/rag-api/Controllers/RagController.cs
+++ b/Apis/rag-api/Controllers/RagController.cs
@@ -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;
diff --git a/Apis/rag-api/Data/Repositories/Contracts/IRagRepository.cs b/Apis/rag-api/Data/Repositories/Contracts/IRagRepository.cs
index 2837287..4993761 100644
--- a/Apis/rag-api/Data/Repositories/Contracts/IRagRepository.cs
+++ b/Apis/rag-api/Data/Repositories/Contracts/IRagRepository.cs
@@ -1,6 +1,6 @@
using Rag.Models;
-namespace Api.Data.Repositories.Contracts;
+namespace Rag.Data.Repositories.Contracts;
public interface IRagRepository
{
diff --git a/Apis/rag-api/Data/Repositories/EfRagRepository.cs b/Apis/rag-api/Data/Repositories/EfRagRepository.cs
index e07b4ee..e644599 100644
--- a/Apis/rag-api/Data/Repositories/EfRagRepository.cs
+++ b/Apis/rag-api/Data/Repositories/EfRagRepository.cs
@@ -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
{
diff --git a/Apis/rag-api/Data/Repositories/VectorSerializer.cs b/Apis/rag-api/Data/Repositories/VectorSerializer.cs
index 2ed02c6..c70d2f3 100644
--- a/Apis/rag-api/Data/Repositories/VectorSerializer.cs
+++ b/Apis/rag-api/Data/Repositories/VectorSerializer.cs
@@ -1,4 +1,4 @@
-namespace Api.Data.Repositories;
+namespace Rag.Data.Repositories;
public static class VectorSerializer
{
diff --git a/Apis/rag-api/Dockerfile b/Apis/rag-api/Dockerfile
index 9878095..5284fbd 100644
--- a/Apis/rag-api/Dockerfile
+++ b/Apis/rag-api/Dockerfile
@@ -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"]
\ No newline at end of file
+ENTRYPOINT ["dotnet", "rag-api.dll"]
diff --git a/Apis/rag-api/Program.cs b/Apis/rag-api/Program.cs
index daadebf..a269169 100644
--- a/Apis/rag-api/Program.cs
+++ b/Apis/rag-api/Program.cs
@@ -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();
- builder.Services.AddScoped();
builder.Services.AddHttpClient();
builder.Services.AddScoped();
builder.Services.AddScoped();
diff --git a/Apis/rag-api/Services/RagService.cs b/Apis/rag-api/Services/RagService.cs
index 9799feb..9a8eab2 100644
--- a/Apis/rag-api/Services/RagService.cs
+++ b/Apis/rag-api/Services/RagService.cs
@@ -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;
diff --git a/Apis/rag-api/rag-api.csproj b/Apis/rag-api/rag-api.csproj
index 91f151b..d2c4dba 100644
--- a/Apis/rag-api/rag-api.csproj
+++ b/Apis/rag-api/rag-api.csproj
@@ -58,28 +58,29 @@
-
-
-
-
-
+
+
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
+
+
+
+
diff --git a/Apis/rag-api/Data/Entities/RagChatCompletionCacheEntity.cs b/Apis/rag-data/Entities/RagChatCompletionCacheEntity.cs
similarity index 81%
rename from Apis/rag-api/Data/Entities/RagChatCompletionCacheEntity.cs
rename to Apis/rag-data/Entities/RagChatCompletionCacheEntity.cs
index 05940b9..c873e60 100644
--- a/Apis/rag-api/Data/Entities/RagChatCompletionCacheEntity.cs
+++ b/Apis/rag-data/Entities/RagChatCompletionCacheEntity.cs
@@ -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;
diff --git a/Apis/rag-api/Data/Entities/RagChunkEntity.cs b/Apis/rag-data/Entities/RagChunkEntity.cs
similarity index 78%
rename from Apis/rag-api/Data/Entities/RagChunkEntity.cs
rename to Apis/rag-data/Entities/RagChunkEntity.cs
index b57467c..6bd1734 100644
--- a/Apis/rag-api/Data/Entities/RagChunkEntity.cs
+++ b/Apis/rag-data/Entities/RagChunkEntity.cs
@@ -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;
diff --git a/Apis/rag-api/Data/Entities/RagDocumentEntity.cs b/Apis/rag-data/Entities/RagDocumentEntity.cs
similarity index 70%
rename from Apis/rag-api/Data/Entities/RagDocumentEntity.cs
rename to Apis/rag-data/Entities/RagDocumentEntity.cs
index 739af12..7b09463 100644
--- a/Apis/rag-api/Data/Entities/RagDocumentEntity.cs
+++ b/Apis/rag-data/Entities/RagDocumentEntity.cs
@@ -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 Chunks { get; set; } = [];
}
diff --git a/Apis/rag-api/Data/Entities/RagEmbeddingCacheEntity.cs b/Apis/rag-data/Entities/RagEmbeddingCacheEntity.cs
similarity index 81%
rename from Apis/rag-api/Data/Entities/RagEmbeddingCacheEntity.cs
rename to Apis/rag-data/Entities/RagEmbeddingCacheEntity.cs
index 63f8132..e96c433 100644
--- a/Apis/rag-api/Data/Entities/RagEmbeddingCacheEntity.cs
+++ b/Apis/rag-data/Entities/RagEmbeddingCacheEntity.cs
@@ -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;
diff --git a/Apis/rag-api/Migrations/20260507140305_InitialRagSchema.Designer.cs b/Apis/rag-data/Migrations/20260507140305_InitialRagSchema.Designer.cs
similarity index 92%
rename from Apis/rag-api/Migrations/20260507140305_InitialRagSchema.Designer.cs
rename to Apis/rag-data/Migrations/20260507140305_InitialRagSchema.Designer.cs
index 3fcaaae..54c078e 100644
--- a/Apis/rag-api/Migrations/20260507140305_InitialRagSchema.Designer.cs
+++ b/Apis/rag-data/Migrations/20260507140305_InitialRagSchema.Designer.cs
@@ -1,6 +1,6 @@
-//
+//
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("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("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("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("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");
});
diff --git a/Apis/rag-api/Migrations/20260507140305_InitialRagSchema.cs b/Apis/rag-data/Migrations/20260507140305_InitialRagSchema.cs
similarity index 99%
rename from Apis/rag-api/Migrations/20260507140305_InitialRagSchema.cs
rename to Apis/rag-data/Migrations/20260507140305_InitialRagSchema.cs
index fc6216c..48b6c3e 100644
--- a/Apis/rag-api/Migrations/20260507140305_InitialRagSchema.cs
+++ b/Apis/rag-data/Migrations/20260507140305_InitialRagSchema.cs
@@ -1,9 +1,9 @@
-using System;
+using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
-namespace Api.Migrations
+namespace Rag.Data.Migrations
{
///
public partial class InitialRagSchema : Migration
diff --git a/Apis/rag-api/Migrations/RagDbContextModelSnapshot.cs b/Apis/rag-data/Migrations/RagDbContextModelSnapshot.cs
similarity index 92%
rename from Apis/rag-api/Migrations/RagDbContextModelSnapshot.cs
rename to Apis/rag-data/Migrations/RagDbContextModelSnapshot.cs
index 908ae81..a409235 100644
--- a/Apis/rag-api/Migrations/RagDbContextModelSnapshot.cs
+++ b/Apis/rag-data/Migrations/RagDbContextModelSnapshot.cs
@@ -1,6 +1,6 @@
-//
+//
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("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("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("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("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");
});
diff --git a/Apis/rag-api/Data/RagDbContext.cs b/Apis/rag-data/RagDbContext.cs
similarity index 98%
rename from Apis/rag-api/Data/RagDbContext.cs
rename to Apis/rag-data/RagDbContext.cs
index 1564ef1..bd5b85a 100644
--- a/Apis/rag-api/Data/RagDbContext.cs
+++ b/Apis/rag-data/RagDbContext.cs
@@ -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
{
diff --git a/Apis/rag-data/rag-data.csproj b/Apis/rag-data/rag-data.csproj
new file mode 100644
index 0000000..207d81d
--- /dev/null
+++ b/Apis/rag-data/rag-data.csproj
@@ -0,0 +1,23 @@
+
+
+
+ net10.0
+ rag-data
+ Rag.Data
+ enable
+ enable
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/Apis/shared-data/Entities/BaseEntity.cs b/Apis/shared-data/Entities/BaseEntity.cs
new file mode 100644
index 0000000..05fd6c8
--- /dev/null
+++ b/Apis/shared-data/Entities/BaseEntity.cs
@@ -0,0 +1,12 @@
+namespace Shared.Data.Entities;
+
+///
+/// Abstract base for all EF entities that carry a surrogate string PK and an audit timestamp.
+/// Entities with a composite PK or a non-Id primary key should NOT inherit this class;
+/// document the exception with a brief comment on the entity.
+///
+public abstract class BaseEntity
+{
+ public required string Id { get; init; }
+ public DateTime CreatedAt { get; init; }
+}
diff --git a/Apis/shared-data/shared-data.csproj b/Apis/shared-data/shared-data.csproj
new file mode 100644
index 0000000..606ae95
--- /dev/null
+++ b/Apis/shared-data/shared-data.csproj
@@ -0,0 +1,9 @@
+
+
+ net10.0
+ shared-data
+ Shared.Data
+ enable
+ enable
+
+
diff --git a/CLAUDE.md b/CLAUDE.md
index 52b200b..27a2b35 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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 \
- --context CvSearchDbContext \
- --project Apis/cv-search-models \
+# cv-matcher-data (schema: cvMatcher)
+dotnet ef migrations add `
+ --context CvMatcherDbContext `
+ --project Apis/cv-matcher-data `
--startup-project Apis/cv-matcher-api
-# Add a migration to cv-matcher-api
-dotnet ef migrations add \
- --context CvMatcherDbContext \
- --project Apis/cv-matcher-api
+# rag-data (schema: rag)
+dotnet ef migrations add `
+ --context RagDbContext `
+ --project Apis/rag-data `
+ --startup-project Apis/rag-api
+
+# cv-search-data (schema: cvSearch)
+dotnet ef migrations add `
+ --context CvSearchDbContext `
+ --project Apis/cv-search-data `
+ --startup-project Apis/cv-matcher-api
+
+# myai-data (schema: myAi)
+dotnet ef migrations add `
+ --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.
diff --git a/CV.pdf b/CV.pdf
new file mode 100644
index 0000000..2c81179
Binary files /dev/null and b/CV.pdf differ
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..7e06527
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,38 @@
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Helpers/startup-helpers/DatabaseExtensions.cs b/Helpers/startup-helpers/DatabaseExtensions.cs
index abe85e3..07c7b60 100644
--- a/Helpers/startup-helpers/DatabaseExtensions.cs
+++ b/Helpers/startup-helpers/DatabaseExtensions.cs
@@ -1,6 +1,6 @@
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
-using Shared.Models.Settings;
+using Common.Settings;
namespace StartupHelpers;
diff --git a/Helpers/startup-helpers/RateLimitingExtensions.cs b/Helpers/startup-helpers/RateLimitingExtensions.cs
index 03e0a6a..5087c4c 100644
--- a/Helpers/startup-helpers/RateLimitingExtensions.cs
+++ b/Helpers/startup-helpers/RateLimitingExtensions.cs
@@ -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;
diff --git a/Helpers/startup-helpers/startup-helpers.csproj b/Helpers/startup-helpers/startup-helpers.csproj
index 00b9f4a..89d7a7c 100644
--- a/Helpers/startup-helpers/startup-helpers.csproj
+++ b/Helpers/startup-helpers/startup-helpers.csproj
@@ -12,19 +12,19 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
diff --git a/Jobs/cv-cleanup-job-models/cv-cleanup-job-models.csproj b/Jobs/cv-cleanup-job-models/cv-cleanup-job-models.csproj
new file mode 100644
index 0000000..1d2bd02
--- /dev/null
+++ b/Jobs/cv-cleanup-job-models/cv-cleanup-job-models.csproj
@@ -0,0 +1,9 @@
+
+
+ net10.0
+ enable
+ enable
+ cv-cleanup-job-models
+ CvCleanup.Job.Models
+
+
diff --git a/Jobs/cv-cleanup-job/Dockerfile b/Jobs/cv-cleanup-job/Dockerfile
index 393a490..d2485e1 100644
--- a/Jobs/cv-cleanup-job/Dockerfile
+++ b/Jobs/cv-cleanup-job/Dockerfile
@@ -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
diff --git a/Jobs/cv-cleanup-job/cv-cleanup-job.csproj b/Jobs/cv-cleanup-job/cv-cleanup-job.csproj
index 9e93dfc..4dd9f40 100644
--- a/Jobs/cv-cleanup-job/cv-cleanup-job.csproj
+++ b/Jobs/cv-cleanup-job/cv-cleanup-job.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/Jobs/cv-search-job-models/cv-search-job-models.csproj b/Jobs/cv-search-job-models/cv-search-job-models.csproj
new file mode 100644
index 0000000..94be90d
--- /dev/null
+++ b/Jobs/cv-search-job-models/cv-search-job-models.csproj
@@ -0,0 +1,9 @@
+
+
+ net10.0
+ enable
+ enable
+ cv-search-job-models
+ CvSearch.Job.Models
+
+
diff --git a/Jobs/cv-search-job/CLAUDE.md b/Jobs/cv-search-job/CLAUDE.md
index 788a1e2..2cc09e1 100644
--- a/Jobs/cv-search-job/CLAUDE.md
+++ b/Jobs/cv-search-job/CLAUDE.md
@@ -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.
diff --git a/Jobs/cv-search-job/Dockerfile b/Jobs/cv-search-job/Dockerfile
index ada4d68..5da6414 100644
--- a/Jobs/cv-search-job/Dockerfile
+++ b/Jobs/cv-search-job/Dockerfile
@@ -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
diff --git a/Jobs/cv-search-job/Program.cs b/Jobs/cv-search-job/Program.cs
index 00733d5..1bdf24a 100644
--- a/Jobs/cv-search-job/Program.cs
+++ b/Jobs/cv-search-job/Program.cs
@@ -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);
});
});
diff --git a/Jobs/cv-search-job/Services/CvSearchEmailSender.cs b/Jobs/cv-search-job/Services/CvSearchEmailSender.cs
index 6c23120..58ef994 100644
--- a/Jobs/cv-search-job/Services/CvSearchEmailSender.cs
+++ b/Jobs/cv-search-job/Services/CvSearchEmailSender.cs
@@ -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;
diff --git a/Jobs/cv-search-job/Services/HtmlJobSearcher.cs b/Jobs/cv-search-job/Services/HtmlJobSearcher.cs
index 1ca539c..fe03132 100644
--- a/Jobs/cv-search-job/Services/HtmlJobSearcher.cs
+++ b/Jobs/cv-search-job/Services/HtmlJobSearcher.cs
@@ -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;
diff --git a/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs b/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs
index d2d50c1..7993736 100644
--- a/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs
+++ b/Jobs/cv-search-job/Tasks/CvSearchJobTask.cs
@@ -1,8 +1,8 @@
using System.Text.Json;
using CvMatcher.Models.Requests;
-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 CvSearchJob.Clients;
using CvSearchJob.Services;
using JobScheduler.Tasks;
diff --git a/Jobs/cv-search-job/cv-search-job.csproj b/Jobs/cv-search-job/cv-search-job.csproj
index 097d9fe..7a695c2 100644
--- a/Jobs/cv-search-job/cv-search-job.csproj
+++ b/Jobs/cv-search-job/cv-search-job.csproj
@@ -9,10 +9,10 @@
-
-
-
-
+
+
+
+
@@ -21,11 +21,11 @@
-
-
+
+
-
+
diff --git a/Jobs/job-scheduler/job-scheduler.csproj b/Jobs/job-scheduler/job-scheduler.csproj
index ddb1c78..9200dc9 100644
--- a/Jobs/job-scheduler/job-scheduler.csproj
+++ b/Jobs/job-scheduler/job-scheduler.csproj
@@ -8,10 +8,10 @@
-
-
-
-
+
+
+
+
diff --git a/docs/skills/general-dev-workflow.md b/docs/skills/general-dev-workflow.md
new file mode 100644
index 0000000..16bfc24
--- /dev/null
+++ b/docs/skills/general-dev-workflow.md
@@ -0,0 +1,230 @@
+---
+name: general-dev-workflow
+description: Structured development workflow for any project. Guides you through Plan → Issue → Implement → Test → PR with checkpoints at each stage to ensure code quality, thorough testing, and well-documented PRs before merge. Use this whenever starting a new feature, bug fix, or change — regardless of tech stack or platform (GitHub, GitLab, Gitea, etc.). Works locally or with remote repositories.
+compatibility: Git, GitHub/GitLab/Gitea (auto-detected)
+---
+
+# General Development Workflow
+
+This skill guides you through a structured, repeatable development workflow that works across any tech stack and version control platform. The goal is to ensure consistent, high-quality code while reducing context-switching and providing clear checkpoints.
+
+## The 5-Phase Workflow
+
+### Phase 1: Plan
+**Goal**: Define the change clearly before writing code.
+
+Start by answering:
+- **What** are you building/fixing? (Brief 1-2 sentence summary)
+- **Why** does it matter? (Problem it solves, value it adds)
+- **Scope**: What's included? What's NOT included? (Define boundaries early)
+- **Success criteria**: How will you know it's done? (Tests pass, performance improves, etc.)
+- **Dependencies**: Does this require changes elsewhere? Do other features depend on this?
+- **Risks**: What could go wrong? (Breaking changes, performance issues, etc.)
+
+**Output**: A clear plan document or issue description ready for the next phase.
+
+**Checkpoint**: Does the plan make sense? Is scope realistic? Have you thought through edge cases?
+
+---
+
+### Phase 2: Create Issue (in your VCS)
+**Goal**: Formally track the work — and the time spent on it — starting from the moment the plan is approved.
+
+> ⚠️ **Create the issue only after the plan has been reviewed and approved.** The issue marks the official start of implementation, so its creation timestamp doubles as the implementation start time. Do not create it speculatively while the plan is still being discussed.
+
+Steps:
+1. **Detect your VCS platform**: GitHub, GitLab, Gitea, or local-only?
+2. **Create the issue** with:
+ - Title matching your plan summary
+ - Description from Phase 1 (what, why, scope, success criteria)
+ - A `## Time tracking` section at the bottom with `Started: ` — this lets you measure implementation duration later
+ - Any relevant labels/tags (bug, feature, enhancement, etc.)
+ - Assignment (if applicable)
+3. **For local-only work**: Create a text file or branch-based tracking if you prefer
+
+**Output**: Issue link (or local tracking document) that you'll reference in commits and in the PR.
+
+**Checkpoint**: Can someone else understand the work from this issue? Would they know how to verify it's done?
+
+> 🕐 **Do not close the issue here.** It stays open until the PR is merged (Phase 5).
+
+---
+
+### Phase 3: Implement
+**Goal**: Write code that solves the problem defined in the plan.
+
+**Before you start coding:**
+- Create a branch (e.g., `feature/user-auth`, `fix/api-latency`) that references your issue
+- Name the branch clearly so it's easy to track what you're working on
+
+**While coding:**
+- Break work into logical, reviewable commits (not one giant commit)
+- Write commit messages that explain *why*, not just *what*
+ - Bad: "fix bug"
+ - Good: "fix race condition in event handler by adding mutex lock — prevents duplicate events when rapidly clicking"
+- Follow the project's code style and conventions
+- Write code you'd be comfortable having someone else maintain
+
+**As you finish sections:**
+- Test locally (Phase 4 will be formal testing, but catch obvious issues early)
+- If you realize the scope has changed, update your Phase 1 plan and issue
+
+**Output**: Commits on your branch that are ready for review.
+
+**Checkpoint**: Does each commit stand alone? Would a reviewer understand why each change was made?
+
+---
+
+### Phase 4: Test
+**Goal**: Verify the code works correctly and doesn't break existing functionality.
+
+**Write tests for your changes:**
+- Unit tests for individual functions/methods
+- Integration tests if your code touches multiple systems
+- Edge case tests (null inputs, boundary conditions, etc.)
+- Regression tests to ensure you didn't break something else
+
+**Run existing tests:**
+- Do all tests pass? Fix failures in Phase 3 code
+- Check code coverage. Did you test your changes?
+
+**Manual testing:**
+- Does the feature work as described in the plan?
+- Did you test the success criteria from Phase 1?
+- Test on different inputs/environments if relevant
+
+**Test results document** (save for Phase 5 PR):
+- Which tests were added?
+- What's the coverage? (old → new)
+- Any manual testing notes
+- Known limitations or edge cases not yet tested
+
+**Output**: Passing tests, code coverage report, test summary.
+
+**Checkpoint**: Can you confidently say the code works? Are there any untested paths you're worried about?
+
+---
+
+### Phase 5: Create Pull Request
+**Goal**: Get code reviewed and merged into `main`, and automatically close the tracking issue on merge.
+
+**Before opening the PR:**
+- Rebase onto the latest `main`/`master` (avoid merge commits if possible)
+- Verify all tests still pass
+- Check for merge conflicts
+- The PR **must target `main`** (or the project's primary branch) — never merge directly without a PR
+
+**PR Description** (use this template):
+```
+## What
+Brief summary of the change (one sentence)
+
+## Why
+Problem it solves / value it adds (2-3 sentences)
+
+## Changes
+- Major code change 1
+- Major code change 2
+- (Be specific—help reviewers understand scope)
+
+## Testing
+- Tests added: [list test files or test count]
+- Coverage change: [old % → new %]
+- Manual testing: [describe what was tested]
+
+## Risk Assessment
+- Breaking changes? [yes/no, if yes: explain migration path]
+- Performance impact? [none/minor/significant]
+- Closes #
+
+## Checklist
+- [ ] All tests passing
+- [ ] Code review ready
+- [ ] Documentation updated (if needed)
+- [ ] No merge conflicts
+```
+
+> 🔗 **Always include `Closes #`** in the PR body. On GitHub and Gitea this auto-closes the issue the moment the PR is merged into the target branch — no manual close needed.
+
+**During review:**
+- Respond to feedback promptly
+- If changes are requested, make them and push again (don't force-push)
+- Ask for clarification if feedback is unclear
+
+**Merge criteria** (before merging):
+- ✅ All tests pass
+- ✅ Approved by at least one reviewer (adapt to your team's policy)
+- ✅ No unresolved discussions
+- ✅ No merge conflicts
+
+**After the PR is merged:**
+- Verify the issue was automatically closed by the `Closes #N` keyword
+- If the platform did not auto-close it, close it now and add a comment with the merge commit SHA and the elapsed time (issue `Created` timestamp → merge timestamp)
+- **Never close the issue before the PR is merged** — an open issue means work is still in progress
+
+**Output**: Merged PR with clear history and review comments; issue automatically closed.
+
+**Checkpoint**: Is the code in `main`? Is the issue closed? Did you record the implementation duration in the issue?
+
+---
+
+## Workflow Summary (Quick Reference)
+
+| Phase | Input | Output | Checkpoint |
+|-------|-------|--------|-----------|
+| 1. Plan | Problem description | Approved plan document | Scope and success criteria defined? Plan reviewed and approved? |
+| 2. Issue | **Approved** plan | Issue/ticket link + start timestamp | Is it understandable to others? Issue open (not closed)? |
+| 3. Implement | Issue/ticket | Commits on feature branch | Are commits logical and well-messaged? |
+| 4. Test | Code on branch | Passing tests + coverage report | Are edge cases covered? |
+| 5. PR | Tests passing | PR merged into `main`; issue auto-closed via `Closes #N` | Is `main` updated? Is the issue closed? Duration recorded? |
+
+---
+
+## Tips for Success
+
+**Keep phases focused.** Don't mix planning with implementation. Don't test in Phase 2. This separation helps catch problems early.
+
+**Commit frequently.** Small commits are easier to review, easier to revert if needed, and easier to understand. Aim for 50-200 lines per commit.
+
+**Write for your future self.** Six months from now, you'll read your own commits and PRs. Make them clear.
+
+**Catch blocking issues early.** If Phase 1 reveals something complex or risky, discuss it with your team *before* you code.
+
+**Adapt to your team.** If your team requires code owners approval, or has a specific PR template, use those instead of the defaults here. The phases stay the same; the details adapt.
+
+---
+
+## For Different Platforms
+
+**GitHub**: Issues are native. Use GitHub's PR template and auto-linking (`Closes #123`).
+
+**GitLab**: Merge Requests (MRs) replace PRs. Pipeline status is built-in. Use GitLab's merge request template.
+
+**Gitea**: Similar to GitHub. Use Gitea's PR templates and linking.
+
+**Local/No Platform**: Create a `.github` folder locally with your own issue and PR templates. Track work via commit messages and branch names.
+
+---
+
+## Common Questions
+
+**Q: Can I skip Phase 2 (create issue)?**
+A: If you're working solo on a small fix, maybe. But issues are useful for: remembering *why* you made a change, helping others understand work in progress, tracking what's been done, and measuring how long implementation actually takes. Recommended even for solo developers.
+
+**Q: When exactly should I create the issue?**
+A: Only after the plan (Phase 1) has been reviewed and approved. The issue creation timestamp marks the official start of implementation and serves as the start of your time-tracking window. Creating it during planning inflates the recorded duration.
+
+**Q: When should I close the issue?**
+A: Never close it manually before the PR is merged. Use `Closes #N` in the PR body — the platform (GitHub/Gitea) will close it automatically when the PR merges into the target branch. If auto-close doesn't trigger, close it immediately after merge and note the elapsed time.
+
+**Q: How do I track implementation time?**
+A: The issue creation time is your start. The issue close time (= PR merge time) is your end. To make this explicit, add a `## Time tracking` section to the issue body with `Started: ` when you create it. After merge, update or comment with `Completed: — Duration: X hours/days`.
+
+**Q: What if my plan changes mid-implementation?**
+A: Update your issue/plan (Phase 1) to reflect the new scope. Let your team know if scope grew significantly.
+
+**Q: How big should commits be?**
+A: Aim for "one logical change per commit." If you're changing authentication and fixing a typo, those are two commits. A good rule: can you describe the commit in one clear sentence without "and"?
+
+**Q: Do I need tests for everything?**
+A: Write tests for anything that could break. UI color changes? Maybe not. API endpoint behavior? Definitely. If you're unsure, write the test.
diff --git a/myAi.sln b/myAi.sln
index 9a74df4..690f362 100644
--- a/myAi.sln
+++ b/myAi.sln
@@ -17,13 +17,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Apis", "Apis", "{0FE6558F-2
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-matcher-api-models", "Apis\cv-matcher-api-models\cv-matcher-api-models.csproj", "{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-search-models", "Apis\cv-search-models\cv-search-models.csproj", "{B2C3D4E5-F6A7-4890-BCDE-F01234567890}"
-EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "api-models", "Apis\api-models\api-models.csproj", "{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "rag-api-models", "Apis\rag-api-models\rag-api-models.csproj", "{6A1ADA81-28E9-4A64-A32D-0755876D5EB7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "shared-models", "Apis\shared-models\shared-models.csproj", "{185A8BB0-344A-4856-AEB4-213866EB2EE7}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "common", "Apis\common\common.csproj", "{185A8BB0-344A-4856-AEB4-213866EB2EE7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Helpers", "Helpers", "{43E9CD21-25B6-4CB4-B94E-5B953B2E1284}"
EndProject
@@ -39,7 +37,23 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-search-job", "Jobs\cv-se
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "job-scheduler", "Jobs\job-scheduler\job-scheduler.csproj", "{A19D2776-B935-BD35-4AB1-3FCE2092805A}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "myai-models", "Apis\myai-models\myai-models.csproj", "{3BE2E134-E773-4574-ABDD-175F00E4932E}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "shared-data", "Apis\shared-data\shared-data.csproj", "{1B66E492-1830-4229-A8EF-135714BEADA2}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "myai-data", "Apis\myai-data\myai-data.csproj", "{9582CD83-0B49-4255-9BA6-BC045C3984AD}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-search-data", "Apis\cv-search-data\cv-search-data.csproj", "{CFC1AED5-72BF-4E84-92B6-65819A5AC961}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "rag-data", "Apis\rag-data\rag-data.csproj", "{31D58517-29D8-46E9-AEAC-F43FDE540590}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-matcher-data", "Apis\cv-matcher-data\cv-matcher-data.csproj", "{92CA82EB-E558-44E7-9185-6FF8B8299C2A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-cleanup-job-models", "Jobs\cv-cleanup-job-models\cv-cleanup-job-models.csproj", "{02DE69CD-19E6-43C0-8916-DB98E5B5CA89}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "cv-search-job-models", "Jobs\cv-search-job-models\cv-search-job-models.csproj", "{069365DB-1916-4C38-A90D-5E909BD9EDD0}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Models", "Models", "{A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Data", "Data", "{D4E5F6A7-B8C9-4012-3456-789ABCDEF012}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -119,18 +133,6 @@ Global
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|x64.Build.0 = Release|Any CPU
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|x86.ActiveCfg = Release|Any CPU
{D09DA1C2-3DC5-48E7-9F5B-739CA41174F1}.Release|x86.Build.0 = Release|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x64.ActiveCfg = Debug|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x64.Build.0 = Debug|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x86.ActiveCfg = Debug|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Debug|x86.Build.0 = Debug|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|Any CPU.Build.0 = Release|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x64.ActiveCfg = Release|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x64.Build.0 = Release|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x86.ActiveCfg = Release|Any CPU
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890}.Release|x86.Build.0 = Release|Any CPU
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769}.Debug|x64.ActiveCfg = Debug|Any CPU
@@ -227,37 +229,115 @@ Global
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|x64.Build.0 = Release|Any CPU
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|x86.ActiveCfg = Release|Any CPU
{A19D2776-B935-BD35-4AB1-3FCE2092805A}.Release|x86.Build.0 = Release|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x64.ActiveCfg = Debug|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x64.Build.0 = Debug|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x86.ActiveCfg = Debug|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Debug|x86.Build.0 = Debug|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|Any CPU.Build.0 = Release|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x64.ActiveCfg = Release|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x64.Build.0 = Release|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x86.ActiveCfg = Release|Any CPU
- {3BE2E134-E773-4574-ABDD-175F00E4932E}.Release|x86.Build.0 = Release|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|x64.Build.0 = Debug|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Debug|x86.Build.0 = Debug|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|x64.ActiveCfg = Release|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|x64.Build.0 = Release|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|x86.ActiveCfg = Release|Any CPU
+ {1B66E492-1830-4229-A8EF-135714BEADA2}.Release|x86.Build.0 = Release|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|x64.Build.0 = Debug|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Debug|x86.Build.0 = Debug|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|Any CPU.Build.0 = Release|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|x64.ActiveCfg = Release|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|x64.Build.0 = Release|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|x86.ActiveCfg = Release|Any CPU
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD}.Release|x86.Build.0 = Release|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|x64.Build.0 = Debug|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Debug|x86.Build.0 = Debug|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|x64.ActiveCfg = Release|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|x64.Build.0 = Release|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|x86.ActiveCfg = Release|Any CPU
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961}.Release|x86.Build.0 = Release|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|x64.Build.0 = Debug|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Debug|x86.Build.0 = Debug|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|Any CPU.Build.0 = Release|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|x64.ActiveCfg = Release|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|x64.Build.0 = Release|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|x86.ActiveCfg = Release|Any CPU
+ {31D58517-29D8-46E9-AEAC-F43FDE540590}.Release|x86.Build.0 = Release|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|x64.Build.0 = Debug|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Debug|x86.Build.0 = Debug|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|x64.ActiveCfg = Release|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|x64.Build.0 = Release|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|x86.ActiveCfg = Release|Any CPU
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A}.Release|x86.Build.0 = Release|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|x64.Build.0 = Debug|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Debug|x86.Build.0 = Debug|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|Any CPU.Build.0 = Release|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|x64.ActiveCfg = Release|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|x64.Build.0 = Release|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|x86.ActiveCfg = Release|Any CPU
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89}.Release|x86.Build.0 = Release|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|x64.Build.0 = Debug|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Debug|x86.Build.0 = Debug|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x64.ActiveCfg = Release|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x64.Build.0 = Release|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x86.ActiveCfg = Release|Any CPU
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{16F8CA9D-341F-47B8-8E34-F4C5B3E72F3C} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
+ {B0A3EAB7-759A-448A-A906-52DF75A70016} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
{A63E1C1A-4A78-49F4-9F5C-D43783294861} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
{C40F5025-B0A6-4B25-B4A2-7EA568E06C40} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
- {D09DA1C2-3DC5-48E7-9F5B-739CA41174F1} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
- {B2C3D4E5-F6A7-4890-BCDE-F01234567890} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
- {FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
- {6A1ADA81-28E9-4A64-A32D-0755876D5EB7} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
- {185A8BB0-344A-4856-AEB4-213866EB2EE7} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
{7446D193-8636-4E58-96E4-0C8CB8790679} = {43E9CD21-25B6-4CB4-B94E-5B953B2E1284}
{4EDDEE9A-E9C7-4972-9C4A-3177611CCFE3} = {43E9CD21-25B6-4CB4-B94E-5B953B2E1284}
{E7F21C94-6D88-4E9B-A12F-9C3E8D5B7A41} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789}
{C3D4E5F6-A7B8-4901-CDEF-012345678901} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789}
{A19D2776-B935-BD35-4AB1-3FCE2092805A} = {F1A2B3C4-D5E6-4789-ABCD-EF0123456789}
- {3BE2E134-E773-4574-ABDD-175F00E4932E} = {0FE6558F-2157-47F2-A835-558416CE0E2B}
+ {D09DA1C2-3DC5-48E7-9F5B-739CA41174F1} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}
+ {FB5EAA9E-1B83-41E4-A3BC-F4B7D1AA0769} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}
+ {6A1ADA81-28E9-4A64-A32D-0755876D5EB7} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}
+ {185A8BB0-344A-4856-AEB4-213866EB2EE7} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}
+ {02DE69CD-19E6-43C0-8916-DB98E5B5CA89} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}
+ {069365DB-1916-4C38-A90D-5E909BD9EDD0} = {A9B8C7D6-E5F4-4321-ABCD-FEDCBA987654}
+ {1B66E492-1830-4229-A8EF-135714BEADA2} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012}
+ {9582CD83-0B49-4255-9BA6-BC045C3984AD} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012}
+ {CFC1AED5-72BF-4E84-92B6-65819A5AC961} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012}
+ {31D58517-29D8-46E9-AEAC-F43FDE540590} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012}
+ {92CA82EB-E558-44E7-9185-6FF8B8299C2A} = {D4E5F6A7-B8C9-4012-3456-789ABCDEF012}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6246A67B-299E-4E64-8DBE-1A66771E7C67}
diff --git a/web/web.csproj b/web/web.csproj
index eea6815..2e28118 100644
--- a/web/web.csproj
+++ b/web/web.csproj
@@ -9,7 +9,7 @@
-
-
+
+