【问题标题】:Why context.SaveChange doesn't throw Exception for a unique index or an non existant foreign key为什么 context.SaveChange 不会为唯一索引或不存在的外键抛出异常
【发布时间】:2020-09-04 19:12:47
【问题描述】:

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

我对以下代码有一些问题。我无法为同一个表中的重复名称和不存在的外键生成异常。

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,
            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 = context.Categories.FirstOrDefault().Name,
            CreationDate = DateTime.UtcNow,
            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。为什么?

真诚地, 新井680

【问题讨论】:

    标签: c# unit-testing .net-core code-first ef-core-3.1


    【解决方案1】:

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

    【讨论】:

    • 我有一个带有附加实体的 InvalidOperationException 和一个带有调试的 ArgumentException(对于重复的主键)。是的,我预计会有一个例外。在网络上的某些来源中,我在 innerException 或您的异常中看到了 UpdateException。所以我不确定到底要抓住什么。我现在没有捕捉它们,因为我没有它们。正如我所说,我有一个“好的”插入(nbRowsImpacté = 1)
    猜你喜欢
    • 2017-08-21
    • 1970-01-01
    • 2014-06-22
    • 1970-01-01
    • 2015-01-12
    • 2010-12-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多