432 lines
14 KiB
C#
Executable File
432 lines
14 KiB
C#
Executable File
using ZB.MOM.WW.CBDD.Bson;
|
|
using ZB.MOM.WW.CBDD.Shared;
|
|
using ZB.MOM.WW.CBDD.Tests;
|
|
using Xunit;
|
|
|
|
namespace ZB.MOM.WW.CBDD.Tests;
|
|
|
|
/// <summary>
|
|
/// Tests for circular references and N-N relationships
|
|
/// Validates that the source generator handles:
|
|
/// 1. Self-referencing entities using ObjectId references (Employee → ManagerId, DirectReportIds)
|
|
/// 2. N-N via referencing with ObjectIds (CategoryRef/ProductRef) - BEST PRACTICE
|
|
///
|
|
/// Note: Bidirectional embedding (Category ↔ Product with full objects) is NOT supported
|
|
/// by the source generator and is an anti-pattern for document databases.
|
|
/// Use referencing (ObjectIds) instead for N-N relationships.
|
|
/// </summary>
|
|
public class CircularReferenceTests : IDisposable
|
|
{
|
|
private readonly string _dbPath;
|
|
private readonly Shared.TestDbContext _context;
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="CircularReferenceTests"/> class.
|
|
/// </summary>
|
|
public CircularReferenceTests()
|
|
{
|
|
_dbPath = Path.Combine(Path.GetTempPath(), $"cbdd_circular_test_{Guid.NewGuid()}");
|
|
_context = new Shared.TestDbContext(_dbPath);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes Dispose.
|
|
/// </summary>
|
|
public void Dispose()
|
|
{
|
|
_context?.Dispose();
|
|
if (Directory.Exists(_dbPath))
|
|
{
|
|
Directory.Delete(_dbPath, true);
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// Self-Reference Tests (Employee hierarchy with ObjectId references)
|
|
// ========================================
|
|
|
|
/// <summary>
|
|
/// Executes SelfReference_InsertAndQuery_ShouldWork.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SelfReference_InsertAndQuery_ShouldWork()
|
|
{
|
|
// Arrange: Create organizational hierarchy using ObjectId references
|
|
var ceoId = ObjectId.NewObjectId();
|
|
var manager1Id = ObjectId.NewObjectId();
|
|
var manager2Id = ObjectId.NewObjectId();
|
|
var developerId = ObjectId.NewObjectId();
|
|
|
|
var ceo = new Employee
|
|
{
|
|
Id = ceoId,
|
|
Name = "Alice CEO",
|
|
Department = "Executive",
|
|
ManagerId = null,
|
|
DirectReportIds = new List<ObjectId> { manager1Id, manager2Id }
|
|
};
|
|
|
|
var manager1 = new Employee
|
|
{
|
|
Id = manager1Id,
|
|
Name = "Bob Manager",
|
|
Department = "Engineering",
|
|
ManagerId = ceoId,
|
|
DirectReportIds = new List<ObjectId> { developerId }
|
|
};
|
|
|
|
var manager2 = new Employee
|
|
{
|
|
Id = manager2Id,
|
|
Name = "Carol Manager",
|
|
Department = "Sales",
|
|
ManagerId = ceoId,
|
|
DirectReportIds = new List<ObjectId>() // No direct reports
|
|
};
|
|
|
|
var developer = new Employee
|
|
{
|
|
Id = developerId,
|
|
Name = "Dave Developer",
|
|
Department = "Engineering",
|
|
ManagerId = manager1Id,
|
|
DirectReportIds = null // Leaf node
|
|
};
|
|
|
|
// Act: Insert all employees
|
|
_context.Employees.Insert(ceo);
|
|
_context.Employees.Insert(manager1);
|
|
_context.Employees.Insert(manager2);
|
|
_context.Employees.Insert(developer);
|
|
|
|
// Assert: Query and verify
|
|
var queriedCeo = _context.Employees.FindById(ceoId);
|
|
queriedCeo.ShouldNotBeNull();
|
|
queriedCeo.Name.ShouldBe("Alice CEO");
|
|
queriedCeo.DirectReportIds.ShouldNotBeNull();
|
|
queriedCeo.DirectReportIds.Count.ShouldBe(2);
|
|
queriedCeo.DirectReportIds.ShouldContain(manager1Id);
|
|
queriedCeo.DirectReportIds.ShouldContain(manager2Id);
|
|
|
|
// Query manager and verify direct reports
|
|
var queriedManager1 = _context.Employees.FindById(manager1Id);
|
|
queriedManager1.ShouldNotBeNull();
|
|
queriedManager1.ManagerId.ShouldBe(ceoId);
|
|
queriedManager1.DirectReportIds.ShouldNotBeNull();
|
|
queriedManager1.DirectReportIds.Count().ShouldBe(1);
|
|
queriedManager1.DirectReportIds.ShouldContain(developerId);
|
|
|
|
// Query developer and verify no direct reports
|
|
var queriedDeveloper = _context.Employees.FindById(developerId);
|
|
queriedDeveloper.ShouldNotBeNull();
|
|
queriedDeveloper.ManagerId.ShouldBe(manager1Id);
|
|
// Empty list is acceptable (same as null semantically - no direct reports)
|
|
(queriedDeveloper.DirectReportIds ?? new List<ObjectId>()).ShouldBeEmpty();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes SelfReference_UpdateDirectReports_ShouldPersist.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SelfReference_UpdateDirectReports_ShouldPersist()
|
|
{
|
|
// Arrange: Create manager with one direct report
|
|
var managerId = ObjectId.NewObjectId();
|
|
var employee1Id = ObjectId.NewObjectId();
|
|
var employee2Id = ObjectId.NewObjectId();
|
|
|
|
var manager = new Employee
|
|
{
|
|
Id = managerId,
|
|
Name = "Manager",
|
|
Department = "Engineering",
|
|
DirectReportIds = new List<ObjectId> { employee1Id }
|
|
};
|
|
|
|
var employee1 = new Employee
|
|
{
|
|
Id = employee1Id,
|
|
Name = "Employee 1",
|
|
Department = "Engineering",
|
|
ManagerId = managerId
|
|
};
|
|
|
|
var employee2 = new Employee
|
|
{
|
|
Id = employee2Id,
|
|
Name = "Employee 2",
|
|
Department = "Engineering",
|
|
ManagerId = managerId
|
|
};
|
|
|
|
_context.Employees.Insert(manager);
|
|
_context.Employees.Insert(employee1);
|
|
_context.Employees.Insert(employee2);
|
|
|
|
// Act: Add another direct report
|
|
manager.DirectReportIds?.Add(employee2Id);
|
|
_context.Employees.Update(manager);
|
|
|
|
// Assert: Verify update persisted
|
|
var queried = _context.Employees.FindById(managerId);
|
|
queried.ShouldNotBeNull();
|
|
queried.DirectReportIds.ShouldNotBeNull();
|
|
queried.DirectReportIds.Count.ShouldBe(2);
|
|
queried.DirectReportIds.ShouldContain(employee1Id);
|
|
queried.DirectReportIds.ShouldContain(employee2Id);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes SelfReference_QueryByManagerId_ShouldWork.
|
|
/// </summary>
|
|
[Fact]
|
|
public void SelfReference_QueryByManagerId_ShouldWork()
|
|
{
|
|
// Arrange: Create hierarchy
|
|
var managerId = ObjectId.NewObjectId();
|
|
|
|
var manager = new Employee
|
|
{
|
|
Id = managerId,
|
|
Name = "Manager",
|
|
Department = "Engineering"
|
|
};
|
|
|
|
var employee1 = new Employee
|
|
{
|
|
Id = ObjectId.NewObjectId(),
|
|
Name = "Employee 1",
|
|
Department = "Engineering",
|
|
ManagerId = managerId
|
|
};
|
|
|
|
var employee2 = new Employee
|
|
{
|
|
Id = ObjectId.NewObjectId(),
|
|
Name = "Employee 2",
|
|
Department = "Engineering",
|
|
ManagerId = managerId
|
|
};
|
|
|
|
_context.Employees.Insert(manager);
|
|
_context.Employees.Insert(employee1);
|
|
_context.Employees.Insert(employee2);
|
|
|
|
// Act: Query all employees with specific manager
|
|
var subordinates = _context.Employees
|
|
.AsQueryable()
|
|
.Where(e => e.ManagerId == managerId)
|
|
.ToList();
|
|
|
|
// Assert: Should find both employees
|
|
subordinates.Count.ShouldBe(2);
|
|
subordinates.ShouldContain(e => e.Name == "Employee 1");
|
|
subordinates.ShouldContain(e => e.Name == "Employee 2");
|
|
}
|
|
|
|
// ========================================
|
|
// N-N Referencing Tests (CategoryRef/ProductRef)
|
|
// BEST PRACTICE for document databases
|
|
// ========================================
|
|
|
|
/// <summary>
|
|
/// Executes NtoNReferencing_InsertAndQuery_ShouldWork.
|
|
/// </summary>
|
|
[Fact]
|
|
public void NtoNReferencing_InsertAndQuery_ShouldWork()
|
|
{
|
|
// Arrange: Create categories and products with ObjectId references
|
|
var categoryId1 = ObjectId.NewObjectId();
|
|
var categoryId2 = ObjectId.NewObjectId();
|
|
var productId1 = ObjectId.NewObjectId();
|
|
var productId2 = ObjectId.NewObjectId();
|
|
|
|
var electronics = new CategoryRef
|
|
{
|
|
Id = categoryId1,
|
|
Name = "Electronics",
|
|
Description = "Electronic devices",
|
|
ProductIds = new List<ObjectId> { productId1, productId2 }
|
|
};
|
|
|
|
var computers = new CategoryRef
|
|
{
|
|
Id = categoryId2,
|
|
Name = "Computers",
|
|
Description = "Computing devices",
|
|
ProductIds = new List<ObjectId> { productId1 }
|
|
};
|
|
|
|
var laptop = new ProductRef
|
|
{
|
|
Id = productId1,
|
|
Name = "Laptop",
|
|
Price = 999.99m,
|
|
CategoryIds = new List<ObjectId> { categoryId1, categoryId2 }
|
|
};
|
|
|
|
var phone = new ProductRef
|
|
{
|
|
Id = productId2,
|
|
Name = "Phone",
|
|
Price = 599.99m,
|
|
CategoryIds = new List<ObjectId> { categoryId1 }
|
|
};
|
|
|
|
// Act: Insert all entities
|
|
_context.CategoryRefs.Insert(electronics);
|
|
_context.CategoryRefs.Insert(computers);
|
|
_context.ProductRefs.Insert(laptop);
|
|
_context.ProductRefs.Insert(phone);
|
|
|
|
// Assert: Query and verify references
|
|
var queriedCategory = _context.CategoryRefs.FindById(categoryId1);
|
|
queriedCategory.ShouldNotBeNull();
|
|
queriedCategory.Name.ShouldBe("Electronics");
|
|
queriedCategory.ProductIds.ShouldNotBeNull();
|
|
queriedCategory.ProductIds.Count.ShouldBe(2);
|
|
queriedCategory.ProductIds.ShouldContain(productId1);
|
|
queriedCategory.ProductIds.ShouldContain(productId2);
|
|
|
|
var queriedProduct = _context.ProductRefs.FindById(productId1);
|
|
queriedProduct.ShouldNotBeNull();
|
|
queriedProduct.Name.ShouldBe("Laptop");
|
|
queriedProduct.CategoryIds.ShouldNotBeNull();
|
|
queriedProduct.CategoryIds.Count.ShouldBe(2);
|
|
queriedProduct.CategoryIds.ShouldContain(categoryId1);
|
|
queriedProduct.CategoryIds.ShouldContain(categoryId2);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes NtoNReferencing_UpdateRelationships_ShouldPersist.
|
|
/// </summary>
|
|
[Fact]
|
|
public void NtoNReferencing_UpdateRelationships_ShouldPersist()
|
|
{
|
|
// Arrange: Create category and product
|
|
var categoryId = ObjectId.NewObjectId();
|
|
var productId1 = ObjectId.NewObjectId();
|
|
var productId2 = ObjectId.NewObjectId();
|
|
|
|
var category = new CategoryRef
|
|
{
|
|
Id = categoryId,
|
|
Name = "Books",
|
|
Description = "Book category",
|
|
ProductIds = new List<ObjectId> { productId1 }
|
|
};
|
|
|
|
var product1 = new ProductRef
|
|
{
|
|
Id = productId1,
|
|
Name = "Book 1",
|
|
Price = 19.99m,
|
|
CategoryIds = new List<ObjectId> { categoryId }
|
|
};
|
|
|
|
var product2 = new ProductRef
|
|
{
|
|
Id = productId2,
|
|
Name = "Book 2",
|
|
Price = 29.99m,
|
|
CategoryIds = new List<ObjectId>()
|
|
};
|
|
|
|
_context.CategoryRefs.Insert(category);
|
|
_context.ProductRefs.Insert(product1);
|
|
_context.ProductRefs.Insert(product2);
|
|
|
|
// Act: Add product2 to category
|
|
category.ProductIds?.Add(productId2);
|
|
_context.CategoryRefs.Update(category);
|
|
|
|
product2.CategoryIds?.Add(categoryId);
|
|
_context.ProductRefs.Update(product2);
|
|
|
|
// Assert: Verify relationships updated
|
|
var queriedCategory = _context.CategoryRefs.FindById(categoryId);
|
|
queriedCategory.ShouldNotBeNull();
|
|
queriedCategory.ProductIds.ShouldNotBeNull();
|
|
queriedCategory.ProductIds.Count.ShouldBe(2);
|
|
queriedCategory.ProductIds.ShouldContain(productId2);
|
|
|
|
var queriedProduct2 = _context.ProductRefs.FindById(productId2);
|
|
queriedProduct2.ShouldNotBeNull();
|
|
queriedProduct2.CategoryIds.ShouldNotBeNull();
|
|
queriedProduct2.CategoryIds.Count().ShouldBe(1);
|
|
queriedProduct2.CategoryIds.ShouldContain(categoryId);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes NtoNReferencing_DocumentSize_RemainSmall.
|
|
/// </summary>
|
|
[Fact]
|
|
public void NtoNReferencing_DocumentSize_RemainSmall()
|
|
{
|
|
// Arrange: Create category referencing 100 products (only IDs)
|
|
var categoryId = ObjectId.NewObjectId();
|
|
var productIds = Enumerable.Range(0, 100)
|
|
.Select(_ => ObjectId.NewObjectId())
|
|
.ToList();
|
|
|
|
var category = new CategoryRef
|
|
{
|
|
Id = categoryId,
|
|
Name = "Large Category",
|
|
Description = "Category with 100 products",
|
|
ProductIds = productIds
|
|
};
|
|
|
|
// Act: Insert and query
|
|
_context.CategoryRefs.Insert(category);
|
|
var queried = _context.CategoryRefs.FindById(categoryId);
|
|
|
|
// Assert: Document remains small (only ObjectIds, no embedding)
|
|
queried.ShouldNotBeNull();
|
|
queried.ProductIds?.Count.ShouldBe(100);
|
|
|
|
// Note: 100 ObjectIds = ~1.2KB (vs embedding full products = potentially hundreds of KBs)
|
|
// This demonstrates why referencing is preferred for large N-N relationships
|
|
}
|
|
|
|
/// <summary>
|
|
/// Executes NtoNReferencing_QueryByProductId_ShouldWork.
|
|
/// </summary>
|
|
[Fact]
|
|
public void NtoNReferencing_QueryByProductId_ShouldWork()
|
|
{
|
|
// Arrange: Create multiple categories referencing same product
|
|
var productId = ObjectId.NewObjectId();
|
|
|
|
var category1 = new CategoryRef
|
|
{
|
|
Id = ObjectId.NewObjectId(),
|
|
Name = "Category 1",
|
|
Description = "First category",
|
|
ProductIds = new List<ObjectId> { productId }
|
|
};
|
|
|
|
var category2 = new CategoryRef
|
|
{
|
|
Id = ObjectId.NewObjectId(),
|
|
Name = "Category 2",
|
|
Description = "Second category",
|
|
ProductIds = new List<ObjectId> { productId }
|
|
};
|
|
|
|
_context.CategoryRefs.Insert(category1);
|
|
_context.CategoryRefs.Insert(category2);
|
|
|
|
// Act: Query all categories containing the product
|
|
var categoriesWithProduct = _context.CategoryRefs
|
|
.AsQueryable()
|
|
.Where(c => c.ProductIds != null && c.ProductIds.Contains(productId))
|
|
.ToList();
|
|
|
|
// Assert: Should find both categories
|
|
categoriesWithProduct.Count.ShouldBe(2);
|
|
categoriesWithProduct.ShouldContain(c => c.Name == "Category 1");
|
|
categoriesWithProduct.ShouldContain(c => c.Name == "Category 2");
|
|
}
|
|
}
|