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 @@ + + + + + +