fix: migrate to IDbContextFactory and remove direct AppDbContext from DI (#11)

Reviewed-on: #11
Co-authored-by: Marek Jasiński <jasins.marek@gmail.com>
Co-committed-by: Marek Jasiński <jasins.marek@gmail.com>
This commit was merged in pull request #11.
This commit is contained in:
2026-05-07 16:39:21 +00:00
committed by Marek Jaisński
parent 3faecbb639
commit 2248a2b757
35 changed files with 983 additions and 215 deletions
@@ -1,15 +0,0 @@
using Microsoft.EntityFrameworkCore;
using NexusReader.Domain.Entities;
namespace NexusReader.Application.Abstractions.Persistence;
public interface IApplicationDbContext
{
DbSet<SemanticKnowledgeCache> SemanticKnowledgeCache { get; }
DbSet<KnowledgeUnit> KnowledgeUnits { get; }
DbSet<KnowledgeUnitLink> KnowledgeUnitLinks { get; }
DbSet<Ebook> Ebooks { get; }
DbSet<QuizResult> QuizResults { get; }
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
}
@@ -5,5 +5,6 @@ public record SubscriptionPlanDto
public int Id { get; init; }
public string Name { get; init; } = string.Empty;
public int AITokenLimit { get; init; }
public bool IsUnlimitedTokens { get; init; }
public decimal MonthlyPrice { get; init; }
}
@@ -2,6 +2,7 @@
<ItemGroup>
<ProjectReference Include="..\NexusReader.Domain\NexusReader.Domain.csproj" />
<ProjectReference Include="..\NexusReader.Data\NexusReader.Data.csproj" />
</ItemGroup>
<ItemGroup>
@@ -13,7 +14,7 @@
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="10.0.7" />
<PackageReference Include="Microsoft.Extensions.AI" Version="10.5.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Core" Version="10.0.7" />
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.2.1" />
<PackageReference Include="Pgvector.EntityFrameworkCore" Version="0.3.0" />
</ItemGroup>
<PropertyGroup>
@@ -4,7 +4,8 @@ using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.AI;
using NexusReader.Application.DTOs.AI;
using NexusReader.Application.Abstractions.Persistence;
using NexusReader.Data.Persistence;
using Pgvector;
using Pgvector.EntityFrameworkCore;
using System.Text.Json;
@@ -16,14 +17,14 @@ public record SearchLibrarySemanticallyQuery(string QueryText, string TenantId,
public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibrarySemanticallyQuery, Result<List<SemanticSearchResultDto>>>
{
private readonly IApplicationDbContext _dbContext;
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
private readonly IEmbeddingGenerator<string, Embedding<float>> _embeddingGenerator;
public SearchLibrarySemanticallyQueryHandler(
IApplicationDbContext dbContext,
IDbContextFactory<AppDbContext> dbContextFactory,
IEmbeddingGenerator<string, Embedding<float>> embeddingGenerator)
{
_dbContext = dbContext;
_dbContextFactory = dbContextFactory;
_embeddingGenerator = embeddingGenerator;
}
@@ -34,6 +35,7 @@ public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibra
return Result.Fail("Query text cannot be empty.");
}
using var dbContext = _dbContextFactory.CreateDbContext();
try
{
// 1. Generate embedding for user query
@@ -41,7 +43,7 @@ public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibra
var queryVector = new Vector(embeddingResponse.First().Vector.ToArray());
// 2. Perform Cosine Similarity Search on Knowledge Units
var candidates = await _dbContext.KnowledgeUnits
var candidates = await dbContext.KnowledgeUnits
.AsNoTracking()
.Where(x => (x.TenantId == request.TenantId || x.TenantId == "global") && x.Vector != null)
.OrderBy(x => x.Vector!.CosineDistance(queryVector))
@@ -51,7 +53,7 @@ public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibra
if (!candidates.Any())
{
// Fallback to legacy cache if no granular units found
var legacyResults = await _dbContext.SemanticKnowledgeCache
var legacyResults = await dbContext.SemanticKnowledgeCache
.AsNoTracking()
.Where(x => x.TenantId == request.TenantId && x.Vector != null)
.OrderBy(x => x.Vector!.CosineDistance(queryVector))
@@ -68,13 +70,13 @@ public class SearchLibrarySemanticallyQueryHandler : IRequestHandler<SearchLibra
// 3. Graph Expansion: Pull related units (e.g. Definitions, Next steps)
var candidateIds = candidates.Select(c => c.Id).ToList();
var links = await _dbContext.KnowledgeUnitLinks
var links = await dbContext.KnowledgeUnitLinks
.AsNoTracking()
.Where(l => candidateIds.Contains(l.SourceUnitId) && (l.RelationType == "Defines" || l.RelationType == "Next"))
.ToListAsync(cancellationToken);
var relatedIds = links.Select(l => l.TargetUnitId).Distinct().ToList();
var relatedUnits = await _dbContext.KnowledgeUnits
var relatedUnits = await dbContext.KnowledgeUnits
.AsNoTracking()
.Where(u => relatedIds.Contains(u.Id))
.ToDictionaryAsync(u => u.Id, cancellationToken);
@@ -2,16 +2,19 @@ using System.Security.Claims;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using NexusReader.Domain.Entities;
using Microsoft.EntityFrameworkCore;
using NexusReader.Data.Persistence;
namespace NexusReader.Application.Security.Authorization;
public class ProUserHandler : AuthorizationHandler<ProUserRequirement>
{
private readonly UserManager<NexusUser> _userManager;
public ProUserHandler(UserManager<NexusUser> userManager)
private readonly IDbContextFactory<AppDbContext> _dbContextFactory;
public ProUserHandler(IDbContextFactory<AppDbContext> dbContextFactory)
{
_userManager = userManager;
_dbContextFactory = dbContextFactory;
}
protected override async Task HandleRequirementAsync(
@@ -24,14 +27,18 @@ public class ProUserHandler : AuthorizationHandler<ProUserRequirement>
return;
}
var user = await _userManager.FindByIdAsync(userId);
using var db = _dbContextFactory.CreateDbContext();
var user = await db.Users
.Include(u => u.SubscriptionPlan)
.FirstOrDefaultAsync(u => u.Id == userId);
if (user == null)
{
return;
}
// Rule 1: Explicit Pro plan
if (user.SubscriptionPlanId == SubscriptionPlan.ProId)
// Rule 1: Unlimited access
if (user.SubscriptionPlan?.IsUnlimitedTokens == true)
{
context.Succeed(requirement);
return;