refactor: transition to DbContextFactory pattern, enhance database lifecycle management

- Replace direct `BgpDbContext` instantiation with `IDbContextFactory` for efficient context creation and scoped usage.
- Refactor `PeerStore` to leverage scoped `DbContext` for improved resource management.
- Update `Program.cs` to streamline database initialization logic and ensure proper setup.
- Add helper for client IP resolution in API requests, supporting `X-Real-IP` and `X-Forwarded-For` headers.
- Modify `Dockerfile` to include missing provider project and adjust exposed API port configuration.
This commit is contained in:
Mikhail Movchan 2026-06-10 03:37:50 +03:00
parent a774418abd
commit d0e1cb88c7
5 changed files with 111 additions and 68 deletions

View file

@ -5,30 +5,17 @@ namespace BGPLite.Api;
public class BgpDbContext : DbContext
{
private readonly string _dbPath;
public DbSet<Peer> Peers => Set<Peer>();
public bool IsNewDatabase { get; }
public BgpDbContext(string dbPath)
public BgpDbContext(DbContextOptions<BgpDbContext> options) : base(options) { }
public static void Initialize(BgpDbContext db)
{
_dbPath = dbPath;
var dir = Path.GetDirectoryName(dbPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
IsNewDatabase = !File.Exists(dbPath);
Database.EnsureCreated();
Peers.Where(p => p.Status == "active").ExecuteUpdate(
db.Database.EnsureCreated();
db.Peers.Where(p => p.Status == "active").ExecuteUpdate(
s => s.SetProperty(p => p.Status, "inactive"));
}
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlite($"Data Source={_dbPath}");
}
protected override void OnModelCreating(ModelBuilder model)
{
model.Entity<Peer>(e =>

View file

@ -229,7 +229,7 @@ public sealed class ManagementApi : IHostedService, IDisposable
private ApiResponse HandleGetMe(HttpListenerContext ctx)
{
var clientIp = ctx.Request.RemoteEndPoint?.Address.ToString() ?? "unknown";
var clientIp = GetClientIp(ctx);
var peerInfo = _store.GetPeerByIp(clientIp);
if (peerInfo is null)
@ -568,6 +568,21 @@ public sealed class ManagementApi : IHostedService, IDisposable
#region Helpers
private static string GetClientIp(HttpListenerContext ctx)
{
var realIp = ctx.Request.Headers["X-Real-IP"];
if (!string.IsNullOrEmpty(realIp)) return realIp;
var forwarded = ctx.Request.Headers["X-Forwarded-For"];
if (!string.IsNullOrEmpty(forwarded))
{
var first = forwarded.Split(',')[0].Trim();
if (first.Length > 0) return first;
}
return ctx.Request.RemoteEndPoint?.Address.ToString() ?? "unknown";
}
private static uint ParseCommunity(string community)
{
var colon = community.IndexOf(':');

View file

@ -6,33 +6,35 @@ namespace BGPLite.Api;
public sealed class PeerStore : IPeerStore
{
private readonly BgpDbContext _db;
private readonly IDbContextFactory<BgpDbContext> _dbFactory;
public PeerStore(BgpDbContext db) => _db = db;
public PeerStore(IDbContextFactory<BgpDbContext> dbFactory) => _dbFactory = dbFactory;
public string CreatePeer(string ip, uint asn, string? description)
{
var existing = _db.Peers.FirstOrDefault(p => p.Ip == ip);
using var db = _dbFactory.CreateDbContext();
var existing = db.Peers.FirstOrDefault(p => p.Ip == ip);
if (existing is not null)
{
existing.Asn = asn;
existing.Description = description;
_db.SaveChanges();
db.SaveChanges();
return existing.Id;
}
var peer = new Peer { Ip = ip, Asn = asn, Description = description };
_db.Peers.Add(peer);
_db.SaveChanges();
db.Peers.Add(peer);
db.SaveChanges();
return peer.Id;
}
public void UpsertPeer(string ip, uint asn)
{
var peer = _db.Peers.FirstOrDefault(p => p.Ip == ip);
using var db = _dbFactory.CreateDbContext();
var peer = db.Peers.FirstOrDefault(p => p.Ip == ip);
if (peer is null)
{
_db.Peers.Add(new Peer { Ip = ip, Asn = asn, Status = "active" });
db.Peers.Add(new Peer { Ip = ip, Asn = asn, Status = "active" });
}
else
{
@ -40,102 +42,128 @@ public sealed class PeerStore : IPeerStore
peer.Status = "active";
peer.LastSessionAt = DateTime.UtcNow;
}
_db.SaveChanges();
db.SaveChanges();
}
public void UpdateSessionStatus(string ip, bool active)
{
var peer = _db.Peers.FirstOrDefault(p => p.Ip == ip);
using var db = _dbFactory.CreateDbContext();
var peer = db.Peers.FirstOrDefault(p => p.Ip == ip);
if (peer is null) return;
peer.Status = active ? "active" : "inactive";
if (active) peer.LastSessionAt = DateTime.UtcNow;
_db.SaveChanges();
db.SaveChanges();
}
public void DeletePeer(string id)
{
_db.Peers.Where(p => p.Id == id).ExecuteDelete();
using var db = _dbFactory.CreateDbContext();
db.Peers.Where(p => p.Id == id).ExecuteDelete();
}
public List<Peer> GetAllPeers() =>
_db.Peers.Include(p => p.Communities).ToList();
public List<Peer> GetAllPeers()
{
using var db = _dbFactory.CreateDbContext();
return db.Peers.Include(p => p.Communities).ToList();
}
public Peer? GetDbPeerById(string id) =>
_db.Peers.Include(p => p.Communities).FirstOrDefault(p => p.Id == id);
public Peer? GetDbPeerById(string id)
{
using var db = _dbFactory.CreateDbContext();
return db.Peers.Include(p => p.Communities).FirstOrDefault(p => p.Id == id);
}
PeerInfo? IPeerStore.GetPeerById(string id)
{
var peer = GetDbPeerById(id);
using var db = _dbFactory.CreateDbContext();
var peer = db.Peers.Include(p => p.Communities).FirstOrDefault(p => p.Id == id);
return peer is null ? null : MapToInfo(peer);
}
public PeerInfo? GetPeerByIp(string ip)
{
var peer = _db.Peers.Include(p => p.Communities).FirstOrDefault(p => p.Ip == ip);
using var db = _dbFactory.CreateDbContext();
var peer = db.Peers.Include(p => p.Communities).FirstOrDefault(p => p.Ip == ip);
return peer is null ? null : MapToInfo(peer);
}
public void SetDescription(string id, string description)
{
_db.Peers.Where(p => p.Id == id).ExecuteUpdate(
using var db = _dbFactory.CreateDbContext();
db.Peers.Where(p => p.Id == id).ExecuteUpdate(
s => s.SetProperty(p => p.Description, description));
}
public HashSet<uint> GetCommunities(string peerId) =>
_db.Peers.Include(p => p.Communities)
public HashSet<uint> GetCommunities(string peerId)
{
using var db = _dbFactory.CreateDbContext();
return db.Peers.Include(p => p.Communities)
.Where(p => p.Id == peerId)
.SelectMany(p => p.Communities)
.Select(c => (uint)c.Community)
.ToHashSet();
}
public HashSet<uint> GetCommunitiesByIp(string ip) =>
_db.Peers.Include(p => p.Communities)
public HashSet<uint> GetCommunitiesByIp(string ip)
{
using var db = _dbFactory.CreateDbContext();
return db.Peers.Include(p => p.Communities)
.Where(p => p.Ip == ip)
.SelectMany(p => p.Communities)
.Select(c => (uint)c.Community)
.ToHashSet();
}
public void SetCommunities(string peerId, HashSet<uint> communities)
{
_db.Set<PeerCommunity>().Where(c => c.PeerId == peerId).ExecuteDelete();
_db.Set<PeerCommunity>().AddRange(
using var db = _dbFactory.CreateDbContext();
db.Set<PeerCommunity>().Where(c => c.PeerId == peerId).ExecuteDelete();
db.Set<PeerCommunity>().AddRange(
communities.Select(c => new PeerCommunity { PeerId = peerId, Community = c }));
_db.SaveChanges();
db.SaveChanges();
}
public void ClearCommunities(string peerId)
{
_db.Set<PeerCommunity>().Where(c => c.PeerId == peerId).ExecuteDelete();
using var db = _dbFactory.CreateDbContext();
db.Set<PeerCommunity>().Where(c => c.PeerId == peerId).ExecuteDelete();
}
public List<string> GetSubscriptions(string peerId) =>
_db.Set<PeerSubscription>()
public List<string> GetSubscriptions(string peerId)
{
using var db = _dbFactory.CreateDbContext();
return db.Set<PeerSubscription>()
.Where(s => s.PeerId == peerId)
.Select(s => s.AsnListName)
.ToList();
}
public void SetSubscriptions(string peerId, List<string> asnListNames)
{
_db.Set<PeerSubscription>().Where(s => s.PeerId == peerId).ExecuteDelete();
_db.ChangeTracker.Clear();
_db.Set<PeerSubscription>().AddRange(
using var db = _dbFactory.CreateDbContext();
db.Set<PeerSubscription>().Where(s => s.PeerId == peerId).ExecuteDelete();
db.Set<PeerSubscription>().AddRange(
asnListNames.Select(n => new PeerSubscription { PeerId = peerId, AsnListName = n }));
_db.SaveChanges();
db.SaveChanges();
}
public List<string> GetCustomPrefixes(string peerId) =>
_db.Set<PeerCustomPrefix>()
public List<string> GetCustomPrefixes(string peerId)
{
using var db = _dbFactory.CreateDbContext();
return db.Set<PeerCustomPrefix>()
.Where(c => c.PeerId == peerId)
.Select(c => c.Prefix + "/" + c.PrefixLength)
.ToList();
}
public void SetCustomPrefixes(string peerId, List<(string Prefix, byte Length)> prefixes)
{
_db.Set<PeerCustomPrefix>().Where(c => c.PeerId == peerId).ExecuteDelete();
_db.Set<PeerCustomPrefix>().AddRange(
using var db = _dbFactory.CreateDbContext();
db.Set<PeerCustomPrefix>().Where(c => c.PeerId == peerId).ExecuteDelete();
db.Set<PeerCustomPrefix>().AddRange(
prefixes.Select(p => new PeerCustomPrefix { PeerId = peerId, Prefix = p.Prefix, PrefixLength = p.Length }));
_db.SaveChanges();
db.SaveChanges();
}
private static PeerInfo MapToInfo(Peer peer) => new()

View file

@ -5,6 +5,7 @@ using BGPLite.Protocol;
using BGPLite.Providers;
using BGPLite.Routing;
using BGPLite.Server;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Hosting;
@ -61,14 +62,12 @@ builder.Services.AddSingleton(config);
builder.Services.AddSingleton(config.Bgp);
builder.Services.AddSingleton(routeTable);
var db = new BgpDbContext(dbPath);
var peerCount = db.Peers.Count();
if (db.IsNewDatabase)
Console.WriteLine($"Created new database ({peerCount} peers)");
else
Console.WriteLine($"Database loaded: {peerCount} peer(s)");
builder.Services.AddDbContextFactory<BgpDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}"));
builder.Services.AddDbContext<BgpDbContext>(options =>
options.UseSqlite($"Data Source={dbPath}"), ServiceLifetime.Scoped);
builder.Services.AddSingleton(db);
builder.Services.AddSingleton<PeerStore>();
builder.Services.AddSingleton<IRouteFilter>(sp =>
{
@ -117,6 +116,21 @@ if (config.RipeStat is { AsnLists.Count: > 0 })
var host = builder.Build();
// Initialize DB
var dir = Path.GetDirectoryName(dbPath);
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
Directory.CreateDirectory(dir);
using (var scope = host.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<BgpDbContext>();
BgpDbContext.Initialize(db);
var peerCount = db.Peers.Count();
Console.WriteLine(peerCount == 0
? "Created new database"
: $"Database loaded: {peerCount} peer(s)");
}
var logger = host.Services.GetRequiredService<ILogger<Program>>();
logger.LogInformation("BGPLite starting — ASN={Asn}, RouterId={RouterId}", config.Bgp.Asn, config.Bgp.RouterId);
logger.LogInformation("Loaded routes: {RouteCount}", routeTable.Count);

View file

@ -2,15 +2,14 @@ FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY BGPLite.sln .
COPY global.json .
COPY BGPLite/BGPLite.csproj BGPLite/
COPY BGPLite.Protocol/BGPLite.Protocol.csproj BGPLite.Protocol/
COPY BGPLite.Server/BGPLite.Server.csproj BGPLite.Server/
COPY BGPLite.Routing/BGPLite.Routing.csproj BGPLite.Routing/
COPY BGPLite.Configuration/BGPLite.Configuration.csproj BGPLite.Configuration/
COPY BGPLite.Api/BGPLite.Api.csproj BGPLite.Api/
COPY BGPLite.Tests/BGPLite.Tests.csproj BGPLite.Tests/
RUN dotnet restore
COPY BGPLite.Providers/BGPLite.Providers.csproj BGPLite.Providers/
RUN dotnet restore BGPLite/BGPLite.csproj
COPY . .
RUN dotnet publish BGPLite/BGPLite.csproj -c Release -o /app/publish
@ -21,6 +20,6 @@ WORKDIR /app
COPY --from=build /app/publish .
RUN mkdir -p /app/data
EXPOSE 179/tcp 5000/tcp
EXPOSE 179/tcp 5001/tcp
ENTRYPOINT ["dotnet", "BGPLite.dll"]