diff --git a/QueryPattern.sln b/QueryPattern.sln
index 4011550..9c05368 100644
--- a/QueryPattern.sln
+++ b/QueryPattern.sln
@@ -15,6 +15,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "QuerySample", "sample\Query
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "myNOC.Tests.EntityFramework.Query", "tests\myNOC.Tests.EntityFramework.Query\myNOC.Tests.EntityFramework.Query.csproj", "{71B8019E-AF04-49FF-965C-DC106A3FC7C9}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{DFFF835B-547A-4D6F-B739-7CBAE2142AD2}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{1ABF61F2-FD86-4BFF-A509-64D673DF9913}"
+ ProjectSection(SolutionItems) = preProject
+ .github\workflows\build-tests.yml = .github\workflows\build-tests.yml
+ .github\workflows\release.yml = .github\workflows\release.yml
+ EndProjectSection
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -41,6 +49,7 @@ Global
{BD5A2D1A-6761-4F89-9573-406079505724} = {A506A935-71CF-4D25-A7C4-FBE112BCFFDD}
{06AA0485-2E8B-4592-970A-7EF12C6149C2} = {2F1180C1-B4C8-4A4F-A348-68D90ABD7787}
{71B8019E-AF04-49FF-965C-DC106A3FC7C9} = {CFA418EB-F1B2-4BAF-A725-31EBAD0DC1D7}
+ {1ABF61F2-FD86-4BFF-A509-64D673DF9913} = {DFFF835B-547A-4D6F-B739-7CBAE2142AD2}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {43AABAA3-16E5-4467-9DAE-6388878E4C1C}
diff --git a/QueryPattern.v3.ncrunchsolution b/QueryPattern.v3.ncrunchsolution
new file mode 100644
index 0000000..10420ac
--- /dev/null
+++ b/QueryPattern.v3.ncrunchsolution
@@ -0,0 +1,6 @@
+
+
+ True
+ True
+
+
\ No newline at end of file
diff --git a/src/myNOC.EntityFramework.Query/Extensions/TypeExtensions.cs b/src/myNOC.EntityFramework.Query/Extensions/TypeExtensions.cs
index 30873db..f40a5ec 100644
--- a/src/myNOC.EntityFramework.Query/Extensions/TypeExtensions.cs
+++ b/src/myNOC.EntityFramework.Query/Extensions/TypeExtensions.cs
@@ -19,7 +19,7 @@ private static void GetServiceInterfaces(TypeInterfaces typeInterfaces, Type int
{
foreach(var it in GetServiceInterfaces(typeInterfaces.Type))
{
- if (it.IsAssignableTo(interfaceType) && it != interfaceType)
+ if (it.IsAssignableTo(interfaceType))
typeInterfaces.Interfaces.Add(it);
}
}
diff --git a/src/myNOC.EntityFramework.Query/README.md b/src/myNOC.EntityFramework.Query/README.md
index 5bf107c..addf911 100644
--- a/src/myNOC.EntityFramework.Query/README.md
+++ b/src/myNOC.EntityFramework.Query/README.md
@@ -174,7 +174,7 @@ Once you have your `queryRepo` you can execute the query.
var result = await queryRepo.Query(new ContactNameContains("a"));
```
-`ContactNameContains` inherits from `IQueryList` so the `Query` method will return `IEnumerable` where the name contains an `a`.
+`ContactNameContains` inherits from `IQueryList` so the `Query` method will return `IEnumerable` where the name contains an `a`.
You run a scalar query the same way.
@@ -218,4 +218,4 @@ static async Task SeedSampleData()
addressBook.Add(new ContactEntity { Id = 4, Name = "David" });
await addressBook.SaveChangesAsync();
}
-```
\ No newline at end of file
+```
diff --git a/tests/myNOC.Tests.EntityFramework.Query/Extensions/IEnumerableExtensionsTests.cs b/tests/myNOC.Tests.EntityFramework.Query/Extensions/IEnumerableExtensionsTests.cs
new file mode 100644
index 0000000..34011c3
--- /dev/null
+++ b/tests/myNOC.Tests.EntityFramework.Query/Extensions/IEnumerableExtensionsTests.cs
@@ -0,0 +1,24 @@
+using myNOC.EntityFramework.Query.Extensions;
+
+namespace myNOC.Tests.EntityFramework.Query.Extensions
+{
+ [TestClass]
+ public class IEnumerableExtensionsTests
+ {
+ [TestMethod]
+ public void Apply_ExecutesAction_ReturnsIEnumerable()
+ {
+ // Assemble
+ var numbers = new List { 1, 2, 3, 4, 5, 6 };
+ var total = numbers.Sum();
+ var calculated = 0;
+
+ // Act
+ var results = numbers.AsEnumerable().Apply(x => calculated += x);
+
+ // Assert
+ Assert.AreEqual(6, results.Count());
+ Assert.AreEqual(total, calculated);
+ }
+ }
+}
diff --git a/tests/myNOC.Tests.EntityFramework.Query/Extensions/IServiceCollectionExtensionsTests.cs b/tests/myNOC.Tests.EntityFramework.Query/Extensions/IServiceCollectionExtensionsTests.cs
new file mode 100644
index 0000000..970a99d
--- /dev/null
+++ b/tests/myNOC.Tests.EntityFramework.Query/Extensions/IServiceCollectionExtensionsTests.cs
@@ -0,0 +1,53 @@
+using Microsoft.Extensions.DependencyInjection;
+using myNOC.EntityFramework.Query;
+using myNOC.EntityFramework.Query.Extensions;
+
+namespace myNOC.Tests.EntityFramework.Query.Extensions
+{
+ [TestClass]
+ public class IServiceCollectionExtensionsTests
+ {
+ [TestMethod]
+ public void AddQueryPattern_ScanAppDomainForClassesThatInheritFromIQueryContextOrIQueryRepository_ScopedAndRegister()
+ {
+ // Assemble
+ var services = new ServiceCollection();
+
+ // Act
+ var results = services.AddQueryPattern();
+
+ // Assert
+ Assert.IsNotNull(results);
+ Assert.IsInstanceOfType(results, typeof(IServiceCollection));
+
+ Assert.IsNotNull(results.FirstOrDefault(x => x.ImplementationType == typeof(TestQueryContext)
+ && x.ServiceType == typeof(IQueryContext)
+ && x.Lifetime == ServiceLifetime.Scoped));
+
+ Assert.IsNotNull(results.FirstOrDefault(x => x.ImplementationType == typeof(TestQueryRepository)
+ && x.ServiceType == typeof(IQueryRepository)
+ && x.Lifetime == ServiceLifetime.Scoped));
+ }
+
+ internal class TestQueryContext : IQueryContext
+ {
+ public IQueryable Set() where TEntity : class
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ internal class TestQueryRepository : IQueryRepository
+ {
+ public Task> Query(IQueryList query) where TModel : class
+ {
+ throw new NotImplementedException();
+ }
+
+ public Task Query(IQueryScalar query)
+ {
+ throw new NotImplementedException();
+ }
+ }
+ }
+}
diff --git a/tests/myNOC.Tests.EntityFramework.Query/Extensions/TypeExtensionsTests.cs b/tests/myNOC.Tests.EntityFramework.Query/Extensions/TypeExtensionsTests.cs
new file mode 100644
index 0000000..6a304de
--- /dev/null
+++ b/tests/myNOC.Tests.EntityFramework.Query/Extensions/TypeExtensionsTests.cs
@@ -0,0 +1,109 @@
+using myNOC.EntityFramework.Query.Extensions;
+using System.Reflection;
+
+namespace myNOC.Tests.EntityFramework.Query.Extensions
+{
+ [TestClass]
+ public class TypeExtensionTests
+ {
+ private IEnumerable _types = default!;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ _types = Assembly.GetExecutingAssembly().GetTypes();
+ }
+
+ [TestMethod]
+ public void CanImplement_Types_ImplementsInterface_DirectInterface_ReturnsTypesThatInheritFromITestInterface2()
+ {
+ // Arrange
+ var implementedInterface = typeof(ITestInterface2);
+
+ // Act
+ var results = _types.CanImplement(implementedInterface);
+
+ // Assert
+ Assert.IsNotNull(results);
+ Assert.AreEqual(1, results.Count());
+ Assert.AreEqual(1, results[0].Interfaces.Count());
+ Assert.AreEqual(implementedInterface, results[0].Interfaces[0]);
+ Assert.AreEqual(typeof(TestClass3), results[0].Type);
+ Assert.IsNull(results.FirstOrDefault(x => x.Type == typeof(TestClass4)));
+ }
+
+ [TestMethod]
+ public void CanImplement_Types_ImplementsInterface_SupportsNestedInterfaces_ReturnsTypesThatInheritFromITestInterface()
+ {
+ // Arrange
+ var implementedInterface = typeof(ITestInterface);
+
+ // Act
+ var results = _types.CanImplement(implementedInterface);
+
+ // Assert
+ Assert.IsNotNull(results);
+ Assert.AreEqual(3, results.Count());
+ Assert.IsNotNull(results.FirstOrDefault(x => x.Type == typeof(TestClass1)));
+ Assert.IsNotNull(results.FirstOrDefault(x => x.Type == typeof(TestClass2)));
+ Assert.IsNotNull(results.FirstOrDefault(x => x.Type == typeof(TestClass5)));
+ Assert.IsNull(results.FirstOrDefault(x => x.Type == typeof(TestClass4)));
+ }
+
+ [TestMethod]
+ public void CanImplement_Types_ImplementsInterface_NestedClassesRecursion()
+ {
+ // Arrange
+ var implementedInterface = typeof(ITestInterface3);
+
+ // Act
+ var results = _types.CanImplement(implementedInterface);
+
+ // Assert
+ Assert.IsNotNull(results);
+ Assert.AreEqual(2, results.Count());
+ Assert.IsNotNull(results.FirstOrDefault(x => x.Type == typeof(TestClass6)));
+ Assert.IsNotNull(results.FirstOrDefault(x => x.Type == typeof(TestClass8)));
+ Assert.IsNull(results.FirstOrDefault(x => x.Type == typeof(TestClass4)));
+ }
+
+ [TestMethod]
+ public void CanImplement_Types_ImplementsInterface_SkipIDisposableInterfaceInGetInterfaces()
+ {
+ // Arrange
+ var implementedInterface = typeof(ITestInterface5);
+
+ // Act
+ var results = _types.CanImplement(implementedInterface);
+
+ // Assert
+ Assert.IsNotNull(results);
+ Assert.AreEqual(1, results.Count());
+ Assert.IsNotNull(results.FirstOrDefault(x => x.Type.Equals(typeof(TestClass7))));
+ Assert.IsNull(results.FirstOrDefault(x => x.Type == typeof(TestClass4)));
+ }
+
+ internal interface ITestInterface { }
+ internal interface ITestInterface2 { }
+ internal interface ITestInterface3 { }
+ internal interface ITestInterface4 : ITestInterface { }
+ internal interface ITestInterface5 : IDisposable { }
+ internal interface ITestInterface7 : ITestInterface3 { }
+
+ internal class TestClass1 : ITestInterface { }
+ internal class TestClass2 : ITestInterface { }
+ internal class TestClass3 : ITestInterface2 { }
+ internal class TestClass4 { }
+ internal class TestClass5 : ITestInterface4 { }
+ internal class TestClass6 : TestClass4, ITestInterface7 { }
+ internal class TestClass7 : ITestInterface5
+ {
+ public void Dispose()
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ internal class TestClass8 : TestClass6 { }
+ }
+}
diff --git a/tests/myNOC.Tests.EntityFramework.Query/QueryContextTests.cs b/tests/myNOC.Tests.EntityFramework.Query/QueryContextTests.cs
new file mode 100644
index 0000000..38b9555
--- /dev/null
+++ b/tests/myNOC.Tests.EntityFramework.Query/QueryContextTests.cs
@@ -0,0 +1,56 @@
+using Microsoft.EntityFrameworkCore;
+using myNOC.EntityFramework.Query;
+using NSubstitute;
+using System.ComponentModel.DataAnnotations;
+
+namespace myNOC.Tests.EntityFramework.Query
+{
+ [TestClass]
+ public class QueryContextTests
+ {
+ private TestContext _testDbContext = default!;
+ private QueryContext _queryContext = Substitute.ForPartsOf();
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ var options = new DbContextOptionsBuilder().UseInMemoryDatabase("testContext").Options;
+ _testDbContext = new TestContext(options);
+ }
+
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ _testDbContext?.Database?.EnsureDeleted();
+ _testDbContext?.Dispose();
+ }
+
+ [TestMethod]
+ public void Set_Returns_Entity()
+ {
+ // Assemble
+ _queryContext.GetContext().Returns(_testDbContext);
+
+ // Act
+ var result = _queryContext.Set();
+
+ // Assert
+ Assert.IsNotNull(result);
+ Assert.IsInstanceOfType>(result);
+ }
+
+ public class TestEntity
+ {
+ [Key]
+ public int Id { get; set; }
+ }
+
+ public class TestContext : DbContext
+ {
+ public DbSet TestEntities { get; set; } = default!;
+
+ public TestContext(DbContextOptions options) : base(options) { }
+ }
+ }
+}
diff --git a/tests/myNOC.Tests.EntityFramework.Query/QueryRepositoryTests.cs b/tests/myNOC.Tests.EntityFramework.Query/QueryRepositoryTests.cs
new file mode 100644
index 0000000..15979ad
--- /dev/null
+++ b/tests/myNOC.Tests.EntityFramework.Query/QueryRepositoryTests.cs
@@ -0,0 +1,96 @@
+using Microsoft.EntityFrameworkCore;
+using myNOC.EntityFramework.Query;
+using NSubstitute;
+using System.ComponentModel.DataAnnotations;
+
+namespace myNOC.Tests.EntityFramework.Query
+{
+ [TestClass]
+ public class QueryRepositoryTests
+ {
+ private QueryContext _queryContext = default!;
+ private TestContext _testDbContext = default!;
+ private QueryRepository _queryRepository = default!;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ var options = new DbContextOptionsBuilder().UseInMemoryDatabase("testRepository").Options;
+ _testDbContext = new TestContext(options);
+ _testDbContext.TestEntities.Add(new TestEntity());
+ _testDbContext.TestEntities.Add(new TestEntity());
+ _testDbContext.TestEntities.Add(new TestEntity());
+ _testDbContext.SaveChanges();
+
+ _queryContext = Substitute.ForPartsOf();
+ _queryContext.GetContext().Returns(_testDbContext);
+
+ _queryRepository = Substitute.ForPartsOf(_queryContext);
+ }
+
+ [TestCleanup]
+ public void Cleanup()
+ {
+ _testDbContext?.Database?.EnsureDeleted();
+ _testDbContext?.Dispose();
+ }
+
+ [TestMethod]
+ public async Task Query_RunAIQueryList_ReturnsIEnumerable()
+ {
+ // Assemble
+ var query = new TestEntitiesGetAll();
+
+ // Act
+ var result = await _queryRepository.Query(query);
+
+ // Assert
+ Assert.AreEqual(3, result.Count());
+ }
+
+ [TestMethod]
+ public async Task Query_RunAIQueryScalar_ReturnsInt()
+ {
+ // Assemble
+ var query = new TestEntitiesCount();
+
+ // Act
+ var result = await _queryRepository.Query(query);
+
+ // Assert
+ Assert.AreEqual(3, result);
+ }
+
+ public class TestModel { }
+
+ public class TestEntity
+ {
+ [Key]
+ public int Id { get; set; }
+ }
+
+ public class TestContext : DbContext
+ {
+ public DbSet TestEntities { get; set; } = default!;
+
+ public TestContext(DbContextOptions options) : base(options) { }
+ }
+
+ internal class TestEntitiesGetAll : IQueryList
+ {
+ public IQueryable Query(IQueryContext context)
+ {
+ return from te in context.Set()
+ select new TestModel();
+ }
+ }
+
+ internal class TestEntitiesCount : IQueryScalar
+ {
+ public async Task GetScalar(IQueryContext context)
+ {
+ return await context.Set().CountAsync();
+ }
+ }
+ }
+}
diff --git a/tests/myNOC.Tests.EntityFramework.Query/UnitTest1.cs b/tests/myNOC.Tests.EntityFramework.Query/UnitTest1.cs
deleted file mode 100644
index e4b984a..0000000
--- a/tests/myNOC.Tests.EntityFramework.Query/UnitTest1.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-namespace myNOC.Tests.EntityFramework.Query
-{
- [TestClass]
- public class UnitTest1
- {
- [TestMethod]
- public void TestMethod1()
- {
- }
- }
-}
diff --git a/tests/myNOC.Tests.EntityFramework.Query/myNOC.Tests.EntityFramework.Query.csproj b/tests/myNOC.Tests.EntityFramework.Query/myNOC.Tests.EntityFramework.Query.csproj
index 8608412..b7ab278 100644
--- a/tests/myNOC.Tests.EntityFramework.Query/myNOC.Tests.EntityFramework.Query.csproj
+++ b/tests/myNOC.Tests.EntityFramework.Query/myNOC.Tests.EntityFramework.Query.csproj
@@ -10,10 +10,16 @@
+
+
+
+
+
+