using System; using System.Linq; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.EntityFrameworkCore.Infrastructure; namespace BTCPayServer.Data { public class DesignTimeDbContextFactory : IDesignTimeDbContextFactory { public ApplicationDbContext CreateDbContext(string[] args) { var builder = new DbContextOptionsBuilder(); builder.UseSqlite("Data Source=temp.db"); return new ApplicationDbContext(builder.Options, true); } } public class ApplicationDbContext : IdentityDbContext { private readonly bool _designTime; public ApplicationDbContext(DbContextOptions options, bool designTime = false) : base(options) { _designTime = designTime; } public DbSet Invoices { get; set; } public DbSet PlannedTransactions { get; set; } public DbSet PayjoinLocks { get; set; } public DbSet Apps { get; set; } public DbSet InvoiceEvents { get; set; } public DbSet OffchainTransactions { get; set; } public DbSet HistoricalAddressInvoices { get; set; } public DbSet PendingInvoices { get; set; } public DbSet RefundAddresses { get; set; } public DbSet Payments { get; set; } public DbSet PaymentRequests { get; set; } public DbSet Wallets { get; set; } public DbSet WalletTransactions { get; set; } public DbSet Stores { get; set; } public DbSet UserStore { get; set; } public DbSet AddressInvoices { get; set; } public DbSet Settings { get; set; } public DbSet PairingCodes { get; set; } public DbSet PairedSINData { get; set; } public DbSet ApiKeys { get; set; } public DbSet Files { get; set; } public DbSet U2FDevices { get; set; } public DbSet Notifications { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { var isConfigured = optionsBuilder.Options.Extensions.OfType().Any(); if (!isConfigured) optionsBuilder.UseSqlite("Data Source=temp.db"); } protected override void OnModelCreating(ModelBuilder builder) { base.OnModelCreating(builder); NotificationData.OnModelCreating(builder); builder.Entity() .HasOne(o => o.StoreData) .WithMany(a => a.Invoices).OnDelete(DeleteBehavior.Cascade); builder.Entity().HasIndex(o => o.StoreDataId); builder.Entity() .HasOne(o => o.InvoiceData) .WithMany(i => i.Payments).OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasIndex(o => o.InvoiceDataId); builder.Entity() .HasOne(o => o.InvoiceData) .WithMany(i => i.RefundAddresses).OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasIndex(o => o.InvoiceDataId); builder.Entity() .HasOne(o => o.StoreData) .WithMany(i => i.UserStores).OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasKey(t => new { t.ApplicationUserId, t.StoreDataId }); builder.Entity() .HasOne(o => o.StoreData) .WithMany(i => i.APIKeys) .HasForeignKey(i => i.StoreId).OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasOne(o => o.User) .WithMany(i => i.APIKeys) .HasForeignKey(i => i.UserId).OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasIndex(o => o.StoreId); builder.Entity() .HasOne(o => o.StoreData) .WithMany(i => i.Apps).OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasOne(a => a.StoreData); builder.Entity() .HasOne(pt => pt.ApplicationUser) .WithMany(p => p.UserStores) .HasForeignKey(pt => pt.ApplicationUserId); builder.Entity() .HasOne(pt => pt.StoreData) .WithMany(t => t.UserStores) .HasForeignKey(pt => pt.StoreDataId); builder.Entity() .HasOne(o => o.InvoiceData) .WithMany(i => i.AddressInvoices).OnDelete(DeleteBehavior.Cascade); builder.Entity() #pragma warning disable CS0618 .HasKey(o => o.Address); #pragma warning restore CS0618 builder.Entity() .HasKey(o => o.Id); builder.Entity() .HasOne(o => o.InvoiceData) .WithMany(o => o.PendingInvoices) .HasForeignKey(o => o.Id).OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasOne(o => o.StoreData) .WithMany(i => i.PairedSINs).OnDelete(DeleteBehavior.Cascade); builder.Entity(b => { b.HasIndex(o => o.SIN); b.HasIndex(o => o.StoreDataId); }); builder.Entity() .HasOne(o => o.InvoiceData) .WithMany(i => i.HistoricalAddressInvoices).OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasKey(o => new { o.InvoiceDataId, #pragma warning disable CS0618 o.Address #pragma warning restore CS0618 }); builder.Entity() .HasOne(o => o.InvoiceData) .WithMany(i => i.Events).OnDelete(DeleteBehavior.Cascade); builder.Entity() .HasKey(o => new { o.InvoiceDataId, #pragma warning disable CS0618 o.UniqueId #pragma warning restore CS0618 }); builder.Entity() .HasOne(o => o.StoreData) .WithMany(i => i.PaymentRequests) .OnDelete(DeleteBehavior.Cascade); builder.Entity() .Property(e => e.Created) .HasDefaultValue(new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)); builder.Entity() .HasIndex(o => o.Status); builder.Entity() .HasKey(o => new { o.WalletDataId, #pragma warning disable CS0618 o.TransactionId #pragma warning restore CS0618 }); builder.Entity() .HasOne(o => o.WalletData) .WithMany(w => w.WalletTransactions).OnDelete(DeleteBehavior.Cascade); if (Database.IsSqlite() && !_designTime) { // SQLite does not have proper support for DateTimeOffset via Entity Framework Core, see the limitations // here: https://docs.microsoft.com/en-us/ef/core/providers/sqlite/limitations#query-limitations // To work around this, when the Sqlite database provider is used, all model properties of type DateTimeOffset // use the DateTimeOffsetToBinaryConverter // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754 // This only supports millisecond precision, but should be sufficient for most use cases. foreach (var entityType in builder.Model.GetEntityTypes()) { var properties = entityType.ClrType.GetProperties().Where(p => p.PropertyType == typeof(DateTimeOffset)); foreach (var property in properties) { builder .Entity(entityType.Name) .Property(property.Name) .HasConversion(new Microsoft.EntityFrameworkCore.Storage.ValueConversion.DateTimeOffsetToBinaryConverter()); } } } } } }