diff --git a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/MetadataProvider.cs b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/MetadataProvider.cs index 059b944..f8bba2b 100644 --- a/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/MetadataProvider.cs +++ b/src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/MetadataProvider.cs @@ -1,47 +1,50 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; - -namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; - -internal sealed class MetadataProvider -{ - private Dictionary> _tablesPerContext = new(); - - public TableMetadata GetTableInfo(DbContext context) - { - var tables = GetTables(context); - - if (!tables.TryGetValue(typeof(T), out var table)) - { - throw new InvalidOperationException($"Cannot find metadata for type '{typeof(T)}'."); - } - - return table; - } - - private Dictionary GetTables(DbContext context) - { - lock (_tablesPerContext) - { - var type = context.GetType(); - if (_tablesPerContext.TryGetValue(context.GetType(), out var tables)) - { - return tables; - } - - var provider = context.GetService(); - - tables = context.Model.GetEntityTypes() - .GroupBy(x => x.ClrType) - .ToDictionary( - x => x.Key, - x => new TableMetadata(x.First(), provider.SqlDialect)); - - _tablesPerContext[type] = tables; - - return tables; - } - } -} +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +using PhenX.EntityFrameworkCore.BulkInsert.Abstractions; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata; + +internal sealed class MetadataProvider +{ + private Dictionary> _tablesPerContext = new(); + + public TableMetadata GetTableInfo(DbContext context) + { + var tables = GetTables(context); + + if (!tables.TryGetValue(typeof(T), out var table)) + { + throw new InvalidOperationException($"Cannot find metadata for type '{typeof(T)}'."); + } + + return table; + } + + private Dictionary GetTables(DbContext context) + { + lock (_tablesPerContext) + { + var type = context.GetType(); + if (_tablesPerContext.TryGetValue(context.GetType(), out var tables)) + { + return tables; + } + + var provider = context.GetService(); + + tables = context.Model.GetEntityTypes() + // Filter out entities without an associated table + // See also https://learn.microsoft.com/en-us/ef/core/modeling/keyless-entity-types + .Where(x => x.GetTableName() is not null) + .GroupBy(x => x.ClrType) + .ToDictionary( + x => x.Key, + x => new TableMetadata(x.First(), provider.SqlDialect)); + + _tablesPerContext[type] = tables; + + return tables; + } + } +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs index c4835a5..98b4ae0 100644 --- a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs @@ -1,155 +1,164 @@ -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; - -using SmartEnum.EFCore; - -namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; - -public class TestDbContext : TestDbContextBase -{ - public DbSet TestEntities { get; set; } = null!; - public DbSet TestEntitiesWithSimpleTypes { get; set; } = null!; - public DbSet TestEntitiesWithJson { get; set; } = null!; - public DbSet TestEntitiesWithGuidId { get; set; } = null!; - public DbSet TestEntitiesWithConverter { get; set; } = null!; - public DbSet TestEntitiesWithComplexType { get; set; } = null!; - public DbSet TestEntitiesWithSmartEnum { get; set; } = null!; - public DbSet Students { get; set; } = null!; - public DbSet Courses { get; set; } = null!; - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.ConfigureSmartEnum(); - - modelBuilder.Entity(builder => - { - builder.Property(e => e.CreatedAt) - .HasConversion(new DateTimeToBinaryConverter()); - }); - - modelBuilder.Entity(builder => - { - builder.Property(e => e.Id) - .ValueGeneratedNever(); - }); - - modelBuilder.Entity(builder => - { - builder - .ComplexProperty(e => e.OwnedComplexType) - .IsRequired(); - }); - - // Many-to-many with shadow property - modelBuilder.Entity() - .HasMany(s => s.Courses) - .WithMany(c => c.Students) - .UsingEntity>( - "StudentCourse", - j => j.HasOne().WithMany().HasForeignKey("CourseId"), - j => j.HasOne().WithMany().HasForeignKey("StudentId"), - j => - { - j.Property("EnrolledAt"); - j.HasKey("StudentId", "CourseId"); - } - ); - } -} - -public class TestDbContextPostgreSql : TestDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.Property(x => x.JsonArray).AsJsonString("jsonb"); - b.Property(x => x.JsonObject).AsJsonString("jsonb"); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.StringEnumValue).HasColumnType("text"); - }); - } -} - -public class TestDbContextMySql : TestDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.Property(x => x.JsonArray).AsJsonString("json"); - b.Property(x => x.JsonObject).AsJsonString("json"); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.StringEnumValue).HasColumnType("text"); - }); - } -} - -public class TestDbContextSqlServer : TestDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.Property(x => x.JsonArray).AsJsonString(null); - b.Property(x => x.JsonObject).AsJsonString(null); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.StringEnumValue).HasColumnType("text"); - }); - } -} - -public class TestDbContextSqlite : TestDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.Property(x => x.JsonArray).AsJsonString(null); - b.Property(x => x.JsonObject).AsJsonString(null); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.StringEnumValue).HasColumnType("text"); - }); - } -} - -public class TestDbContextOracle : TestDbContext -{ - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(b => - { - b.Property(x => x.JsonArray).AsJsonString(null); - b.Property(x => x.JsonObject).AsJsonString(null); - }); - - modelBuilder.Entity(b => - { - b.Property(x => x.StringEnumValue).HasColumnType("nvarchar2(255)"); - }); - } -} +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +using SmartEnum.EFCore; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +public class TestDbContext : TestDbContextBase +{ + public DbSet TestEntities { get; set; } = null!; + public DbSet TestEntitiesWithSimpleTypes { get; set; } = null!; + public DbSet TestEntitiesWithJson { get; set; } = null!; + public DbSet TestEntitiesWithGuidId { get; set; } = null!; + public DbSet TestEntitiesWithConverter { get; set; } = null!; + public DbSet TestEntitiesWithComplexType { get; set; } = null!; + public DbSet TestEntitiesWithSmartEnum { get; set; } = null!; + public DbSet Students { get; set; } = null!; + public DbSet Courses { get; set; } = null!; + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.ConfigureSmartEnum(); + + modelBuilder.Entity(builder => + { + builder.Property(e => e.CreatedAt) + .HasConversion(new DateTimeToBinaryConverter()); + }); + + modelBuilder.Entity(builder => + { + builder.Property(e => e.Id) + .ValueGeneratedNever(); + }); + + modelBuilder.Entity(builder => + { + builder + .ComplexProperty(e => e.OwnedComplexType) + .IsRequired(); + }); + + // Many-to-many with shadow property + modelBuilder.Entity() + .HasMany(s => s.Courses) + .WithMany(c => c.Students) + .UsingEntity>( + "StudentCourse", + j => j.HasOne().WithMany().HasForeignKey("CourseId"), + j => j.HasOne().WithMany().HasForeignKey("StudentId"), + j => + { + j.Property("EnrolledAt"); + j.HasKey("StudentId", "CourseId"); + } + ); + + // Keyless entity type + modelBuilder.Entity(builder => + { + builder.HasNoKey(); + // ToView will use the given table name read-only, it doesn't have to actually be a database view. + // We just reuse the table for the standard TestEntity. + builder.ToView("test_entity"); + }); + } +} + +public class TestDbContextPostgreSql : TestDbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => + { + b.Property(x => x.JsonArray).AsJsonString("jsonb"); + b.Property(x => x.JsonObject).AsJsonString("jsonb"); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.StringEnumValue).HasColumnType("text"); + }); + } +} + +public class TestDbContextMySql : TestDbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => + { + b.Property(x => x.JsonArray).AsJsonString("json"); + b.Property(x => x.JsonObject).AsJsonString("json"); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.StringEnumValue).HasColumnType("text"); + }); + } +} + +public class TestDbContextSqlServer : TestDbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => + { + b.Property(x => x.JsonArray).AsJsonString(null); + b.Property(x => x.JsonObject).AsJsonString(null); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.StringEnumValue).HasColumnType("text"); + }); + } +} + +public class TestDbContextSqlite : TestDbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => + { + b.Property(x => x.JsonArray).AsJsonString(null); + b.Property(x => x.JsonObject).AsJsonString(null); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.StringEnumValue).HasColumnType("text"); + }); + } +} + +public class TestDbContextOracle : TestDbContext +{ + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(b => + { + b.Property(x => x.JsonArray).AsJsonString(null); + b.Property(x => x.JsonObject).AsJsonString(null); + }); + + modelBuilder.Entity(b => + { + b.Property(x => x.StringEnumValue).HasColumnType("nvarchar2(255)"); + }); + } +} diff --git a/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityKeyless.cs b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityKeyless.cs new file mode 100644 index 0000000..3dabcbe --- /dev/null +++ b/tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestEntityKeyless.cs @@ -0,0 +1,19 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +using Microsoft.EntityFrameworkCore; + +namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext; + +[Keyless] +public class TestEntityKeyless : TestEntityBase +{ + public int Id { get; set; } + + [Column("name")] + [MaxLength(100)] + public string Name { get; set; } = string.Empty; + + [Column("some_price")] + public decimal Price { get; set; } +}