using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using System.Collections.Generic;
using Microsoft.Data.SqlClient;
using System.Threading.Tasks;
private static DbContextOptions<StockContext> _options;
private static Guid _stockId;
private static async Task SetupDb()
_options = new DbContextOptionsBuilder<StockContext>()
.UseSqlServer(new SqlConnection(FiddleHelper.GetConnectionStringSqlServer()))
_stockId = new("1b1f4b9b-1b1b-4b1b-8b1b-1b1b1b1b1b1b");
using (var context = new StockContext(_options))
context.Database.EnsureCreated();
await context.Database.MigrateAsync();
bool exists = await context.Stock.AnyAsync(x => x.Id == _stockId);
var stockItem = new Stock()
Prices = { new StockPrice() { Id = Guid.NewGuid(), CreatedAt = DateTime.Now, CreatedBy = "Test", Price = 100, StockId = _stockId } }
context.Stock.Add(stockItem);
await context.SaveChangesAsync();
public static async Task Main()
Console.WriteLine("Hello World");
public static async Task MergeAndAdd()
using (var context = new StockContext(_options))
var updatedStock = new Stock()
Prices = { new StockPrice() { Id = Guid.NewGuid(), CreatedAt = DateTime.Now, CreatedBy = "Test", Price = 200, StockId = _stockId } }
var items = new List<Stock>() { updatedStock };
await context.BulkMergeAsync(items, options =>
options.IncludeGraph = true;
options.IncludeGraphOperationBuilder = operationBuilder =>
options.ColumnPrimaryKeyExpression = x => new { x.MarketId, x.SectorId };
private static async Task MergeButDontAdd()
using (var context = new StockContext(_options))
var updatedStock = new Stock()
Name = "Stock 2 - NEW NAME",
Prices = { new StockPrice() { Id = Guid.NewGuid(), CreatedAt = DateTime.Now, CreatedBy = "Test", Price = 100 } }
var items = new List<Stock>() { updatedStock };
await context.BulkMergeAsync(items, options =>
options.IncludeGraph = true;
options.IncludeGraphOperationBuilder = operation =>
if (operation is BulkOperation<Stock>)
var bulk = (BulkOperation<Stock>)operation;
bulk.ColumnPrimaryKeyExpression = x => new { x.MarketId, x.SectorId };
if (operation is BulkOperation<StockPrice>)
var bulk = (BulkOperation<StockPrice>)operation;
bulk.MergeMatchedAndConditionExpression = x => new { x.Price, x.StockId };
public class StockContext(DbContextOptions<StockContext> options) : DbContext(options)
public DbSet<Stock> Stock { get; set; }
public DbSet<StockPrice> StockPrice { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
modelBuilder.ApplyConfiguration(new StockConfiguration());
modelBuilder.ApplyConfiguration(new StockPriceConfiguration());
public Guid Id { get; set; }
public int MarketId { get; set; }
public int SectorId { get; set; }
public string Name { get; set; } = string.Empty;
public IList<StockPrice> Prices = [];
public Guid Id { get; set; }
public DateTime CreatedAt { get; set; }
public decimal Price { get; set; }
public Guid StockId { get; set; }
public string CreatedBy { get; set; } = string.Empty;
public class StockConfiguration : IEntityTypeConfiguration<Stock>
public void Configure(EntityTypeBuilder<Stock> builder)
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
builder.Property(x => x.MarketId)
builder.Property(x => x.SectorId)
builder.HasIndex(x => new { x.MarketId, x.SectorId })
builder.Property(x => x.Name)
builder.HasMany(x => x.Prices)
.HasForeignKey(x => x.StockId);
public class StockPriceConfiguration : IEntityTypeConfiguration<StockPrice>
public void Configure(EntityTypeBuilder<StockPrice> builder)
builder.HasKey(x => x.Id);
builder.Property(x => x.Id)
builder.Property(x => x.CreatedBy)
builder.Property(x => x.Price)