Laurent Meyer (из репозитория Pomelo GitHub), укажите мне, в чем была моя проблема.
Явно использует мой собственный экземпляр MySqlDbContext. Этот экземпляр НЕ является экземпляром, который будут использовать репозитории.
Я представляю здесь пример программы Simple-proof-of-concept, которая работает, как ожидалось, которую сделал Лоран Мейер, чтобы доказать правильность работы отката.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Storage;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace IssueConsoleTemplate
{
public class Pbd
{
public int PbdId { get; set; }
public string Name { get; set; }
}
public class PbdDetail
{
public int PbdId { get; set; }
public string Status { get; set; }
}
public class MySqlDbContext : DbContext
{
private static int _contextCount = 0;
public static int ContextCount => _contextCount;
public virtual DbSet<Pbd> Pbd { get; set; }
public virtual DbSet<PbdDetail> PbdDetail { get; set; }
public MySqlDbContext()
{
Interlocked.Increment(ref _contextCount);
}
public IDbContextTransaction BeginTransaction() => Database.BeginTransaction();
public async Task Commit(IDbContextTransaction _transaction)
{
try
{
await SaveChangesAsync();
await _transaction.CommitAsync();
}
finally
{
_transaction.Dispose();
}
}
public void Rollback(IDbContextTransaction transaction)
{
transaction.Rollback();
transaction.Dispose();
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder
.UseMySql(
"server=127.0.0.1;port=3306;user=root;password=;database=Issue1134_01",
b => b.ServerVersion("8.0.20-mysql"))
.UseLoggerFactory(
LoggerFactory.Create(
b => b
.AddConsole()
.AddFilter(level => level >= LogLevel.Information)))
.EnableSensitiveDataLogging()
.EnableDetailedErrors();
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<PbdDetail>()
.HasKey(e => e.PbdId);
}
}
public interface IRepository<T>
{
Task<T> AddAsync(T entity);
Task<List<T>> AddRangeAsync(List<T> entities);
}
public class Repository<TEntity> : IRepository<TEntity>
where TEntity : class, new()
{
private readonly MySqlDbContext _context;
protected readonly DbSet<TEntity> Entity;
public Repository(MySqlDbContext context)
{
_context = context;
Entity = context.Set<TEntity>();
}
public async Task<TEntity> AddAsync(TEntity entity)
{
if (entity == null)
{
throw new ArgumentNullException($"{nameof(AddAsync)} entity must not be null");
}
try
{
await _context.AddAsync(entity);
await _context.SaveChangesAsync();
return entity;
}
catch (Exception)
{
throw new Exception($"{nameof(entity)} could not be saved");
}
}
public async Task<List<TEntity>> AddRangeAsync(List<TEntity> entities)
{
if (entities == null)
{
throw new ArgumentNullException($"{nameof(AddRangeAsync)} entities must not be null");
}
try
{
await _context.AddRangeAsync(entities);
await _context.SaveChangesAsync();
return entities;
}
catch (Exception)
{
throw new Exception($"{nameof(entities)} could not be updated");
}
}
}
public class PbdRepository : Repository<Pbd>
{
public PbdRepository(MySqlDbContext context)
: base(context)
{
}
}
public class PbdDetailRepository : Repository<PbdDetail>
{
public PbdDetailRepository(MySqlDbContext context)
: base(context)
{
}
}
public class CreatePbdCommand : IRequest<Pbd>
{
public Pbd Pbd { get; set; }
}
public class CreatePbdDetailCommand : IRequest<List<PbdDetail>>
{
public List<PbdDetail> PbdDetails { get; set; }
}
public class CreatePbdCommandHandler : IRequestHandler<CreatePbdCommand, Pbd>
{
private readonly IRepository<Pbd> _repository;
public CreatePbdCommandHandler(IRepository<Pbd> repository) => _repository = repository;
public async Task<Pbd> Handle(CreatePbdCommand request, CancellationToken cancellationToken)
{
return await _repository.AddAsync(request.Pbd);
}
}
public class CreatePbdDetailCommandHandler : IRequestHandler<CreatePbdDetailCommand, List<PbdDetail>>
{
private readonly IRepository<PbdDetail> _repository;
public CreatePbdDetailCommandHandler(IRepository<PbdDetail> repository) => _repository = repository;
public async Task<List<PbdDetail>> Handle(CreatePbdDetailCommand request, CancellationToken cancellationToken)
{
return await _repository.AddRangeAsync(request.PbdDetails);
}
}
internal static class Program
{
private static async Task Main()
{
var serviceProvider = new ServiceCollection()
.AddScoped<MySqlDbContext>()
.AddScoped<IRepository<Pbd>, PbdRepository>()
.AddScoped<IRepository<PbdDetail>, PbdDetailRepository>()
.AddScoped<IRequestHandler<CreatePbdCommand, Pbd>, CreatePbdCommandHandler>()
.AddScoped<IRequestHandler<CreatePbdDetailCommand, List<PbdDetail>>, CreatePbdDetailCommandHandler>()
.BuildServiceProvider();
using (var serviceScope = serviceProvider.CreateScope())
{
var mediator = new Mediator(t => serviceScope.ServiceProvider.GetService(t));
var pbd = new Pbd {Name = "First Pdb"};
var pbdDetails = new List<PbdDetail>()
{
new PbdDetail {Status = "New"},
new PbdDetail {Status = "Old"}
};
// Uses the service provider in scope to get the same MySqlDbContext instance as the repositories.
using (var context = serviceScope.ServiceProvider.GetService<MySqlDbContext>())
{
await context.Database.EnsureDeletedAsync();
await context.Database.EnsureCreatedAsync();
using (var transaction = context.BeginTransaction())
{
try
{
//Add the Pbd to database
await mediator.Send(
new CreatePbdCommand
{
Pbd = pbd
});
//Add the Pbdetails to database
await mediator.Send(
new CreatePbdDetailCommand
{
PbdDetails = pbdDetails
});
throw new InvalidOperationException("Let's trigger the transactions rollback.");
//await context.Commit(transaction);
}
catch (Exception)
{
context.Rollback(transaction);
}
// A total of only two contexts should have been created up until now.
Debug.Assert(MySqlDbContext.ContextCount == 1);
}
}
}
// New scope to create new context.
using (var serviceScope = serviceProvider.CreateScope())
{
// Create a new context to ensure, that EF Core does not return not yet committed entities to us.
using (var context = serviceScope.ServiceProvider.GetService<MySqlDbContext>())
{
var pdbs = context.Pbd.ToList();
var pdbDetails = context.PbdDetail.ToList();
// A total of only two contexts should have been created up until now.
Debug.Assert(MySqlDbContext.ContextCount == 2);
// No entities should have been committed.
Debug.Assert(pdbs.Count == 0);
Debug.Assert(pdbDetails.Count == 0);
}
}
}
}
}