155 lines
6.7 KiB
C#
155 lines
6.7 KiB
C#
using Microsoft.Extensions.DependencyInjection;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Microsoft.Extensions.AI;
|
|
using NexusReader.Application.Common;
|
|
using GeminiDotnet;
|
|
using GeminiDotnet.Extensions.AI;
|
|
using NexusReader.Data.Persistence;
|
|
|
|
using NexusReader.Application.Abstractions.Messaging;
|
|
using NexusReader.Application.Abstractions.Persistence;
|
|
using NexusReader.Application.Abstractions.Services;
|
|
using NexusReader.Infrastructure.Persistence;
|
|
using NexusReader.Infrastructure.RealTime;
|
|
using NexusReader.Infrastructure.Services;
|
|
using NexusReader.Infrastructure.Configuration;
|
|
using Polly;
|
|
using Polly.Retry;
|
|
using NexusReader.Domain.Entities;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using NexusReader.Application.Security.Authorization;
|
|
using Qdrant.Client;
|
|
using Neo4j.Driver;
|
|
using Hangfire;
|
|
using Hangfire.PostgreSql;
|
|
|
|
namespace NexusReader.Infrastructure;
|
|
|
|
public static class DependencyInjection
|
|
{
|
|
public static IServiceCollection AddInfrastructure(this IServiceCollection services, IConfiguration configuration)
|
|
{
|
|
var pgConnectionString = configuration.GetConnectionString("PostgresConnection");
|
|
if (!string.IsNullOrEmpty(pgConnectionString))
|
|
{
|
|
services.AddDbContextFactory<AppDbContext>(options =>
|
|
options.UseNpgsql(pgConnectionString),
|
|
ServiceLifetime.Scoped);
|
|
|
|
// Also register a scoped DbContext for repositories that need it
|
|
services.AddDbContext<AppDbContext>(options =>
|
|
options.UseNpgsql(pgConnectionString));
|
|
}
|
|
else
|
|
{
|
|
var sqliteConnectionString = configuration.GetConnectionString("SqliteConnection") ?? "Data Source=nexus.db";
|
|
services.AddDbContextFactory<AppDbContext>(options =>
|
|
options.UseSqlite(sqliteConnectionString),
|
|
ServiceLifetime.Scoped);
|
|
|
|
services.AddDbContext<AppDbContext>(options =>
|
|
options.UseSqlite(sqliteConnectionString));
|
|
}
|
|
|
|
// Qdrant Client registration
|
|
var qdrantUrl = configuration.GetConnectionString("QdrantConnection") ?? "http://localhost:6334";
|
|
services.AddSingleton<QdrantClient>(sp => new QdrantClient(new Uri(qdrantUrl)));
|
|
|
|
// Neo4j Driver registration (supports optional authentication)
|
|
var neo4jUrl = configuration.GetConnectionString("Neo4jConnection") ?? "bolt://localhost:7687";
|
|
var neo4jUser = configuration["Neo4j:Username"];
|
|
var neo4jPass = configuration["Neo4j:Password"];
|
|
var neo4jAuth = !string.IsNullOrEmpty(neo4jUser)
|
|
? AuthTokens.Basic(neo4jUser, neo4jPass ?? string.Empty)
|
|
: AuthTokens.None;
|
|
services.AddSingleton<IDriver>(sp => GraphDatabase.Driver(neo4jUrl, neo4jAuth));
|
|
|
|
// Hangfire registration
|
|
if (!string.IsNullOrEmpty(pgConnectionString))
|
|
{
|
|
services.AddHangfire(config => config
|
|
.UseRecommendedSerializerSettings()
|
|
.UsePostgreSqlStorage(options => options.UseNpgsqlConnection(pgConnectionString)));
|
|
services.AddHangfireServer();
|
|
}
|
|
|
|
services.Configure<AiSettings>(configuration.GetSection(AiSettings.SectionName));
|
|
services.Configure<StripeSettings>(configuration.GetSection(StripeSettings.SectionName));
|
|
services.Configure<RagMonetizationOptions>(configuration.GetSection(RagMonetizationOptions.SectionName));
|
|
var aiSettings = configuration.GetSection(AiSettings.SectionName).Get<AiSettings>() ?? new AiSettings();
|
|
|
|
if (string.IsNullOrWhiteSpace(aiSettings.ApiKey) || aiSettings.ApiKey == "PLACEHOLDER")
|
|
{
|
|
Console.WriteLine("[Infrastructure] WARNING: AI API Key is missing or placeholder!");
|
|
}
|
|
|
|
services.AddResiliencePipeline("ai-retry", builder =>
|
|
{
|
|
builder.AddRetry(new RetryStrategyOptions
|
|
{
|
|
ShouldHandle = new PredicateBuilder().Handle<Exception>(ex =>
|
|
ex.Message.Contains("429") ||
|
|
ex.Message.Contains("Too Many Requests") ||
|
|
ex.Message.Contains("quota") ||
|
|
ex.Message.Contains("503") ||
|
|
ex.Message.Contains("ServiceUnavailable") ||
|
|
ex.Message.Contains("demand")),
|
|
BackoffType = DelayBackoffType.Exponential,
|
|
UseJitter = true,
|
|
MaxRetryAttempts = aiSettings.RetryAttempts,
|
|
Delay = TimeSpan.FromSeconds(2)
|
|
});
|
|
});
|
|
|
|
services.AddChatClient(new GeminiChatClient(new GeminiClientOptions
|
|
{
|
|
ApiKey = aiSettings.ApiKey,
|
|
ModelId = aiSettings.Model
|
|
}));
|
|
|
|
services.AddEmbeddingGenerator(new GeminiEmbeddingGenerator(new GeminiClientOptions
|
|
{
|
|
ApiKey = aiSettings.ApiKey,
|
|
ModelId = aiSettings.EmbeddingModel ?? "gemini-embedding-001"
|
|
}));
|
|
|
|
// Application-layer service implementations
|
|
services.AddScoped<IKnowledgeService, KnowledgeService>();
|
|
services.AddTransient<IEpubReader, EpubReaderService>();
|
|
services.AddTransient<IEpubMetadataExtractor, EpubMetadataExtractor>();
|
|
services.AddTransient<IEpubExtractor, EpubExtractor>();
|
|
|
|
// Fix #4: Scoped instead of Singleton — BookStorageService uses path resolution
|
|
// that is environment-specific and incompatible with Singleton lifetime in MAUI.
|
|
services.AddScoped<IBookStorageService, BookStorageService>();
|
|
|
|
// Fix #1: Ebook repository (scoped, matches AppDbContext lifetime)
|
|
services.AddScoped<IEbookRepository, EbookRepository>();
|
|
services.AddScoped<IQuizResultRepository, QuizResultRepository>();
|
|
services.AddScoped<IConceptsMapReadRepository, ConceptsMapReadRepository>();
|
|
services.AddScoped<IUserLibraryStore, UserLibraryStore>();
|
|
services.AddScoped<IUserReadingStateStore, UserReadingStateStore>();
|
|
services.AddScoped<IVectorSearchStore, VectorSearchStore>();
|
|
|
|
// Fix #2: SignalR broadcaster (scoped, wraps IHubContext which is itself a singleton wrapper)
|
|
services.AddScoped<ISyncBroadcaster, SignalRSyncBroadcaster>();
|
|
|
|
services.AddAuthorizationCore(options =>
|
|
{
|
|
options.AddPolicy("ProUser", policy => policy.Requirements.Add(new ProUserRequirement()));
|
|
});
|
|
|
|
services.AddScoped<IAuthorizationHandler, ProUserHandler>();
|
|
services.AddScoped<IInfrastructureMarker, InfrastructureMarker>();
|
|
|
|
return services;
|
|
}
|
|
|
|
public static System.Reflection.Assembly Assembly => typeof(DependencyInjection).Assembly;
|
|
}
|
|
|
|
public interface IInfrastructureMarker { }
|
|
internal class InfrastructureMarker : IInfrastructureMarker { }
|