为什么context.SaveChange不会为唯一索引或不存在的外键抛出异常

问题描述

我正在使用EF Core 3.1和内存数据库开发一些API。

以下代码存在一些问题。我不能为重复的Name和同一表中不存在的外键生成异常。

public class Category
{
    public Guid Id { get; set; }

    public DateTime CreationDate { get; set; }
    public DateTime? LastModifiedDate { get; set; }

    public bool IsActive { get; set; }
    public string Name { get; set; }
    public Guid? ParentId { get; set; }

    public virtual Category Parent { get; set; }
    public virtual IList<Category> Children { get; set; }
}

public class ItemDbContext : DbContext
{
    public ItemDbContext(DbContextOptions<ItemDbContext> options)
        : base(options) {}
        
    public DbSet<Category> Categories { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.HasDefaultSchema("s_items");

        GetCategoryBuilder(modelBuilder);

        base.OnModelCreating(modelBuilder);
    }
    
    private void GetCategoryBuilder(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Category>(
            entity =>
            {
                entity.Property(c => c.Id)
                        .Isrequired()
                        .HasColumnName("CATE_ID")
                        .HasMaxLength(40);

                entity.Property(c => c.Name)
                        .Isrequired()
                        .IsFixedLength(false)
                        .IsUnicode()
                        .HasColumnName("CATE_NAME")
                        .HasMaxLength(50);

                entity.Property(c => c.CreationDate)
                        .Isrequired()
                        .HasColumnName("CATE_CREATION_DATE")
                        .HasDefaultValuesql("CURRENT_TIMESTAMP")
                        .ValueGeneratedOnAdd();

                entity.Property(c => c.LastModifiedDate)
                        .Isrequired(false)
                        .HasColumnName("CATE_UPDATE_DATE")
                        .HasDefaultValuesql("CURRENT_TIMESTAMP")
                        .ValueGeneratedOnUpdate();

                entity.Property(c => c.IsActive)
                        .HasColumnName("CATE_ACTIVE")
                        .HasDefaultValue(true)
                        .ValueGeneratedOnAddOrUpdate();

                entity.Property(c => c.ParentId)
                        .IsFixedLength(true)
                        .HasColumnName("CATE_PARENT_ID")
                        .HasMaxLength(40);
            }
        );

        modelBuilder.Entity<Category>()
            .HasOne(c => c.Parent)
            .WithMany(c => c.Children)
            .HasForeignKey(c => c.ParentId)
            .HasConstraintName("FK_CATE_PARENT");

        modelBuilder.Entity<Category>()
            .ToTable("Categories")
            .HasKey(c => c.Id)
            .HasName("PK_CATE");

        modelBuilder.Entity<Category>()
            .HasIndex(u => u.Name)
            .IsUnique(true)
            .HasName("UK_CATE_NAME");
    }
}

public class CategoryRepository : ICategoryRepository
{
    private readonly ItemDbContext _context;
    
    public CreateCategoryRepository(ItemDbContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        _context = context;
    }
    
    public Category Create(Category item)
    {
        if (item == null)
        {
            throw new ArgumentNullException(nameof(item));
        }

        Category result = null;

        try
        {
            _context.Categories.Add(item);
            int nbrowsImpacted = _context.SaveChanges();

            if (nbrowsImpacted == 1)
            {
                result = item;
            }
        }
        catch (InvalidOperationException ex)
        {
            var message = "The instance of entity type 'Category' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities,ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.";

            if (ex.Message == message)
            {
                throw new DBConcurrencyException("There is already exists a similar category");
            }
        }
        catch (ArgumentException ex)
        {
            var message = "An item with the same key has already been added.";

            if (ex.Message.StartsWith(message))
            {
                throw new DBConcurrencyException("There is already exists a similar category");
            }
        }

        return result;
    }
}

[TestClass]
public class CategoryRepository
{
    [TestMethod]
    [TestCategory("Category_Repository")]
    public void Category_Create_NotExistantParentId()
    {
        #region Arrange
        IServiceProvider provider = GetServiceProvider(
            DatabaseType.InMemory,injectCreateCategoryRepository: true);
        ItemDbContext context = provider.GetrequiredService<ItemDbContext>();
        IList<Product> products = SeedInMemory.GetProducts();
        context.Products.AddRange(products);
        context.SaveChanges();

        Category category = new Category()
        {
            Id = CategoryId.New(),Name = "Name",CreationDate = DateTime.UtcNow,IsActive = true,ParentId = CategoryId.New()
        };

        ICategoryRepository repository = provider.GetrequiredService<ICategoryRepository>();
        var message = "There is already exists a similar category";
        #endregion

        #region Assert
        var result = Assert.ThrowsException<DBConcurrencyException>(() => repository.Create(category));
        Assert.AreEqual(message,result.Message);
        #endregion
    }
    
    [TestMethod]
    [TestCategory("CreateCategory_Repository")]
    public void CreateCategory_Create_DuplicateName()
    {
        #region Arrange
        IServiceProvider provider = GetServiceProvider(
            DatabaseType.InMemory,Name = context.Categories.FirstOrDefault().Name,IsActive = true
        };
        ICategoryRepository repository = provider.GetrequiredService<ICategoryRepository>();
        var message = "There is already exists a similar category";
        #endregion

        #region Assert
        var result = Assert.ThrowsException<DBConcurrencyException>(() => repository.Create(category));
        Assert.AreEqual(message,result.Message);
        #endregion
    }
}

// Part for GetServiceProvider
services.AddDbContext<ItemDbContext>(
    options =>
    {
        options.ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.DetachedLazyLoadingWarning));
        options.UseLazyLoadingProxies().UseInMemoryDatabase("Test");
    });

对于重复的主键,我有一个ArgumentException

两个测试用例都不会引发异常,nbrowsImpacted变量等于1。为什么?

此致, @R_502_6460@i680

解决方法

我不知道您为什么认为_context.SaveChanges会引发异常InvalidOperationExceptionArgumentException。我调查了documentation,发现在这种情况下可以期望DbUpdateExceptionDbUpdateConcurrencyException。在测试中,您仅添加一个元素,因此不会出现唯一的键冲突。也许在种子方法中很有趣,但是您没有粘贴它。