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:
parent
a774418abd
commit
d0e1cb88c7
5 changed files with 111 additions and 68 deletions
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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(':');
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue