diff --git a/src/DataLinq.Tests.Compliance/Relations/EmployeesRelationAndThreadingTests.cs b/src/DataLinq.Tests.Compliance/Relations/EmployeesRelationAndThreadingTests.cs index 7ee0e214..3a4d0318 100644 --- a/src/DataLinq.Tests.Compliance/Relations/EmployeesRelationAndThreadingTests.cs +++ b/src/DataLinq.Tests.Compliance/Relations/EmployeesRelationAndThreadingTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using DataLinq.Instances; using DataLinq.Tests.Models.Employees; using DataLinq.Testing; @@ -53,6 +54,22 @@ public async Task Relations_DepartmentManagersLazyLoad_ResolvesCollection(TestPr await Assert.That(department.Managers).IsNotEmpty(); await Assert.That(department.Managers.All(x => x.Department.DeptNo == department.Department.DeptNo)).IsTrue(); + + var relation = department.Department.Managers; + IEnumerable rows = relation.AsEnumerable(); + var expectedByKey = rows.ToDictionary(manager => manager.PrimaryKeys()); + + // Manager keys are composite; keyed enumeration must not use the parent department key. + relation.Clear(); + IEnumerable> keyedRows = relation.AsKeyValuePairs(); + var pairs = keyedRows.ToArray(); + await Assert.That(pairs.Length).IsEqualTo(expectedByKey.Count); + foreach (var pair in pairs) + { + await Assert.That(pair.Key).IsEqualTo(DataLinqKey.FromValues([pair.Value.dept_fk, pair.Value.emp_no])); + await Assert.That(ReferenceEquals(pair.Value, expectedByKey[pair.Key])).IsTrue(); + await Assert.That(ReferenceEquals(relation.Get(pair.Key), pair.Value)).IsTrue(); + } } [Test] diff --git a/src/DataLinq.Tests.Compliance/Relations/MultipleForeignKeyRelationTests.cs b/src/DataLinq.Tests.Compliance/Relations/MultipleForeignKeyRelationTests.cs index 171ba3ed..5b872f28 100644 --- a/src/DataLinq.Tests.Compliance/Relations/MultipleForeignKeyRelationTests.cs +++ b/src/DataLinq.Tests.Compliance/Relations/MultipleForeignKeyRelationTests.cs @@ -1,6 +1,8 @@ +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using DataLinq.Attributes; +using DataLinq.Diagnostics; using DataLinq.Instances; using DataLinq.Interfaces; using DataLinq.Metadata; @@ -12,6 +14,7 @@ namespace DataLinq.Tests.Compliance; public class MultipleForeignKeyRelationTests { [Test] + [NotInParallel] [Property(TestProviderAffinity.PropertyName, TestProviderAffinity.EveryProvider)] [MethodDataSource(typeof(TestProviderDataSources), nameof(TestProviderDataSources.ActiveProviders))] public async Task Transaction_MultipleForeignKeysToSameTable_LazyLoadsDistinctRelations(TestProviderDescriptor provider) @@ -40,18 +43,46 @@ public async Task Transaction_MultipleForeignKeysToSameTable_LazyLoadsDistinctRe await Assert.That(ReferenceEquals(createdBy, creator)).IsTrue(); await Assert.That(ReferenceEquals(approvedBy, approver)).IsTrue(); - var createdInvoices = creator.CreatedInvoices.ToArray(); + var relation = creator.CreatedInvoices; + var concreteRelation = (ImmutableRelation)relation; + var loadsBeforeRowViews = GetCollectionLoads(databaseScope.Database); + + // Both receiver types must bind to the standard row extension without loading. + IEnumerable rows = relation.AsEnumerable(); + IEnumerable concreteRows = concreteRelation.AsEnumerable(); + + await Assert.That(ReferenceEquals(rows, relation)).IsTrue(); + await Assert.That(ReferenceEquals(concreteRows, relation)).IsTrue(); + await Assert.That(GetCollectionLoads(databaseScope.Database)).IsEqualTo(loadsBeforeRowViews); + + var createdInvoices = rows.ToArray(); + await Assert.That(GetCollectionLoads(databaseScope.Database)).IsEqualTo(loadsBeforeRowViews + 1); var approvedInvoices = approver.ApprovedInvoices.ToArray(); await Assert.That(createdInvoices.Length).IsEqualTo(1); await Assert.That(approvedInvoices.Length).IsEqualTo(1); await Assert.That(ReferenceEquals(invoice, createdInvoices.Single())).IsTrue(); await Assert.That(ReferenceEquals(invoice, approvedInvoices.Single())).IsTrue(); - await Assert.That(creator.ApprovedInvoices).IsEmpty(); - await Assert.That(approver.CreatedInvoices).IsEmpty(); + + IEnumerable> keyedRows = relation.AsKeyValuePairs(); + var pair = keyedRows.Single(); + await Assert.That(pair.Key).IsEqualTo(DataLinqKey.FromValue(100)); + await Assert.That(ReferenceEquals(pair.Value, createdInvoices.Single())).IsTrue(); + await Assert.That(ReferenceEquals(relation.Get(pair.Key), pair.Value)).IsTrue(); + await Assert.That(ReferenceEquals(concreteRelation.AsKeyValuePairs().Single().Value, pair.Value)).IsTrue(); + + await Assert.That(creator.ApprovedInvoices.AsEnumerable()).IsEmpty(); + await Assert.That(creator.ApprovedInvoices.AsKeyValuePairs()).IsEmpty(); + await Assert.That(approver.CreatedInvoices.AsEnumerable()).IsEmpty(); + await Assert.That(approver.CreatedInvoices.AsKeyValuePairs()).IsEmpty(); transaction.Commit(); } + + private static long GetCollectionLoads(Database database) + => DataLinqMetrics.Snapshot().Providers + .Single(provider => provider.ProviderInstanceId == database.Provider.TelemetryInstanceId) + .Tables.Sum(table => table.Relations.CollectionLoads); } [Database("multiple_fk_relation")] diff --git a/src/DataLinq/Instances/ImmutableRelation.cs b/src/DataLinq/Instances/ImmutableRelation.cs index 7004ae04..a23a6cd7 100644 --- a/src/DataLinq/Instances/ImmutableRelation.cs +++ b/src/DataLinq/Instances/ImmutableRelation.cs @@ -21,7 +21,11 @@ public interface IImmutableRelation : IEnumerable where T : IModelInstance ImmutableArray Keys { get; } ImmutableArray Values { get; } - IEnumerable> AsEnumerable(); + /// + /// Returns relation rows paired with their primary keys. + /// Resolving the keyed collection may synchronously load the relation. + /// + IEnumerable> AsKeyValuePairs(); void Clear(); bool Any() => Count != 0; @@ -100,7 +104,7 @@ public ImmutableRelationMock(IEnumerable list) public ImmutableArray Values => throw new System.NotImplementedException(); - public IEnumerable> AsEnumerable() + public IEnumerable> AsKeyValuePairs() { throw new System.NotImplementedException(); } @@ -172,7 +176,8 @@ public class ImmutableRelation(TKey foreignKey, IDataSourceAccess dataS public ImmutableArray Keys => GetInstances().Keys; public int Count => GetValues().Length; public bool ContainsKey(DataLinqKey key) => GetInstances().ContainsKey(key); - public IEnumerable> AsEnumerable() => GetInstances().AsEnumerable(); + /// + public IEnumerable> AsKeyValuePairs() => GetInstances().AsEnumerable(); public FrozenDictionary ToFrozenDictionary() => GetInstances(); protected TableCache GetTableCache() => GetTableCache(GetDataSource());