From 2774fa73bbe7a25e7df18670734adf481c2137a5 Mon Sep 17 00:00:00 2001 From: Dries Verbeke Date: Mon, 28 Mar 2022 20:36:06 +0200 Subject: [PATCH 1/9] Using a fixture to setup instant api config builder tests Using a central test fixture to setup the tests around the configuraiton builder so we do not duplicate the creation of the setup mock. --- .../InstantAPIsConfigBuilderFixture.cs | 19 ++++++++++++ .../WhenIncludeDoesNotSpecifyBaseUrl.cs | 23 ++------------- .../WhenIncludeSpecifiesBaseUrl.cs | 29 ++----------------- Test/Configuration/WithIncludesAndExcludes.cs | 19 ++---------- Test/Configuration/WithOnlyExcludes.cs | 21 ++------------ Test/Configuration/WithOnlyIncludes.cs | 21 +------------- Test/Configuration/WithoutIncludes.cs | 1 - 7 files changed, 28 insertions(+), 105 deletions(-) create mode 100644 Test/Configuration/InstantAPIsConfigBuilderFixture.cs diff --git a/Test/Configuration/InstantAPIsConfigBuilderFixture.cs b/Test/Configuration/InstantAPIsConfigBuilderFixture.cs new file mode 100644 index 0000000..84b1c62 --- /dev/null +++ b/Test/Configuration/InstantAPIsConfigBuilderFixture.cs @@ -0,0 +1,19 @@ +using InstantAPIs; +using Microsoft.EntityFrameworkCore; + +namespace Test.Configuration; + +public abstract class InstantAPIsConfigBuilderFixture : BaseFixture +{ + internal InstantAPIsConfigBuilder _Builder; + + public InstantAPIsConfigBuilderFixture() + { + + var _ContextOptions = new DbContextOptionsBuilder() + .UseInMemoryDatabase("TestDb") + .Options; + _Builder = new(new(_ContextOptions)); + + } +} diff --git a/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs b/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs index 0acaa0c..a9e21ff 100644 --- a/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs +++ b/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs @@ -1,25 +1,10 @@ -using InstantAPIs; -using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using Xunit; +using Xunit; namespace Test.Configuration; -public class WhenIncludeDoesNotSpecifyBaseUrl : BaseFixture +public class WhenIncludeDoesNotSpecifyBaseUrl : InstantAPIsConfigBuilderFixture { - InstantAPIsConfigBuilder _Builder; - - public WhenIncludeDoesNotSpecifyBaseUrl() - { - - var _ContextOptions = new DbContextOptionsBuilder() - .UseInMemoryDatabase("TestDb") - .Options; - _Builder = new(new(_ContextOptions)); - - } - [Fact] public void ShouldSpecifyDefaultUrl() { @@ -35,8 +20,4 @@ public void ShouldSpecifyDefaultUrl() Assert.Equal(new Uri("/api/Contacts", uriKind: UriKind.Relative), config.Tables.First().BaseUrl); } - - } - - diff --git a/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs b/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs index ef4ed08..c2e113f 100644 --- a/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs +++ b/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs @@ -1,31 +1,10 @@ -using InstantAPIs; -using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Xunit; +using Xunit; namespace Test.Configuration; - -public class WhenIncludeSpecifiesBaseUrl : BaseFixture +public class WhenIncludeSpecifiesBaseUrl : InstantAPIsConfigBuilderFixture { - InstantAPIsConfigBuilder _Builder; - - public WhenIncludeSpecifiesBaseUrl() - { - - var _ContextOptions = new DbContextOptionsBuilder() - .UseInMemoryDatabase("TestDb") - .Options; - _Builder = new(new(_ContextOptions)); - - } - [Fact] public void ShouldSpecifyThatUrl() { @@ -42,8 +21,4 @@ public void ShouldSpecifyThatUrl() Assert.Equal(BaseUrl, config.Tables.First().BaseUrl); } - - } - - diff --git a/Test/Configuration/WithIncludesAndExcludes.cs b/Test/Configuration/WithIncludesAndExcludes.cs index dad6b54..4e28530 100644 --- a/Test/Configuration/WithIncludesAndExcludes.cs +++ b/Test/Configuration/WithIncludesAndExcludes.cs @@ -1,24 +1,10 @@ -using InstantAPIs; -using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using Xunit; +using Xunit; namespace Test.Configuration; -public class WithIncludesAndExcludes : BaseFixture +public class WithIncludesAndExcludes : InstantAPIsConfigBuilderFixture { - InstantAPIsConfigBuilder _Builder; - - public WithIncludesAndExcludes() - { - - var _ContextOptions = new DbContextOptionsBuilder() - .UseInMemoryDatabase("TestDb") - .Options; - _Builder = new(new(_ContextOptions)); - - } [Fact] public void ShouldExcludePreviouslyIncludedTable() @@ -39,4 +25,3 @@ public void ShouldExcludePreviouslyIncludedTable() } } - diff --git a/Test/Configuration/WithOnlyExcludes.cs b/Test/Configuration/WithOnlyExcludes.cs index d40c587..df23e5a 100644 --- a/Test/Configuration/WithOnlyExcludes.cs +++ b/Test/Configuration/WithOnlyExcludes.cs @@ -1,26 +1,10 @@ -using InstantAPIs; -using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using System.Linq; -using Xunit; +using Xunit; namespace Test.Configuration; -public class WithOnlyExcludes : BaseFixture +public class WithOnlyExcludes : InstantAPIsConfigBuilderFixture { - InstantAPIsConfigBuilder _Builder; - - public WithOnlyExcludes() - { - - var _ContextOptions = new DbContextOptionsBuilder() - .UseInMemoryDatabase("TestDb") - .Options; - _Builder = new(new(_ContextOptions)); - - } - [Fact] public void ShouldExcludeSpecifiedTable() { @@ -52,4 +36,3 @@ public void ShouldThrowAnErrorIfAllTablesExcluded() } } - diff --git a/Test/Configuration/WithOnlyIncludes.cs b/Test/Configuration/WithOnlyIncludes.cs index 765da73..bc27887 100644 --- a/Test/Configuration/WithOnlyIncludes.cs +++ b/Test/Configuration/WithOnlyIncludes.cs @@ -1,30 +1,11 @@ using InstantAPIs; -using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; using Xunit; namespace Test.Configuration; -public class WithOnlyIncludes : BaseFixture +public class WithOnlyIncludes : InstantAPIsConfigBuilderFixture { - InstantAPIsConfigBuilder _Builder; - - public WithOnlyIncludes() - { - - var _ContextOptions = new DbContextOptionsBuilder() - .UseInMemoryDatabase("TestDb") - .Options; - _Builder = new(new(_ContextOptions)); - - } - [Fact] public void ShouldNotIncludeAllTables() { diff --git a/Test/Configuration/WithoutIncludes.cs b/Test/Configuration/WithoutIncludes.cs index 11e4f1a..5756b36 100644 --- a/Test/Configuration/WithoutIncludes.cs +++ b/Test/Configuration/WithoutIncludes.cs @@ -39,4 +39,3 @@ public void ShouldIncludeAllTables() } } - From b47c1b00ebe94a0e217534a813efe12d1e7ce3bd Mon Sep 17 00:00:00 2001 From: Dries Verbeke Date: Tue, 29 Mar 2022 09:17:51 +0200 Subject: [PATCH 2/9] Remove class with single property The instiant apis' config class only holds one property. Instead of using a class to hold one property as a dto, pass only that property. Applying the yagni (yet) principle. --- InstantAPIs/InstantAPIsConfig.cs | 16 ++++------------ InstantAPIs/WebApplicationExtensions.cs | 6 +++--- .../WhenIncludeDoesNotSpecifyBaseUrl.cs | 4 ++-- .../Configuration/WhenIncludeSpecifiesBaseUrl.cs | 4 ++-- Test/Configuration/WithIncludesAndExcludes.cs | 4 ++-- Test/Configuration/WithOnlyExcludes.cs | 4 ++-- Test/Configuration/WithOnlyIncludes.cs | 6 +++--- Test/Configuration/WithoutIncludes.cs | 6 +++--- 8 files changed, 21 insertions(+), 29 deletions(-) diff --git a/InstantAPIs/InstantAPIsConfig.cs b/InstantAPIs/InstantAPIsConfig.cs index f7ce505..7cb1246 100644 --- a/InstantAPIs/InstantAPIsConfig.cs +++ b/InstantAPIs/InstantAPIsConfig.cs @@ -1,17 +1,9 @@ namespace InstantAPIs; -internal class InstantAPIsConfig -{ - - internal HashSet Tables { get; } = new HashSet(); - -} - - public class InstantAPIsConfigBuilder where D : DbContext { - private InstantAPIsConfig _Config = new(); + private HashSet _Config = new(); private Type _ContextType = typeof(D); private D _TheContext; private readonly HashSet _IncludedTables = new(); @@ -111,7 +103,7 @@ private void BuildTables() // Exit now if no tables were excluded if (!_ExcludedTables.Any()) { - _Config.Tables.UnionWith(outTables); + _Config.UnionWith(outTables); return; } @@ -120,13 +112,13 @@ private void BuildTables() if (outTables == null || !outTables.Any()) throw new ArgumentException("All tables were excluded from this configuration"); - _Config.Tables.UnionWith(outTables); + _Config.UnionWith(outTables); } #endregion - internal InstantAPIsConfig Build() + internal HashSet Build() { BuildTables(); diff --git a/InstantAPIs/WebApplicationExtensions.cs b/InstantAPIs/WebApplicationExtensions.cs index f52f6b6..b5fa269 100644 --- a/InstantAPIs/WebApplicationExtensions.cs +++ b/InstantAPIs/WebApplicationExtensions.cs @@ -14,7 +14,7 @@ public static class WebApplicationExtensions internal const string LOGGER_CATEGORY_NAME = "InstantAPI"; - private static InstantAPIsConfig Configuration { get; set; } = new(); + private static HashSet Configuration { get; set; } = new(); public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action> options = null) where D : DbContext { @@ -26,9 +26,9 @@ public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder // Get the tables on the DbContext var dbTables = GetDbTablesForContext(); - var requestedTables = !Configuration.Tables.Any() ? + var requestedTables = !Configuration.Any() ? dbTables : - Configuration.Tables.Where(t => dbTables.Any(db => db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray(); + Configuration.Where(t => dbTables.Any(db => db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray(); MapInstantAPIsUsingReflection(app, requestedTables); diff --git a/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs b/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs index a9e21ff..b392e48 100644 --- a/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs +++ b/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs @@ -16,8 +16,8 @@ public void ShouldSpecifyDefaultUrl() var config = _Builder.Build(); // assert - Assert.Single(config.Tables); - Assert.Equal(new Uri("/api/Contacts", uriKind: UriKind.Relative), config.Tables.First().BaseUrl); + Assert.Single(config); + Assert.Equal(new Uri("/api/Contacts", uriKind: UriKind.Relative), config.First().BaseUrl); } } diff --git a/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs b/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs index c2e113f..340e89b 100644 --- a/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs +++ b/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs @@ -17,8 +17,8 @@ public void ShouldSpecifyThatUrl() var config = _Builder.Build(); // assert - Assert.Single(config.Tables); - Assert.Equal(BaseUrl, config.Tables.First().BaseUrl); + Assert.Single(config); + Assert.Equal(BaseUrl, config.First().BaseUrl); } } diff --git a/Test/Configuration/WithIncludesAndExcludes.cs b/Test/Configuration/WithIncludesAndExcludes.cs index 4e28530..3c4af9e 100644 --- a/Test/Configuration/WithIncludesAndExcludes.cs +++ b/Test/Configuration/WithIncludesAndExcludes.cs @@ -19,8 +19,8 @@ public void ShouldExcludePreviouslyIncludedTable() var config = _Builder.Build(); // assert - Assert.Single(config.Tables); - Assert.Equal("Contacts", config.Tables.First().Name); + Assert.Single(config); + Assert.Equal("Contacts", config.First().Name); } diff --git a/Test/Configuration/WithOnlyExcludes.cs b/Test/Configuration/WithOnlyExcludes.cs index df23e5a..fe50448 100644 --- a/Test/Configuration/WithOnlyExcludes.cs +++ b/Test/Configuration/WithOnlyExcludes.cs @@ -16,8 +16,8 @@ public void ShouldExcludeSpecifiedTable() var config = _Builder.Build(); // assert - Assert.Single(config.Tables); - Assert.Equal("Contacts", config.Tables.First().Name); + Assert.Single(config); + Assert.Equal("Contacts", config.First().Name); } diff --git a/Test/Configuration/WithOnlyIncludes.cs b/Test/Configuration/WithOnlyIncludes.cs index bc27887..5ab4c32 100644 --- a/Test/Configuration/WithOnlyIncludes.cs +++ b/Test/Configuration/WithOnlyIncludes.cs @@ -17,8 +17,8 @@ public void ShouldNotIncludeAllTables() var config = _Builder.Build(); // assert - Assert.Single(config.Tables); - Assert.Equal("Contacts", config.Tables.First().Name); + Assert.Single(config); + Assert.Equal("Contacts", config.First().Name); } @@ -36,7 +36,7 @@ public void ShouldIncludeAndSetAPIMethodsToInclude(ApiMethodsToGenerate methodsT var config = _Builder.Build(); // assert - Assert.Equal(methodsToGenerate, config.Tables.First().ApiMethodsToGenerate); + Assert.Equal(methodsToGenerate, config.First().ApiMethodsToGenerate); } diff --git a/Test/Configuration/WithoutIncludes.cs b/Test/Configuration/WithoutIncludes.cs index 5756b36..708e9f9 100644 --- a/Test/Configuration/WithoutIncludes.cs +++ b/Test/Configuration/WithoutIncludes.cs @@ -32,9 +32,9 @@ public void ShouldIncludeAllTables() var config = _Builder.Build(); // assert - Assert.Equal(2, config.Tables.Count); - Assert.Equal(ApiMethodsToGenerate.All, config.Tables.First().ApiMethodsToGenerate); - Assert.Equal(ApiMethodsToGenerate.All, config.Tables.Skip(1).First().ApiMethodsToGenerate); + Assert.Equal(2, config.Count); + Assert.Equal(ApiMethodsToGenerate.All, config.First().ApiMethodsToGenerate); + Assert.Equal(ApiMethodsToGenerate.All, config.Skip(1).First().ApiMethodsToGenerate); } From 9c96a2f00b363be68f3ba44d865d8a543903b965 Mon Sep 17 00:00:00 2001 From: Dries Verbeke Date: Wed, 20 Apr 2022 16:34:37 +0200 Subject: [PATCH 3/9] Rename the InstantAPIsConfigBuilder Renaming the InstantAPIsConfigBuilder to the InstantAPIsBuilder as there is no need to make this distinction and the builder represent beter of what it is doing. Also renamed the file to match the class --- .../{InstantAPIsConfig.cs => InstantAPIsBuilder.cs} | 10 +++++----- InstantAPIs/WebApplicationExtensions.cs | 6 +++--- Test/Configuration/InstantAPIsConfigBuilderFixture.cs | 4 ++-- Test/Configuration/WithoutIncludes.cs | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) rename InstantAPIs/{InstantAPIsConfig.cs => InstantAPIsBuilder.cs} (90%) diff --git a/InstantAPIs/InstantAPIsConfig.cs b/InstantAPIs/InstantAPIsBuilder.cs similarity index 90% rename from InstantAPIs/InstantAPIsConfig.cs rename to InstantAPIs/InstantAPIsBuilder.cs index 7cb1246..6440e7c 100644 --- a/InstantAPIs/InstantAPIsConfig.cs +++ b/InstantAPIs/InstantAPIsBuilder.cs @@ -1,6 +1,6 @@ -namespace InstantAPIs; +namespace Microsoft.AspNetCore.Builder; -public class InstantAPIsConfigBuilder where D : DbContext +public class InstantAPIsBuilder where D : DbContext { private HashSet _Config = new(); @@ -10,7 +10,7 @@ public class InstantAPIsConfigBuilder where D : DbContext private readonly List _ExcludedTables = new(); private const string DEFAULT_URI = "/api/"; - public InstantAPIsConfigBuilder(D theContext) + public InstantAPIsBuilder(D theContext) { this._TheContext = theContext; } @@ -23,7 +23,7 @@ public InstantAPIsConfigBuilder(D theContext) /// Select the EntityFramework DbSet to include - Required /// A flags enumerable indicating the methods to generate. By default ALL are generated /// Configuration builder with this configuration applied - public InstantAPIsConfigBuilder IncludeTable(Func> entitySelector, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All, string baseUrl = "") where T : class + public InstantAPIsBuilder IncludeTable(Func> entitySelector, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All, string baseUrl = "") where T : class { var theSetType = entitySelector(_TheContext).GetType().BaseType; @@ -61,7 +61,7 @@ public InstantAPIsConfigBuilder IncludeTable(Func> entitySelec /// /// Select the entity to exclude from generation /// Configuration builder with this configuraiton applied - public InstantAPIsConfigBuilder ExcludeTable(Func> entitySelector) where T : class + public InstantAPIsBuilder ExcludeTable(Func> entitySelector) where T : class { var theSetType = entitySelector(_TheContext).GetType().BaseType; diff --git a/InstantAPIs/WebApplicationExtensions.cs b/InstantAPIs/WebApplicationExtensions.cs index b5fa269..1708372 100644 --- a/InstantAPIs/WebApplicationExtensions.cs +++ b/InstantAPIs/WebApplicationExtensions.cs @@ -16,7 +16,7 @@ public static class WebApplicationExtensions private static HashSet Configuration { get; set; } = new(); - public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action> options = null) where D : DbContext + public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action> options = null) where D : DbContext { if (app is IApplicationBuilder applicationBuilder) { @@ -71,7 +71,7 @@ private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, } } - private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action> options, IApplicationBuilder applicationBuilder) where D : DbContext + private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action> options, IApplicationBuilder applicationBuilder) where D : DbContext { // Check if AddInstantAPIs was called by getting the service options and evaluate EnableSwagger property var serviceOptions = applicationBuilder.ApplicationServices.GetRequiredService>().Value; @@ -89,7 +89,7 @@ private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action } var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetService(typeof(D)) as D; - var builder = new InstantAPIsConfigBuilder(ctx); + var builder = new InstantAPIsBuilder(ctx); if (options != null) { options(builder); diff --git a/Test/Configuration/InstantAPIsConfigBuilderFixture.cs b/Test/Configuration/InstantAPIsConfigBuilderFixture.cs index 84b1c62..7ed6e2e 100644 --- a/Test/Configuration/InstantAPIsConfigBuilderFixture.cs +++ b/Test/Configuration/InstantAPIsConfigBuilderFixture.cs @@ -1,11 +1,11 @@ -using InstantAPIs; +using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; namespace Test.Configuration; public abstract class InstantAPIsConfigBuilderFixture : BaseFixture { - internal InstantAPIsConfigBuilder _Builder; + internal InstantAPIsBuilder _Builder; public InstantAPIsConfigBuilderFixture() { diff --git a/Test/Configuration/WithoutIncludes.cs b/Test/Configuration/WithoutIncludes.cs index 708e9f9..fceb34e 100644 --- a/Test/Configuration/WithoutIncludes.cs +++ b/Test/Configuration/WithoutIncludes.cs @@ -9,7 +9,7 @@ namespace Test.Configuration; public class WithoutIncludes : BaseFixture { - InstantAPIsConfigBuilder _Builder; + InstantAPIsBuilder _Builder; public WithoutIncludes() { From bd3279d1e7a22989c30d19e4a6ddbc4877e6d79a Mon Sep 17 00:00:00 2001 From: Dries Verbeke Date: Fri, 8 Apr 2022 11:22:37 +0200 Subject: [PATCH 4/9] Resolving all warnings The build produced a couple warnings around null references and had some bad project references. In order not to hide real warnings that need to resolve. The idea is to make sure there are zero warnings --- ...nstantAPIs.Generators.Helpers.Tests.csproj | 3 +- ...ritz.InstantAPIs.Generators.Helpers.csproj | 1 + .../Fritz.InstantAPIs.Generators.csproj | 9 +- InstantAPIs/InstantAPIsBuilder.cs | 2 +- InstantAPIs/JsonAPIsConfig.cs | 8 +- InstantAPIs/JsonApiExtensions.cs | 238 +++++++++--------- InstantAPIs/MapApiExtensions.cs | 7 +- InstantAPIs/WebApplicationExtensions.cs | 26 +- Test/XunitLogger.cs | 41 ++- 9 files changed, 174 insertions(+), 161 deletions(-) diff --git a/Fritz.InstantAPIs.Generators.Helpers.Tests/Fritz.InstantAPIs.Generators.Helpers.Tests.csproj b/Fritz.InstantAPIs.Generators.Helpers.Tests/Fritz.InstantAPIs.Generators.Helpers.Tests.csproj index b59977f..e9054c8 100644 --- a/Fritz.InstantAPIs.Generators.Helpers.Tests/Fritz.InstantAPIs.Generators.Helpers.Tests.csproj +++ b/Fritz.InstantAPIs.Generators.Helpers.Tests/Fritz.InstantAPIs.Generators.Helpers.Tests.csproj @@ -2,7 +2,8 @@ net6.0 enable - + True + diff --git a/Fritz.InstantAPIs.Generators.Helpers/Fritz.InstantAPIs.Generators.Helpers.csproj b/Fritz.InstantAPIs.Generators.Helpers/Fritz.InstantAPIs.Generators.Helpers.csproj index 132c02c..7f98370 100644 --- a/Fritz.InstantAPIs.Generators.Helpers/Fritz.InstantAPIs.Generators.Helpers.csproj +++ b/Fritz.InstantAPIs.Generators.Helpers/Fritz.InstantAPIs.Generators.Helpers.csproj @@ -4,6 +4,7 @@ net6.0 enable enable + True diff --git a/Fritz.InstantAPIs.Generators/Fritz.InstantAPIs.Generators.csproj b/Fritz.InstantAPIs.Generators/Fritz.InstantAPIs.Generators.csproj index 5d5c4ed..cba8a4b 100644 --- a/Fritz.InstantAPIs.Generators/Fritz.InstantAPIs.Generators.csproj +++ b/Fritz.InstantAPIs.Generators/Fritz.InstantAPIs.Generators.csproj @@ -3,9 +3,10 @@ latest enable netstandard2.0 + True - - - - + + + + diff --git a/InstantAPIs/InstantAPIsBuilder.cs b/InstantAPIs/InstantAPIsBuilder.cs index 6440e7c..ae99dd3 100644 --- a/InstantAPIs/InstantAPIsBuilder.cs +++ b/InstantAPIs/InstantAPIsBuilder.cs @@ -108,7 +108,7 @@ private void BuildTables() } // Remove the Excluded tables - outTables = outTables.Where(t => !_ExcludedTables.Any(e => t.Name.Equals(e, StringComparison.InvariantCultureIgnoreCase))).ToArray(); + outTables = outTables.Where(t => !_ExcludedTables.Any(e => e.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase))).ToArray(); if (outTables == null || !outTables.Any()) throw new ArgumentException("All tables were excluded from this configuration"); diff --git a/InstantAPIs/JsonAPIsConfig.cs b/InstantAPIs/JsonAPIsConfig.cs index 6c4a90f..da51e1e 100644 --- a/InstantAPIs/JsonAPIsConfig.cs +++ b/InstantAPIs/JsonAPIsConfig.cs @@ -16,7 +16,7 @@ public class JsonAPIsConfigBuilder { private JsonAPIsConfig _Config = new(); - private string _FileName; + private string? _FileName; private readonly HashSet _IncludedTables = new(); private readonly List _ExcludedTables = new(); @@ -63,12 +63,12 @@ public JsonAPIsConfigBuilder ExcludeTable(string entityName) private HashSet IdentifyEntities() { - var writableDoc = JsonNode.Parse(File.ReadAllText(_FileName)); + var writableDoc = JsonNode.Parse(File.ReadAllText(_Config.JsonFilename)); // print API return writableDoc?.Root.AsObject() .AsEnumerable().Select(x => x.Key) - .ToHashSet(); + .ToHashSet() ?? new HashSet(); } @@ -106,7 +106,7 @@ private void BuildTables() } // Remove the Excluded tables - outTables = outTables.Where(t => !_ExcludedTables.Any(e => t.Name.Equals(e, StringComparison.InvariantCultureIgnoreCase))).ToArray(); + outTables = outTables.Where(t => !_ExcludedTables.Any(e => e.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase))).ToArray(); if (outTables == null || !outTables.Any()) throw new ArgumentException("All tables were excluded from this configuration"); diff --git a/InstantAPIs/JsonApiExtensions.cs b/InstantAPIs/JsonApiExtensions.cs index 5e2b740..4d894ae 100644 --- a/InstantAPIs/JsonApiExtensions.cs +++ b/InstantAPIs/JsonApiExtensions.cs @@ -7,123 +7,131 @@ namespace InstantAPIs; public static class JsonApiExtensions { - static JsonAPIsConfig _Config; + static JsonAPIsConfig _Config = new JsonAPIsConfig(); - public static WebApplication UseJsonRoutes(this WebApplication app, Action options = null) - { + public static WebApplication UseJsonRoutes(this WebApplication app, Action? options = null) + { - var builder = new JsonAPIsConfigBuilder(); - _Config = new JsonAPIsConfig(); - if (options != null) + var builder = new JsonAPIsConfigBuilder(); + if (options != null) { - options(builder); - _Config = builder.Build(); - } - - var writableDoc = JsonNode.Parse(File.ReadAllText(_Config.JsonFilename)); - - // print API - foreach (var elem in writableDoc?.Root.AsObject().AsEnumerable()) - { - - var thisEntity = _Config.Tables.FirstOrDefault(t => t.Name.Equals(elem.Key, StringComparison.InvariantCultureIgnoreCase)); - if (thisEntity == null) continue; - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Get) == ApiMethodsToGenerate.Get) - Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower())); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.GetById) == ApiMethodsToGenerate.GetById) - Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower()) + "/id"); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Insert) == ApiMethodsToGenerate.Insert) - Console.WriteLine(string.Format("POST /{0}", elem.Key.ToLower())); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Update) == ApiMethodsToGenerate.Update) - Console.WriteLine(string.Format("PUT /{0}", elem.Key.ToLower())); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Delete) == ApiMethodsToGenerate.Delete) - Console.WriteLine(string.Format("DELETE /{0}", elem.Key.ToLower()) + "/id"); - - Console.WriteLine(" "); - } - - // setup routes - foreach (var elem in writableDoc?.Root.AsObject().AsEnumerable()) - { - - var thisEntity = _Config.Tables.FirstOrDefault(t => t.Name.Equals(elem.Key, StringComparison.InvariantCultureIgnoreCase)); - if (thisEntity == null) continue; - - var arr = elem.Value.AsArray(); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Get) == ApiMethodsToGenerate.Get) - app.MapGet(string.Format("/{0}", elem.Key), () => elem.Value.ToString()); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.GetById) == ApiMethodsToGenerate.GetById) - app.MapGet(string.Format("/{0}", elem.Key) + "/{id}", (int id) => - { - var matchedItem = arr.SingleOrDefault(row => row - .AsObject() - .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) - ); - return matchedItem; - }); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Insert) == ApiMethodsToGenerate.Insert) - app.MapPost(string.Format("/{0}", elem.Key), async (HttpRequest request) => - { - string content = string.Empty; - using (StreamReader reader = new StreamReader(request.Body)) - { - content = await reader.ReadToEndAsync(); - } - var newNode = JsonNode.Parse(content); - var array = elem.Value.AsArray(); - newNode.AsObject().Add("Id", array.Count() + 1); - array.Add(newNode); - - File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); - return content; - }); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Update) == ApiMethodsToGenerate.Update) - app.MapPut(string.Format("/{0}", elem.Key), async (HttpRequest request) => + options(builder); + _Config = builder.Build(); + } + + var writableDoc = JsonNode.Parse(File.ReadAllText(_Config.JsonFilename)) + ?? throw new Exception("Missing json file"); + var sets = writableDoc.Root?.AsObject()?.AsEnumerable(); + if (sets == null || !sets.Any()) return app; + + // print API + foreach (var elem in sets) + { + + var thisEntity = _Config.Tables.FirstOrDefault(t => elem.Key.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)); + if (thisEntity == null) continue; + + if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Get) == ApiMethodsToGenerate.Get) + Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower())); + + if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.GetById) == ApiMethodsToGenerate.GetById) + Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower()) + "/id"); + + if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Insert) == ApiMethodsToGenerate.Insert) + Console.WriteLine(string.Format("POST /{0}", elem.Key.ToLower())); + + if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Update) == ApiMethodsToGenerate.Update) + Console.WriteLine(string.Format("PUT /{0}", elem.Key.ToLower())); + + if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Delete) == ApiMethodsToGenerate.Delete) + Console.WriteLine(string.Format("DELETE /{0}", elem.Key.ToLower()) + "/id"); + + Console.WriteLine(" "); + } + + // setup routes + foreach (var elem in sets) { - string content = string.Empty; - using (StreamReader reader = new StreamReader(request.Body)) - { - content = await reader.ReadToEndAsync(); - } - var newNode = JsonNode.Parse(content); - var array = elem.Value.AsArray(); - array.Add(newNode); - - File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); - - return "OK"; - }); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Delete) == ApiMethodsToGenerate.Delete) - app.MapDelete(string.Format("/{0}", elem.Key) + "/{id}", (int id) => - { - - var matchedItem = arr - .Select((value, index) => new { value, index }) - .SingleOrDefault(row => row.value - .AsObject() - .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) - ); - if (matchedItem != null) - { - arr.RemoveAt(matchedItem.index); - File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); - } - - return "OK"; - }); - - }; - - return app; - } + + var thisEntity = _Config.Tables.FirstOrDefault(t => elem.Key.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)); + if (thisEntity == null) continue; + + var arr = elem.Value?.AsArray() ?? new JsonArray(); + + if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Get) == ApiMethodsToGenerate.Get) + app.MapGet(string.Format("/{0}", elem.Key), () => elem.Value?.ToString()); + + if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.GetById) == ApiMethodsToGenerate.GetById) + app.MapGet(string.Format("/{0}", elem.Key) + "/{id}", (int id) => + { + var matchedItem = arr == null ? null : + arr.SingleOrDefault(row => row != null && row + .AsObject() + .Any(o => o.Key.ToLower() == "id" && o.Value != null && int.Parse(o.Value.ToString()) == id) + ); + return matchedItem == null ? Results.NotFound() : Results.Ok(matchedItem); + }); + + if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Insert) == ApiMethodsToGenerate.Insert) + app.MapPost(string.Format("/{0}", elem.Key), async (HttpRequest request) => + { + string content = string.Empty; + using (StreamReader reader = new StreamReader(request.Body)) + { + content = await reader.ReadToEndAsync(); + } + var newNode = JsonNode.Parse(content); + var array = elem.Value?.AsArray(); + if (newNode == null || array == null) return Results.NotFound(); + newNode.AsObject().Add("Id", array.Count() + 1); + array.Add(newNode); + + File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); + return Results.Ok(content); + }); + + if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Update) == ApiMethodsToGenerate.Update) + app.MapPut(string.Format("/{0}", elem.Key), async (HttpRequest request) => + { + string content = string.Empty; + using (StreamReader reader = new StreamReader(request.Body)) + { + content = await reader.ReadToEndAsync(); + } + var newNode = JsonNode.Parse(content); + var array = elem.Value?.AsArray(); + if (array != null) + { + array.Add(newNode); + + File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); + } + + return "OK"; + }); + + if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Delete) == ApiMethodsToGenerate.Delete) + app.MapDelete(string.Format("/{0}", elem.Key) + "/{id}", (int id) => + { + + var matchedItem = arr + .Select((value, index) => new { value, index }) + .SingleOrDefault(row => row.value + ?.AsObject() + ?.Any(o => o.Key.ToLower() == "id" && o.Value != null && int.Parse(o.Value.ToString()) == id) + ?? false + ); + if (matchedItem != null) + { + arr.RemoveAt(matchedItem.index); + File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); + } + + return "OK"; + }); + + }; + + return app; + } } \ No newline at end of file diff --git a/InstantAPIs/MapApiExtensions.cs b/InstantAPIs/MapApiExtensions.cs index 2df9539..abb43d0 100644 --- a/InstantAPIs/MapApiExtensions.cs +++ b/InstantAPIs/MapApiExtensions.cs @@ -5,6 +5,7 @@ using System.ComponentModel.DataAnnotations; using System.Reflection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; namespace InstantAPIs; @@ -14,7 +15,7 @@ internal class MapApiExtensions // TODO: Authentication / Authorization private static Dictionary _IdLookup = new(); - private static ILogger Logger; + private static ILogger Logger = NullLogger.Instance; internal static void Initialize(ILogger logger) where D: DbContext @@ -62,7 +63,7 @@ internal static void MapGetById(IEndpointRouteBuilder app, string url) app.MapGet($"{url}/{{id}}", async ([FromServices] D db, [FromRoute] string id) => { - C outValue = default(C); + var outValue = default(C); if (idProp.PropertyType == typeof(Guid)) outValue = await db.Set().FindAsync(Guid.Parse(id)); else if (idProp.PropertyType == typeof(int)) @@ -91,7 +92,7 @@ internal static void MapInstantPost(IEndpointRouteBuilder app, string url) db.Add(newObj); await db.SaveChangesAsync(); var id = _IdLookup[typeof(C)].GetValue(newObj); - return Results.Created($"{url}/{id.ToString()}", newObj); + return Results.Created($"{url}/{id}", newObj); }); } diff --git a/InstantAPIs/WebApplicationExtensions.cs b/InstantAPIs/WebApplicationExtensions.cs index 1708372..d70c7a6 100644 --- a/InstantAPIs/WebApplicationExtensions.cs +++ b/InstantAPIs/WebApplicationExtensions.cs @@ -16,7 +16,7 @@ public static class WebApplicationExtensions private static HashSet Configuration { get; set; } = new(); - public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action> options = null) where D : DbContext + public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action>? options = null) where D : DbContext { if (app is IApplicationBuilder applicationBuilder) { @@ -28,7 +28,7 @@ public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder var requestedTables = !Configuration.Any() ? dbTables : - Configuration.Where(t => dbTables.Any(db => db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray(); + Configuration.Where(t => dbTables.Any(db => db.Name != null && db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray(); MapInstantAPIsUsingReflection(app, requestedTables); @@ -46,14 +46,15 @@ private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, } var allMethods = typeof(MapApiExtensions).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(m => m.Name.StartsWith("Map")).ToArray(); - var initialize = typeof(MapApiExtensions).GetMethod("Initialize", BindingFlags.NonPublic | BindingFlags.Static); + var initialize = typeof(MapApiExtensions).GetMethod("Initialize", BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new Exception($"{nameof(MapApiExtensions)} doest have an {nameof(MapApiExtensions.Initialize)}, this should never happen"); foreach (var table in requestedTables) { // The default URL for an InstantAPI is /api/TABLENAME //var url = $"/api/{table.Name}"; - initialize.MakeGenericMethod(typeof(D), table.InstanceType).Invoke(null, new[] { logger }); + initialize.MakeGenericMethod(typeof(D), table.InstanceType ?? throw new Exception($"Instance type for table {table.Name} is null")).Invoke(null, new[] { logger }); // The remaining private static methods in this class build out the Mapped API methods.. // let's use some reflection to get them @@ -61,17 +62,18 @@ private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, { var sigAttr = method.CustomAttributes.First(x => x.AttributeType == typeof(ApiMethodAttribute)).ConstructorArguments.First(); + if (sigAttr.Value == null) continue; var methodType = (ApiMethodsToGenerate)sigAttr.Value; if ((table.ApiMethodsToGenerate & methodType) != methodType) continue; var genericMethod = method.MakeGenericMethod(typeof(D), table.InstanceType); - genericMethod.Invoke(null, new object[] { app, table.BaseUrl.ToString() }); + genericMethod.Invoke(null, new object[] { app, table.BaseUrl?.ToString() ?? throw new Exception($"BaseUrl for {table.Name} is null") }); } } } - private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action> options, IApplicationBuilder applicationBuilder) where D : DbContext + private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action>? options, IApplicationBuilder applicationBuilder) where D : DbContext { // Check if AddInstantAPIs was called by getting the service options and evaluate EnableSwagger property var serviceOptions = applicationBuilder.ApplicationServices.GetRequiredService>().Value; @@ -88,7 +90,7 @@ private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action applicationBuilder.UseSwaggerUI(); } - var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetService(typeof(D)) as D; + var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService(); var builder = new InstantAPIsBuilder(ctx); if (options != null) { @@ -100,8 +102,8 @@ private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action internal static IEnumerable GetDbTablesForContext() where D : DbContext { return typeof(D).GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(x => x.PropertyType.FullName.StartsWith("Microsoft.EntityFrameworkCore.DbSet") - && x.PropertyType.GenericTypeArguments.First().GetCustomAttributes(typeof(KeylessAttribute), true).Length <= 0) + .Where(x => (x.PropertyType.FullName?.StartsWith("Microsoft.EntityFrameworkCore.DbSet")).GetValueOrDefault() + && x.PropertyType.GenericTypeArguments.First().GetCustomAttributes(typeof(KeylessAttribute), true).Length <= 0) .Select(x => new TypeTable { Name = x.Name, InstanceType = x.PropertyType.GenericTypeArguments.First(), @@ -112,10 +114,10 @@ internal static IEnumerable GetDbTablesForContext() where D : DbCo internal class TypeTable { - public string Name { get; set; } - public Type InstanceType { get; set; } + public string? Name { get; set; } + public Type? InstanceType { get; set; } public ApiMethodsToGenerate ApiMethodsToGenerate { get; set; } = ApiMethodsToGenerate.All; - public Uri BaseUrl { get; set; } + public Uri? BaseUrl { get; set; } } } diff --git a/Test/XunitLogger.cs b/Test/XunitLogger.cs index b3f8443..df4835a 100644 --- a/Test/XunitLogger.cs +++ b/Test/XunitLogger.cs @@ -1,33 +1,32 @@ using Microsoft.Extensions.Logging; -using System; using Xunit.Abstractions; namespace Test; public class XunitLogger : ILogger, IDisposable { - private ITestOutputHelper _output; + private ITestOutputHelper _output; - public XunitLogger(ITestOutputHelper output) - { - _output = output; - } - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - _output.WriteLine(state.ToString()); - } + public XunitLogger(ITestOutputHelper output) + { + _output = output; + } + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + _output.WriteLine(state?.ToString()); + } - public bool IsEnabled(LogLevel logLevel) - { - return true; - } + public bool IsEnabled(LogLevel logLevel) + { + return true; + } - public IDisposable BeginScope(TState state) - { - return this; - } + public IDisposable BeginScope(TState state) + { + return this; + } - public void Dispose() - { - } + public void Dispose() + { + } } From 42f131c6555906efe57d40491ee3fc761ed211e2 Mon Sep 17 00:00:00 2001 From: Dries Verbeke Date: Wed, 20 Apr 2022 16:48:21 +0200 Subject: [PATCH 5/9] Renaming InstantAPIsServiceOptions To be more inline with other library setups we rename the ServiceOptions to plain Options. There is no specific need for ServiceOptions in a plugin. --- .../{InstantAPIsServiceOptions.cs => InstantAPIsOptions.cs} | 2 +- InstantAPIs/InstantAPIsServiceCollectionExtensions.cs | 6 +++--- InstantAPIs/WebApplicationExtensions.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename InstantAPIs/{InstantAPIsServiceOptions.cs => InstantAPIsOptions.cs} (86%) diff --git a/InstantAPIs/InstantAPIsServiceOptions.cs b/InstantAPIs/InstantAPIsOptions.cs similarity index 86% rename from InstantAPIs/InstantAPIsServiceOptions.cs rename to InstantAPIs/InstantAPIsOptions.cs index 6c540ce..943630b 100644 --- a/InstantAPIs/InstantAPIsServiceOptions.cs +++ b/InstantAPIs/InstantAPIsOptions.cs @@ -9,7 +9,7 @@ public enum EnableSwagger Always } -public class InstantAPIsServiceOptions +public class InstantAPIsOptions { public EnableSwagger? EnableSwagger { get; set; } diff --git a/InstantAPIs/InstantAPIsServiceCollectionExtensions.cs b/InstantAPIs/InstantAPIsServiceCollectionExtensions.cs index c2ed9cb..b32c31c 100644 --- a/InstantAPIs/InstantAPIsServiceCollectionExtensions.cs +++ b/InstantAPIs/InstantAPIsServiceCollectionExtensions.cs @@ -4,9 +4,9 @@ namespace InstantAPIs; public static class InstantAPIsServiceCollectionExtensions { - public static IServiceCollection AddInstantAPIs(this IServiceCollection services, Action? setupAction = null) + public static IServiceCollection AddInstantAPIs(this IServiceCollection services, Action? setupAction = null) { - var options = new InstantAPIsServiceOptions(); + var options = new InstantAPIsOptions(); // Get the service options setupAction?.Invoke(options); @@ -24,7 +24,7 @@ public static IServiceCollection AddInstantAPIs(this IServiceCollection services } // Register the required options so that it can be accessed by InstantAPIs middleware - services.Configure(config => + services.Configure(config => { config.EnableSwagger = options.EnableSwagger; }); diff --git a/InstantAPIs/WebApplicationExtensions.cs b/InstantAPIs/WebApplicationExtensions.cs index d70c7a6..bb4bb18 100644 --- a/InstantAPIs/WebApplicationExtensions.cs +++ b/InstantAPIs/WebApplicationExtensions.cs @@ -76,7 +76,7 @@ private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action>? options, IApplicationBuilder applicationBuilder) where D : DbContext { // Check if AddInstantAPIs was called by getting the service options and evaluate EnableSwagger property - var serviceOptions = applicationBuilder.ApplicationServices.GetRequiredService>().Value; + var serviceOptions = applicationBuilder.ApplicationServices.GetRequiredService>().Value; if (serviceOptions == null || serviceOptions.EnableSwagger == null) { throw new ArgumentException("Call builder.Services.AddInstantAPIs(options) before MapInstantAPIs."); From dab302cbf9aacf5e6a8699d4e60d6d65c1f47fc4 Mon Sep 17 00:00:00 2001 From: Dries Verbeke Date: Tue, 19 Jul 2022 17:33:29 +0200 Subject: [PATCH 6/9] Remove type table and only use a single table class for configuration --- InstantAPIs/ApiMethodsToGenerate.cs | 6 ---- InstantAPIs/InstantAPIsBuilder.cs | 32 ++++++++------------- InstantAPIs/JsonAPIsConfig.cs | 37 ++++++++++--------------- InstantAPIs/WebApplicationExtensions.cs | 22 ++++----------- 4 files changed, 32 insertions(+), 65 deletions(-) diff --git a/InstantAPIs/ApiMethodsToGenerate.cs b/InstantAPIs/ApiMethodsToGenerate.cs index db24ed2..fe93fad 100644 --- a/InstantAPIs/ApiMethodsToGenerate.cs +++ b/InstantAPIs/ApiMethodsToGenerate.cs @@ -11,12 +11,6 @@ public enum ApiMethodsToGenerate All = 31 } -public record TableApiMapping( - string TableName, - ApiMethodsToGenerate MethodsToGenerate = ApiMethodsToGenerate.All, - string BaseUrl = "" -); - [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] public class ApiMethodAttribute : Attribute { diff --git a/InstantAPIs/InstantAPIsBuilder.cs b/InstantAPIs/InstantAPIsBuilder.cs index ae99dd3..0825030 100644 --- a/InstantAPIs/InstantAPIsBuilder.cs +++ b/InstantAPIs/InstantAPIsBuilder.cs @@ -3,10 +3,10 @@ public class InstantAPIsBuilder where D : DbContext { - private HashSet _Config = new(); + private HashSet _Config = new(); private Type _ContextType = typeof(D); private D _TheContext; - private readonly HashSet _IncludedTables = new(); + private readonly HashSet _IncludedTables = new(); private readonly List _ExcludedTables = new(); private const string DEFAULT_URI = "/api/"; @@ -43,13 +43,13 @@ public InstantAPIsBuilder IncludeTable(Func> entitySelector, A } else { - baseUrl = String.Concat(DEFAULT_URI, property.Name); + baseUrl = string.Concat(DEFAULT_URI, property.Name); } - var tableApiMapping = new TableApiMapping(property.Name, methodsToGenerate, baseUrl); + var tableApiMapping = new InstantAPIsOptions.Table(property.Name, new Uri(baseUrl), typeof(T)) { ApiMethodsToGenerate = methodsToGenerate }; _IncludedTables.Add(tableApiMapping); - if (_ExcludedTables.Contains(tableApiMapping.TableName)) _ExcludedTables.Remove(tableApiMapping.TableName); + if (_ExcludedTables.Contains(tableApiMapping.Name)) _ExcludedTables.Remove(tableApiMapping.Name); _IncludedTables.Add(tableApiMapping); return this; @@ -67,7 +67,7 @@ public InstantAPIsBuilder ExcludeTable(Func> entitySelector) w var theSetType = entitySelector(_TheContext).GetType().BaseType; var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType); - if (_IncludedTables.Select(t => t.TableName).Contains(property.Name)) _IncludedTables.Remove(_IncludedTables.First(t => t.TableName == property.Name)); + if (_IncludedTables.Select(t => t.Name).Contains(property.Name)) _IncludedTables.Remove(_IncludedTables.First(t => t.Name == property.Name)); _ExcludedTables.Add(property.Name); return this; @@ -78,26 +78,18 @@ private void BuildTables() { var tables = WebApplicationExtensions.GetDbTablesForContext().ToArray(); - WebApplicationExtensions.TypeTable[]? outTables; + InstantAPIsOptions.Table[]? outTables; // Add the Included tables if (_IncludedTables.Any()) { - outTables = tables.Where(t => _IncludedTables.Any(i => i.TableName.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase))) - .Select(t => new WebApplicationExtensions.TypeTable + outTables = tables.Where(t => _IncludedTables.Any(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase))) + .Select(t => new InstantAPIsOptions.Table(t.Name, new Uri(_IncludedTables.First(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).BaseUrl.ToString(), UriKind.Relative), t.InstanceType) { - Name = t.Name, - InstanceType = t.InstanceType, - ApiMethodsToGenerate = _IncludedTables.First(i => i.TableName.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).MethodsToGenerate, - BaseUrl = new Uri(_IncludedTables.First(i => i.TableName.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).BaseUrl, UriKind.Relative) + ApiMethodsToGenerate = _IncludedTables.First(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).ApiMethodsToGenerate }).ToArray(); } else { - outTables = tables.Select(t => new WebApplicationExtensions.TypeTable - { - Name = t.Name, - InstanceType = t.InstanceType, - BaseUrl = new Uri(DEFAULT_URI + t.Name, uriKind: UriKind.Relative) - }).ToArray(); + outTables = tables.Select(t => new InstantAPIsOptions.Table(t.Name, new Uri(DEFAULT_URI + t.Name, uriKind: UriKind.Relative), t.InstanceType)).ToArray(); } // Exit now if no tables were excluded @@ -118,7 +110,7 @@ private void BuildTables() #endregion - internal HashSet Build() + internal HashSet Build() { BuildTables(); diff --git a/InstantAPIs/JsonAPIsConfig.cs b/InstantAPIs/JsonAPIsConfig.cs index da51e1e..42c6a36 100644 --- a/InstantAPIs/JsonAPIsConfig.cs +++ b/InstantAPIs/JsonAPIsConfig.cs @@ -1,11 +1,12 @@ -using System.Text.Json.Nodes; +using System.Linq; +using System.Text.Json.Nodes; namespace InstantAPIs; internal class JsonAPIsConfig { - internal HashSet Tables { get; } = new HashSet(); + internal HashSet Tables { get; } = new HashSet(); internal string JsonFilename = "mock.json"; @@ -17,7 +18,7 @@ public class JsonAPIsConfigBuilder private JsonAPIsConfig _Config = new(); private string? _FileName; - private readonly HashSet _IncludedTables = new(); + private readonly HashSet _IncludedTables = new(); private readonly List _ExcludedTables = new(); public JsonAPIsConfigBuilder SetFilename(string fileName) @@ -37,10 +38,10 @@ public JsonAPIsConfigBuilder SetFilename(string fileName) public JsonAPIsConfigBuilder IncludeEntity(string entityName, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All) { - var tableApiMapping = new TableApiMapping(entityName, methodsToGenerate); + var tableApiMapping = new InstantAPIsOptions.Table(entityName, new Uri(entityName, UriKind.Relative), typeof(JsonObject)) { ApiMethodsToGenerate = methodsToGenerate }; _IncludedTables.Add(tableApiMapping); - if (_ExcludedTables.Contains(entityName)) _ExcludedTables.Remove(tableApiMapping.TableName); + if (_ExcludedTables.Contains(entityName)) _ExcludedTables.Remove(tableApiMapping.Name); return this; @@ -54,7 +55,7 @@ public JsonAPIsConfigBuilder IncludeEntity(string entityName, ApiMethodsToGenera public JsonAPIsConfigBuilder ExcludeTable(string entityName) { - if (_IncludedTables.Select(t => t.TableName).Contains(entityName)) _IncludedTables.Remove(_IncludedTables.First(t => t.TableName == entityName)); + if (_IncludedTables.Select(t => t.Name).Contains(entityName)) _IncludedTables.Remove(_IncludedTables.First(t => t.Name == entityName)); _ExcludedTables.Add(entityName); return this; @@ -75,34 +76,26 @@ private HashSet IdentifyEntities() private void BuildTables() { - var tables = IdentifyEntities(); + var tables = IdentifyEntities() + .Select(t => new InstantAPIsOptions.Table(t, new Uri(t, UriKind.Relative), typeof(JsonObject)) + { + ApiMethodsToGenerate = ApiMethodsToGenerate.All + }); if (!_IncludedTables.Any() && !_ExcludedTables.Any()) { - _Config.Tables.UnionWith(tables.Select(t => new WebApplicationExtensions.TypeTable - { - Name = t, - ApiMethodsToGenerate = ApiMethodsToGenerate.All - })); + _Config.Tables.UnionWith(tables); return; } // Add the Included tables var outTables = _IncludedTables - .Select(t => new WebApplicationExtensions.TypeTable - { - Name = t.TableName, - ApiMethodsToGenerate = t.MethodsToGenerate - }).ToArray(); + .ToArray(); // If no tables were added, added them all if (outTables.Length == 0) { - outTables = tables.Select(t => new WebApplicationExtensions.TypeTable - { - Name = t, - ApiMethodsToGenerate = ApiMethodsToGenerate.All - }).ToArray(); + outTables = tables.ToArray(); } // Remove the Excluded tables diff --git a/InstantAPIs/WebApplicationExtensions.cs b/InstantAPIs/WebApplicationExtensions.cs index bb4bb18..be00374 100644 --- a/InstantAPIs/WebApplicationExtensions.cs +++ b/InstantAPIs/WebApplicationExtensions.cs @@ -14,7 +14,7 @@ public static class WebApplicationExtensions internal const string LOGGER_CATEGORY_NAME = "InstantAPI"; - private static HashSet Configuration { get; set; } = new(); + private static HashSet Configuration { get; set; } = new(); public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action>? options = null) where D : DbContext { @@ -35,7 +35,7 @@ public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder return app; } - private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, IEnumerable requestedTables) where D : DbContext + private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, IEnumerable requestedTables) where D : DbContext { ILogger logger = NullLogger.Instance; @@ -99,25 +99,13 @@ private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action } } - internal static IEnumerable GetDbTablesForContext() where D : DbContext + internal static IEnumerable GetDbTablesForContext() where D : DbContext { return typeof(D).GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(x => (x.PropertyType.FullName?.StartsWith("Microsoft.EntityFrameworkCore.DbSet")).GetValueOrDefault() + .Where(x => (x.PropertyType.FullName?.StartsWith("Microsoft.EntityFrameworkCore.DbSet") ?? false) && x.PropertyType.GenericTypeArguments.First().GetCustomAttributes(typeof(KeylessAttribute), true).Length <= 0) - .Select(x => new TypeTable { - Name = x.Name, - InstanceType = x.PropertyType.GenericTypeArguments.First(), - BaseUrl = new Uri($"/api/{x.Name}", uriKind: UriKind.RelativeOrAbsolute) - }) + .Select(x => new InstantAPIsOptions.Table(x.Name, new Uri($"/api/{x.Name}", uriKind: UriKind.RelativeOrAbsolute), x.PropertyType.GenericTypeArguments.First())) .ToArray(); } - internal class TypeTable - { - public string? Name { get; set; } - public Type? InstanceType { get; set; } - public ApiMethodsToGenerate ApiMethodsToGenerate { get; set; } = ApiMethodsToGenerate.All; - public Uri? BaseUrl { get; set; } - } - } From 3ebe5d590c298ad56153fd64ffebc5fceff69097 Mon Sep 17 00:00:00 2001 From: Dries Verbeke Date: Fri, 22 Jul 2022 00:13:53 +0200 Subject: [PATCH 7/9] Using expressions and generic parameters. - Renaming existing generic types to convention Txxx - Add generic types for set, entity, key - Use an expression to identity the dbcontext.set. This ensure better/correct configuration --- InstantAPIs/InstantAPIsBuilder.cs | 97 ++++++++++++++----- InstantAPIs/InstantAPIsOptions.cs | 49 ++++++++++ InstantAPIs/JsonAPIsConfig.cs | 35 ++++++- InstantAPIs/WebApplicationExtensions.cs | 32 +++--- .../WhenIncludeDoesNotSpecifyBaseUrl.cs | 5 +- .../WhenIncludeSpecifiesBaseUrl.cs | 5 +- Test/Configuration/WithIncludesAndExcludes.cs | 7 +- Test/Configuration/WithOnlyIncludes.cs | 4 +- WorkingApi/Program.cs | 2 +- 9 files changed, 184 insertions(+), 52 deletions(-) diff --git a/InstantAPIs/InstantAPIsBuilder.cs b/InstantAPIs/InstantAPIsBuilder.cs index 0825030..c6f61db 100644 --- a/InstantAPIs/InstantAPIsBuilder.cs +++ b/InstantAPIs/InstantAPIsBuilder.cs @@ -1,16 +1,20 @@ -namespace Microsoft.AspNetCore.Builder; +using System.Linq.Expressions; +using System.Reflection; -public class InstantAPIsBuilder where D : DbContext +namespace Microsoft.AspNetCore.Builder; + +public class InstantAPIsBuilder + where TContext : DbContext { - private HashSet _Config = new(); - private Type _ContextType = typeof(D); - private D _TheContext; - private readonly HashSet _IncludedTables = new(); + private HashSet _Config = new(); + private Type _ContextType = typeof(TContext); + private TContext _TheContext; + private readonly HashSet _IncludedTables = new(); private readonly List _ExcludedTables = new(); private const string DEFAULT_URI = "/api/"; - public InstantAPIsBuilder(D theContext) + public InstantAPIsBuilder(TContext theContext) { this._TheContext = theContext; } @@ -20,13 +24,16 @@ public InstantAPIsBuilder(D theContext) /// /// Specify individual tables to include in the API generation with the methods requested /// - /// Select the EntityFramework DbSet to include - Required + /// Select the EntityFramework DbSet to include - Required /// A flags enumerable indicating the methods to generate. By default ALL are generated /// Configuration builder with this configuration applied - public InstantAPIsBuilder IncludeTable(Func> entitySelector, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All, string baseUrl = "") where T : class + public InstantAPIsBuilder IncludeTable(Expression> setSelector, + InstantAPIsOptions.TableOptions config, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All, string baseUrl = "") + where TSet : DbSet + where TEntity : class { - var theSetType = entitySelector(_TheContext).GetType().BaseType; + var theSetType = setSelector.Compile()(_TheContext).GetType().BaseType; var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType); if (!string.IsNullOrEmpty(baseUrl)) @@ -46,7 +53,10 @@ public InstantAPIsBuilder IncludeTable(Func> entitySelector, A baseUrl = string.Concat(DEFAULT_URI, property.Name); } - var tableApiMapping = new InstantAPIsOptions.Table(property.Name, new Uri(baseUrl), typeof(T)) { ApiMethodsToGenerate = methodsToGenerate }; + var tableApiMapping = new InstantAPIsOptions.Table(property.Name, new Uri(baseUrl, UriKind.Relative), setSelector, config) + { + ApiMethodsToGenerate = methodsToGenerate + }; _IncludedTables.Add(tableApiMapping); if (_ExcludedTables.Contains(tableApiMapping.Name)) _ExcludedTables.Remove(tableApiMapping.Name); @@ -61,7 +71,7 @@ public InstantAPIsBuilder IncludeTable(Func> entitySelector, A /// /// Select the entity to exclude from generation /// Configuration builder with this configuraiton applied - public InstantAPIsBuilder ExcludeTable(Func> entitySelector) where T : class + public InstantAPIsBuilder ExcludeTable(Func> entitySelector) where T : class { var theSetType = entitySelector(_TheContext).GetType().BaseType; @@ -76,20 +86,28 @@ public InstantAPIsBuilder ExcludeTable(Func> entitySelector) w private void BuildTables() { - - var tables = WebApplicationExtensions.GetDbTablesForContext().ToArray(); - InstantAPIsOptions.Table[]? outTables; + var tables = WebApplicationExtensions.GetDbTablesForContext().ToArray(); + InstantAPIsOptions.ITable[]? outTables; // Add the Included tables if (_IncludedTables.Any()) { outTables = tables.Where(t => _IncludedTables.Any(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase))) - .Select(t => new InstantAPIsOptions.Table(t.Name, new Uri(_IncludedTables.First(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).BaseUrl.ToString(), UriKind.Relative), t.InstanceType) - { - ApiMethodsToGenerate = _IncludedTables.First(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).ApiMethodsToGenerate - }).ToArray(); + .Select(t => { + var table = CreateTable(t.Name, new Uri(_IncludedTables.First(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).BaseUrl.ToString(), UriKind.Relative), typeof(TContext), typeof(DbSet<>).MakeGenericType(t.InstanceType), t.InstanceType); + if (table != null) + { + table.ApiMethodsToGenerate = _IncludedTables.First(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).ApiMethodsToGenerate; + } + return table; + }) + .Where(x => x != null).OfType() + .ToArray(); } else { - outTables = tables.Select(t => new InstantAPIsOptions.Table(t.Name, new Uri(DEFAULT_URI + t.Name, uriKind: UriKind.Relative), t.InstanceType)).ToArray(); + outTables = tables + .Select(t => CreateTable(t.Name, new Uri(DEFAULT_URI + t.Name, uriKind: UriKind.Relative), typeof(TContext), typeof(DbSet<>).MakeGenericType(t.InstanceType), t.InstanceType)) + .Where(x => x != null).OfType() + .ToArray(); } // Exit now if no tables were excluded @@ -108,9 +126,44 @@ private void BuildTables() } -#endregion + public static InstantAPIsOptions.ITable? CreateTable(string name, Uri baseUrl, Type contextType, Type setType, Type entityType) + { + var keyProperty = entityType.GetProperties().Where(x => "id".Equals(x.Name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + if (keyProperty == null) return null; + + var genericMethod = typeof(InstantAPIsBuilder<>).MakeGenericType(contextType).GetMethod(nameof(CreateTableGeneric), BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new Exception("Missing method"); + var concreteMethod = genericMethod.MakeGenericMethod(contextType, setType, entityType, keyProperty.PropertyType); + + var entitySelector = CreateExpression(contextType, name, setType); + var keySelector = CreateExpression(entityType, keyProperty.Name, keyProperty.PropertyType); + return concreteMethod.Invoke(null, new object?[] { name, baseUrl, entitySelector, keySelector, null }) as InstantAPIsOptions.ITable; + } + + private static object CreateExpression(Type memberOwnerType, string property, Type returnType) + { + var parameterExpression = Expression.Parameter(memberOwnerType, "x"); + var propertyExpression = Expression.Property(parameterExpression, property); + //var block = Expression.Block(propertyExpression, returnExpression); + return Expression.Lambda(typeof(Func<,>).MakeGenericType(memberOwnerType, returnType), propertyExpression, parameterExpression); + } + + private static InstantAPIsOptions.ITable CreateTableGeneric(string name, Uri baseUrl, + Expression> entitySelector, Expression>? keySelector, Expression>? orderBy) + where TContextStatic : class + where TSet : class + where TEntity : class + { + return new InstantAPIsOptions.Table(name, baseUrl, entitySelector, + new InstantAPIsOptions.TableOptions() + { + KeySelector = keySelector, + OrderBy = orderBy + }); + } + #endregion - internal HashSet Build() + internal HashSet Build() { BuildTables(); diff --git a/InstantAPIs/InstantAPIsOptions.cs b/InstantAPIs/InstantAPIsOptions.cs index 943630b..274cd7c 100644 --- a/InstantAPIs/InstantAPIsOptions.cs +++ b/InstantAPIs/InstantAPIsOptions.cs @@ -1,4 +1,5 @@ using Swashbuckle.AspNetCore.SwaggerGen; +using System.Linq.Expressions; namespace InstantAPIs; @@ -14,4 +15,52 @@ public class InstantAPIsOptions public EnableSwagger? EnableSwagger { get; set; } public Action? Swagger { get; set; } + + internal class Table + : ITable + { + public Table(string name, Uri baseUrl, Expression> entitySelector, TableOptions config) + { + Name = name; + BaseUrl = baseUrl; + EntitySelector = entitySelector; + Config = config; + + RepoType = typeof(TContext); + InstanceType = typeof(TEntity); + } + + public string Name { get; } + public Type RepoType { get; } + public Type InstanceType { get; } + public Uri BaseUrl { get; set; } + + public Expression> EntitySelector { get; } + public TableOptions Config { get; } + + public ApiMethodsToGenerate ApiMethodsToGenerate { get; set; } = ApiMethodsToGenerate.All; + + public object EntitySelectorObject => EntitySelector; + public object ConfigObject => Config; + } + + public interface ITable + { + public string Name { get; } + public Type RepoType { get; } + public Type InstanceType { get; } + public Uri BaseUrl { get; set; } + public ApiMethodsToGenerate ApiMethodsToGenerate { get; set; } + + public object EntitySelectorObject { get; } + public object ConfigObject { get; } + + } + + public record TableOptions() + { + public Expression>? KeySelector { get; set; } + + public Expression>? OrderBy { get; set; } + } } \ No newline at end of file diff --git a/InstantAPIs/JsonAPIsConfig.cs b/InstantAPIs/JsonAPIsConfig.cs index 42c6a36..5f1abf2 100644 --- a/InstantAPIs/JsonAPIsConfig.cs +++ b/InstantAPIs/JsonAPIsConfig.cs @@ -1,4 +1,4 @@ -using System.Linq; +using Microsoft.Extensions.Options; using System.Text.Json.Nodes; namespace InstantAPIs; @@ -6,7 +6,7 @@ namespace InstantAPIs; internal class JsonAPIsConfig { - internal HashSet Tables { get; } = new HashSet(); + internal HashSet Tables { get; } = new HashSet(); internal string JsonFilename = "mock.json"; @@ -18,7 +18,7 @@ public class JsonAPIsConfigBuilder private JsonAPIsConfig _Config = new(); private string? _FileName; - private readonly HashSet _IncludedTables = new(); + private readonly HashSet _IncludedTables = new(); private readonly List _ExcludedTables = new(); public JsonAPIsConfigBuilder SetFilename(string fileName) @@ -38,7 +38,8 @@ public JsonAPIsConfigBuilder SetFilename(string fileName) public JsonAPIsConfigBuilder IncludeEntity(string entityName, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All) { - var tableApiMapping = new InstantAPIsOptions.Table(entityName, new Uri(entityName, UriKind.Relative), typeof(JsonObject)) { ApiMethodsToGenerate = methodsToGenerate }; + var tableApiMapping = new InstantAPIsOptions.Table(entityName, new Uri(entityName, UriKind.Relative), c => c.LoadTable(entityName), + new InstantAPIsOptions.TableOptions()) { ApiMethodsToGenerate = methodsToGenerate }; _IncludedTables.Add(tableApiMapping); if (_ExcludedTables.Contains(entityName)) _ExcludedTables.Remove(tableApiMapping.Name); @@ -77,7 +78,8 @@ private void BuildTables() { var tables = IdentifyEntities() - .Select(t => new InstantAPIsOptions.Table(t, new Uri(t, UriKind.Relative), typeof(JsonObject)) + .Select(t => new InstantAPIsOptions.Table(t, new Uri(t, UriKind.Relative), c => c.LoadTable(t), + new InstantAPIsOptions.TableOptions()) { ApiMethodsToGenerate = ApiMethodsToGenerate.All }); @@ -121,4 +123,27 @@ internal JsonAPIsConfig Build() return _Config; } + public class JsonContext + { + const string JSON_FILENAME = "mock.json"; + private readonly JsonNode _writableDoc; + + public JsonContext() + { + _writableDoc = JsonNode.Parse(File.ReadAllText(JSON_FILENAME)) + ?? throw new Exception("Invalid json file"); + } + + public JsonArray LoadTable(string name) + { + return _writableDoc?.Root.AsObject().AsEnumerable().First(elem => elem.Key == name).Value as JsonArray + ?? throw new Exception("No json array"); + } + + internal void SaveChanges() + { + File.WriteAllText(JSON_FILENAME, _writableDoc.ToString()); + } + } + } \ No newline at end of file diff --git a/InstantAPIs/WebApplicationExtensions.cs b/InstantAPIs/WebApplicationExtensions.cs index be00374..4f9b6fd 100644 --- a/InstantAPIs/WebApplicationExtensions.cs +++ b/InstantAPIs/WebApplicationExtensions.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using System.Linq; using System.Reflection; namespace InstantAPIs; @@ -14,9 +15,9 @@ public static class WebApplicationExtensions internal const string LOGGER_CATEGORY_NAME = "InstantAPI"; - private static HashSet Configuration { get; set; } = new(); + private static HashSet Configuration { get; set; } = new(); - public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action>? options = null) where D : DbContext + public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action>? options = null) where TContext : DbContext { if (app is IApplicationBuilder applicationBuilder) { @@ -24,18 +25,18 @@ public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder } // Get the tables on the DbContext - var dbTables = GetDbTablesForContext(); + var dbTables = GetDbTablesForContext(); var requestedTables = !Configuration.Any() ? dbTables : Configuration.Where(t => dbTables.Any(db => db.Name != null && db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray(); - MapInstantAPIsUsingReflection(app, requestedTables); + MapInstantAPIsUsingReflection(app, requestedTables); return app; } - private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, IEnumerable requestedTables) where D : DbContext + private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, IEnumerable requestedTables) where TContext : DbContext { ILogger logger = NullLogger.Instance; @@ -54,7 +55,7 @@ private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, // The default URL for an InstantAPI is /api/TABLENAME //var url = $"/api/{table.Name}"; - initialize.MakeGenericMethod(typeof(D), table.InstanceType ?? throw new Exception($"Instance type for table {table.Name} is null")).Invoke(null, new[] { logger }); + initialize.MakeGenericMethod(typeof(TContext), table.InstanceType ?? throw new Exception($"Instance type for table {table.Name} is null")).Invoke(null, new[] { logger }); // The remaining private static methods in this class build out the Mapped API methods.. // let's use some reflection to get them @@ -66,14 +67,14 @@ private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, var methodType = (ApiMethodsToGenerate)sigAttr.Value; if ((table.ApiMethodsToGenerate & methodType) != methodType) continue; - var genericMethod = method.MakeGenericMethod(typeof(D), table.InstanceType); + var genericMethod = method.MakeGenericMethod(typeof(TContext), table.InstanceType); genericMethod.Invoke(null, new object[] { app, table.BaseUrl?.ToString() ?? throw new Exception($"BaseUrl for {table.Name} is null") }); } } } - private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action>? options, IApplicationBuilder applicationBuilder) where D : DbContext + private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action>? options, IApplicationBuilder applicationBuilder) where TContext : DbContext { // Check if AddInstantAPIs was called by getting the service options and evaluate EnableSwagger property var serviceOptions = applicationBuilder.ApplicationServices.GetRequiredService>().Value; @@ -90,8 +91,8 @@ private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action applicationBuilder.UseSwaggerUI(); } - var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService(); - var builder = new InstantAPIsBuilder(ctx); + var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService(); + var builder = new InstantAPIsBuilder(ctx); if (options != null) { options(builder); @@ -99,12 +100,13 @@ private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action } } - internal static IEnumerable GetDbTablesForContext() where D : DbContext + internal static IEnumerable GetDbTablesForContext() where TContext : DbContext { - return typeof(D).GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(x => (x.PropertyType.FullName?.StartsWith("Microsoft.EntityFrameworkCore.DbSet") ?? false) - && x.PropertyType.GenericTypeArguments.First().GetCustomAttributes(typeof(KeylessAttribute), true).Length <= 0) - .Select(x => new InstantAPIsOptions.Table(x.Name, new Uri($"/api/{x.Name}", uriKind: UriKind.RelativeOrAbsolute), x.PropertyType.GenericTypeArguments.First())) + return typeof(TContext).GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(x => (x.PropertyType.FullName?.StartsWith("Microsoft.EntityFrameworkCore.DbSet") ?? false) + && x.PropertyType.GenericTypeArguments.First().GetCustomAttributes(typeof(KeylessAttribute), true).Length <= 0) + .Select(x => InstantAPIsBuilder.CreateTable(x.Name, new Uri($"/api/{x.Name}", uriKind: UriKind.RelativeOrAbsolute), typeof(TContext), x.PropertyType, x.PropertyType.GenericTypeArguments.First())) + .Where(x => x != null).OfType() .ToArray(); } diff --git a/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs b/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs index b392e48..252e81a 100644 --- a/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs +++ b/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs @@ -1,4 +1,5 @@ -using Xunit; +using InstantAPIs; +using Xunit; namespace Test.Configuration; @@ -12,7 +13,7 @@ public void ShouldSpecifyDefaultUrl() // arrange // act - _Builder.IncludeTable(db => db.Contacts); + _Builder.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions()); var config = _Builder.Build(); // assert diff --git a/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs b/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs index 340e89b..9491a47 100644 --- a/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs +++ b/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs @@ -1,4 +1,5 @@ -using Xunit; +using InstantAPIs; +using Xunit; namespace Test.Configuration; @@ -13,7 +14,7 @@ public void ShouldSpecifyThatUrl() // act var BaseUrl = new Uri("/testapi", UriKind.Relative); - _Builder.IncludeTable(db => db.Contacts, baseUrl: BaseUrl.ToString()); + _Builder.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions(), baseUrl: BaseUrl.ToString()); var config = _Builder.Build(); // assert diff --git a/Test/Configuration/WithIncludesAndExcludes.cs b/Test/Configuration/WithIncludesAndExcludes.cs index 3c4af9e..292d47e 100644 --- a/Test/Configuration/WithIncludesAndExcludes.cs +++ b/Test/Configuration/WithIncludesAndExcludes.cs @@ -1,4 +1,5 @@ -using Xunit; +using InstantAPIs; +using Xunit; namespace Test.Configuration; @@ -13,8 +14,8 @@ public void ShouldExcludePreviouslyIncludedTable() // arrange // act - _Builder.IncludeTable(db => db.Addresses) - .IncludeTable(db => db.Contacts) + _Builder.IncludeTable(db => db.Addresses, new InstantAPIsOptions.TableOptions()) + .IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions()) .ExcludeTable(db => db.Addresses); var config = _Builder.Build(); diff --git a/Test/Configuration/WithOnlyIncludes.cs b/Test/Configuration/WithOnlyIncludes.cs index 5ab4c32..b6ca155 100644 --- a/Test/Configuration/WithOnlyIncludes.cs +++ b/Test/Configuration/WithOnlyIncludes.cs @@ -13,7 +13,7 @@ public void ShouldNotIncludeAllTables() // arrange // act - _Builder.IncludeTable(db => db.Contacts); + _Builder.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions()); var config = _Builder.Build(); // assert @@ -32,7 +32,7 @@ public void ShouldIncludeAndSetAPIMethodsToInclude(ApiMethodsToGenerate methodsT // arrange // act - _Builder.IncludeTable(db => db.Contacts, methodsToGenerate); + _Builder.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions(), methodsToGenerate); var config = _Builder.Build(); // assert diff --git a/WorkingApi/Program.cs b/WorkingApi/Program.cs index 9452016..7180330 100644 --- a/WorkingApi/Program.cs +++ b/WorkingApi/Program.cs @@ -15,7 +15,7 @@ app.MapInstantAPIs(config => { - config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.All, "addressBook"); + config.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions(), ApiMethodsToGenerate.All, "addressBook"); }); app.Run(); From c614f7a92cf132c7d60337c9f79f4a23b4577e71 Mon Sep 17 00:00:00 2001 From: Dries Verbeke Date: Fri, 22 Jul 2022 01:25:39 +0200 Subject: [PATCH 8/9] Enabling multiple data providers - added an interface with factory - included ef core provider - included json provider - update the test programs to utilize the repository approach --- InstantAPIs/InstantAPIsBuilder.cs | 150 +++++------------ InstantAPIs/InstantAPIsOptions.cs | 7 +- .../InstantAPIsServiceCollectionExtensions.cs | 69 ++++---- InstantAPIs/JsonAPIsConfig.cs | 149 ----------------- InstantAPIs/JsonApiExtensions.cs | 137 ---------------- InstantAPIs/MapApiExtensions.cs | 153 ++++++------------ InstantAPIs/Repositories/ContextHelper.cs | 24 +++ .../EntityFrameworkCore/ContextHelper.cs | 67 ++++++++ .../EntityFrameworkCore/RepositoryHelper.cs | 87 ++++++++++ .../RepositoryHelperFactory.cs | 21 +++ InstantAPIs/Repositories/IContextHelper.cs | 16 ++ InstantAPIs/Repositories/IRepositoryHelper.cs | 12 ++ .../Repositories/IRepositoryHelperFactory.cs | 21 +++ InstantAPIs/Repositories/Json/Context.cs | 34 ++++ .../Repositories/Json/ContextHelper.cs | 38 +++++ .../Repositories/Json/RepositoryHelper.cs | 72 +++++++++ .../Json/RepositoryHelperFactory.cs | 22 +++ .../Repositories/RepositoryHelperFactory.cs | 42 +++++ InstantAPIs/WebApplicationExtensions.cs | 79 ++++----- .../InstantAPIsConfigBuilderFixture.cs | 21 ++- Test/Configuration/WithoutIncludes.cs | 22 +-- Test/InstantAPIs/WebApplicationExtensions.cs | 23 ++- Test/StubData/Contact.cs | 2 +- Test/StubData/MyContext.cs | 2 +- TestJson/Program.cs | 13 +- WorkingApi/Program.cs | 9 +- 26 files changed, 674 insertions(+), 618 deletions(-) delete mode 100644 InstantAPIs/JsonAPIsConfig.cs delete mode 100644 InstantAPIs/JsonApiExtensions.cs create mode 100644 InstantAPIs/Repositories/ContextHelper.cs create mode 100644 InstantAPIs/Repositories/EntityFrameworkCore/ContextHelper.cs create mode 100644 InstantAPIs/Repositories/EntityFrameworkCore/RepositoryHelper.cs create mode 100644 InstantAPIs/Repositories/EntityFrameworkCore/RepositoryHelperFactory.cs create mode 100644 InstantAPIs/Repositories/IContextHelper.cs create mode 100644 InstantAPIs/Repositories/IRepositoryHelper.cs create mode 100644 InstantAPIs/Repositories/IRepositoryHelperFactory.cs create mode 100644 InstantAPIs/Repositories/Json/Context.cs create mode 100644 InstantAPIs/Repositories/Json/ContextHelper.cs create mode 100644 InstantAPIs/Repositories/Json/RepositoryHelper.cs create mode 100644 InstantAPIs/Repositories/Json/RepositoryHelperFactory.cs create mode 100644 InstantAPIs/Repositories/RepositoryHelperFactory.cs diff --git a/InstantAPIs/InstantAPIsBuilder.cs b/InstantAPIs/InstantAPIsBuilder.cs index c6f61db..8553cf8 100644 --- a/InstantAPIs/InstantAPIsBuilder.cs +++ b/InstantAPIs/InstantAPIsBuilder.cs @@ -1,22 +1,27 @@ -using System.Linq.Expressions; -using System.Reflection; +using InstantAPIs.Repositories; +using System.Linq.Expressions; namespace Microsoft.AspNetCore.Builder; -public class InstantAPIsBuilder - where TContext : DbContext +public class InstantAPIsBuilder + where TContext : class { + private readonly InstantAPIsOptions _instantApiOptions; + private readonly IContextHelper _contextFactory; + private readonly HashSet _tables = new HashSet(); + private readonly IList _excludedTables = new List(); - private HashSet _Config = new(); - private Type _ContextType = typeof(TContext); - private TContext _TheContext; - private readonly HashSet _IncludedTables = new(); - private readonly List _ExcludedTables = new(); - private const string DEFAULT_URI = "/api/"; + public InstantAPIsBuilder(InstantAPIsOptions instantApiOptions, IContextHelper contextFactory) + { + _instantApiOptions = instantApiOptions; + _contextFactory = contextFactory; + } - public InstantAPIsBuilder(TContext theContext) + private IEnumerable DiscoverTables() { - this._TheContext = theContext; + return _contextFactory != null + ? _contextFactory.DiscoverFromContext(_instantApiOptions.DefaultUri) + : Array.Empty(); } #region Table Inclusion/Exclusion @@ -27,14 +32,13 @@ public InstantAPIsBuilder(TContext theContext) /// Select the EntityFramework DbSet to include - Required /// A flags enumerable indicating the methods to generate. By default ALL are generated /// Configuration builder with this configuration applied - public InstantAPIsBuilder IncludeTable(Expression> setSelector, - InstantAPIsOptions.TableOptions config, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All, string baseUrl = "") - where TSet : DbSet + public InstantAPIsBuilder IncludeTable(Expression> setSelector, + InstantAPIsOptions.TableOptions config, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All, + string baseUrl = "") + where TSet : class where TEntity : class { - - var theSetType = setSelector.Compile()(_TheContext).GetType().BaseType; - var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType); + var propertyName = _contextFactory.NameTable(setSelector); if (!string.IsNullOrEmpty(baseUrl)) { @@ -50,17 +54,16 @@ public InstantAPIsBuilder IncludeTable(Expression } else { - baseUrl = string.Concat(DEFAULT_URI, property.Name); + baseUrl = string.Concat(_instantApiOptions.DefaultUri.ToString(), "/", propertyName); } - var tableApiMapping = new InstantAPIsOptions.Table(property.Name, new Uri(baseUrl, UriKind.Relative), setSelector, config) - { - ApiMethodsToGenerate = methodsToGenerate + var tableApiMapping = new InstantAPIsOptions.Table(propertyName, new Uri(baseUrl, UriKind.Relative), setSelector, config) + { + ApiMethodsToGenerate = methodsToGenerate }; - _IncludedTables.Add(tableApiMapping); - if (_ExcludedTables.Contains(tableApiMapping.Name)) _ExcludedTables.Remove(tableApiMapping.Name); - _IncludedTables.Add(tableApiMapping); + _tables.RemoveWhere(x => x.Name == tableApiMapping.Name); + _tables.Add(tableApiMapping); return this; @@ -69,106 +72,39 @@ public InstantAPIsBuilder IncludeTable(Expression /// /// Exclude individual tables from the API generation. Exclusion takes priority over inclusion /// - /// Select the entity to exclude from generation + /// Select the entity to exclude from generation /// Configuration builder with this configuraiton applied - public InstantAPIsBuilder ExcludeTable(Func> entitySelector) where T : class + public InstantAPIsBuilder ExcludeTable(Expression> setSelector) where TSet : class { - - var theSetType = entitySelector(_TheContext).GetType().BaseType; - var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType); - - if (_IncludedTables.Select(t => t.Name).Contains(property.Name)) _IncludedTables.Remove(_IncludedTables.First(t => t.Name == property.Name)); - _ExcludedTables.Add(property.Name); + var propertyName = _contextFactory.NameTable(setSelector); + _excludedTables.Add(propertyName); return this; - } private void BuildTables() { - var tables = WebApplicationExtensions.GetDbTablesForContext().ToArray(); - InstantAPIsOptions.ITable[]? outTables; - - // Add the Included tables - if (_IncludedTables.Any()) - { - outTables = tables.Where(t => _IncludedTables.Any(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase))) - .Select(t => { - var table = CreateTable(t.Name, new Uri(_IncludedTables.First(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).BaseUrl.ToString(), UriKind.Relative), typeof(TContext), typeof(DbSet<>).MakeGenericType(t.InstanceType), t.InstanceType); - if (table != null) - { - table.ApiMethodsToGenerate = _IncludedTables.First(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).ApiMethodsToGenerate; - } - return table; - }) - .Where(x => x != null).OfType() - .ToArray(); - } else { - outTables = tables - .Select(t => CreateTable(t.Name, new Uri(DEFAULT_URI + t.Name, uriKind: UriKind.Relative), typeof(TContext), typeof(DbSet<>).MakeGenericType(t.InstanceType), t.InstanceType)) - .Where(x => x != null).OfType() - .ToArray(); - } - - // Exit now if no tables were excluded - if (!_ExcludedTables.Any()) + if (!_tables.Any()) { - _Config.UnionWith(outTables); - return; + var discoveredTables = DiscoverTables(); + foreach (var discoveredTable in discoveredTables) + { + _tables.Add(discoveredTable); + } } - // Remove the Excluded tables - outTables = outTables.Where(t => !_ExcludedTables.Any(e => e.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase))).ToArray(); - - if (outTables == null || !outTables.Any()) throw new ArgumentException("All tables were excluded from this configuration"); + _tables.RemoveWhere(t => _excludedTables.Any(e => t.Name.Equals(e, StringComparison.InvariantCultureIgnoreCase))); - _Config.UnionWith(outTables); - - } - - public static InstantAPIsOptions.ITable? CreateTable(string name, Uri baseUrl, Type contextType, Type setType, Type entityType) - { - var keyProperty = entityType.GetProperties().Where(x => "id".Equals(x.Name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); - if (keyProperty == null) return null; - - var genericMethod = typeof(InstantAPIsBuilder<>).MakeGenericType(contextType).GetMethod(nameof(CreateTableGeneric), BindingFlags.NonPublic | BindingFlags.Static) - ?? throw new Exception("Missing method"); - var concreteMethod = genericMethod.MakeGenericMethod(contextType, setType, entityType, keyProperty.PropertyType); - - var entitySelector = CreateExpression(contextType, name, setType); - var keySelector = CreateExpression(entityType, keyProperty.Name, keyProperty.PropertyType); - return concreteMethod.Invoke(null, new object?[] { name, baseUrl, entitySelector, keySelector, null }) as InstantAPIsOptions.ITable; + if (!_tables.Any()) throw new ArgumentException("All tables were excluded from this configuration"); } - private static object CreateExpression(Type memberOwnerType, string property, Type returnType) - { - var parameterExpression = Expression.Parameter(memberOwnerType, "x"); - var propertyExpression = Expression.Property(parameterExpression, property); - //var block = Expression.Block(propertyExpression, returnExpression); - return Expression.Lambda(typeof(Func<,>).MakeGenericType(memberOwnerType, returnType), propertyExpression, parameterExpression); - } - - private static InstantAPIsOptions.ITable CreateTableGeneric(string name, Uri baseUrl, - Expression> entitySelector, Expression>? keySelector, Expression>? orderBy) - where TContextStatic : class - where TSet : class - where TEntity : class - { - return new InstantAPIsOptions.Table(name, baseUrl, entitySelector, - new InstantAPIsOptions.TableOptions() - { - KeySelector = keySelector, - OrderBy = orderBy - }); - } #endregion - internal HashSet Build() + internal IEnumerable Build() { - BuildTables(); - return _Config; + return _tables; } -} \ No newline at end of file +} diff --git a/InstantAPIs/InstantAPIsOptions.cs b/InstantAPIs/InstantAPIsOptions.cs index 274cd7c..e8f520c 100644 --- a/InstantAPIs/InstantAPIsOptions.cs +++ b/InstantAPIs/InstantAPIsOptions.cs @@ -12,9 +12,12 @@ public enum EnableSwagger public class InstantAPIsOptions { + public Uri DefaultUri = new Uri("/api", UriKind.Relative); - public EnableSwagger? EnableSwagger { get; set; } - public Action? Swagger { get; set; } + public EnableSwagger? EnableSwagger { get; set; } + public Action? Swagger { get; set; } + + public IEnumerable Tables { get; internal set; } = new HashSet(); internal class Table : ITable diff --git a/InstantAPIs/InstantAPIsServiceCollectionExtensions.cs b/InstantAPIs/InstantAPIsServiceCollectionExtensions.cs index b32c31c..ecd87ef 100644 --- a/InstantAPIs/InstantAPIsServiceCollectionExtensions.cs +++ b/InstantAPIs/InstantAPIsServiceCollectionExtensions.cs @@ -1,34 +1,45 @@ -using Microsoft.Extensions.DependencyInjection; +using InstantAPIs.Repositories; -namespace InstantAPIs; +namespace Microsoft.Extensions.DependencyInjection; public static class InstantAPIsServiceCollectionExtensions { - public static IServiceCollection AddInstantAPIs(this IServiceCollection services, Action? setupAction = null) - { - var options = new InstantAPIsOptions(); - - // Get the service options - setupAction?.Invoke(options); - - if (options.EnableSwagger == null) - { - options.EnableSwagger = EnableSwagger.DevelopmentOnly; - } - - // Add and configure Swagger services if it is enabled - if (options.EnableSwagger != EnableSwagger.None) - { - services.AddEndpointsApiExplorer(); - services.AddSwaggerGen(options.Swagger); - } - - // Register the required options so that it can be accessed by InstantAPIs middleware - services.Configure(config => - { - config.EnableSwagger = options.EnableSwagger; - }); - - return services; - } + public static IServiceCollection AddInstantAPIs(this IServiceCollection services, Action? setupAction = null) + { + var options = new InstantAPIsOptions(); + + // Get the service options + setupAction?.Invoke(options); + + if (options.EnableSwagger == null) + { + options.EnableSwagger = EnableSwagger.DevelopmentOnly; + } + + // Add and configure Swagger services if it is enabled + if (options.EnableSwagger != EnableSwagger.None) + { + services.AddEndpointsApiExplorer(); + services.AddSwaggerGen(options.Swagger); + } + + // Register the required options so that it can be accessed by InstantAPIs middleware + services.Configure(config => + { + config.EnableSwagger = options.EnableSwagger; + }); + + services.AddSingleton(typeof(IRepositoryHelperFactory<,,,>), typeof(RepositoryHelperFactory<,,,>)); + services.AddSingleton(typeof(IContextHelper<>), typeof(ContextHelper<>)); + + // ef core specific + services.AddSingleton(); + services.AddSingleton(); + + // json specific + services.AddSingleton(); + services.AddSingleton(); + + return services; + } } \ No newline at end of file diff --git a/InstantAPIs/JsonAPIsConfig.cs b/InstantAPIs/JsonAPIsConfig.cs deleted file mode 100644 index 5f1abf2..0000000 --- a/InstantAPIs/JsonAPIsConfig.cs +++ /dev/null @@ -1,149 +0,0 @@ -using Microsoft.Extensions.Options; -using System.Text.Json.Nodes; - -namespace InstantAPIs; - -internal class JsonAPIsConfig -{ - - internal HashSet Tables { get; } = new HashSet(); - - internal string JsonFilename = "mock.json"; - -} - - -public class JsonAPIsConfigBuilder -{ - - private JsonAPIsConfig _Config = new(); - private string? _FileName; - private readonly HashSet _IncludedTables = new(); - private readonly List _ExcludedTables = new(); - - public JsonAPIsConfigBuilder SetFilename(string fileName) - { - _FileName = fileName; - return this; - } - - #region Table Inclusion/Exclusion - - /// - /// Specify individual entities to include in the API generation with the methods requested - /// - /// Name of the JSON entity collection to include - /// A flags enumerable indicating the methods to generate. By default ALL are generated - /// Configuration builder with this configuration applied - public JsonAPIsConfigBuilder IncludeEntity(string entityName, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All) - { - - var tableApiMapping = new InstantAPIsOptions.Table(entityName, new Uri(entityName, UriKind.Relative), c => c.LoadTable(entityName), - new InstantAPIsOptions.TableOptions()) { ApiMethodsToGenerate = methodsToGenerate }; - _IncludedTables.Add(tableApiMapping); - - if (_ExcludedTables.Contains(entityName)) _ExcludedTables.Remove(tableApiMapping.Name); - - return this; - - } - - /// - /// Exclude individual entities from the API generation. Exclusion takes priority over inclusion - /// - /// Name of the JSON entity collection to exclude - /// Configuration builder with this configuraiton applied - public JsonAPIsConfigBuilder ExcludeTable(string entityName) - { - - if (_IncludedTables.Select(t => t.Name).Contains(entityName)) _IncludedTables.Remove(_IncludedTables.First(t => t.Name == entityName)); - _ExcludedTables.Add(entityName); - - return this; - - } - - private HashSet IdentifyEntities() - { - var writableDoc = JsonNode.Parse(File.ReadAllText(_Config.JsonFilename)); - - // print API - return writableDoc?.Root.AsObject() - .AsEnumerable().Select(x => x.Key) - .ToHashSet() ?? new HashSet(); - - } - - private void BuildTables() - { - - var tables = IdentifyEntities() - .Select(t => new InstantAPIsOptions.Table(t, new Uri(t, UriKind.Relative), c => c.LoadTable(t), - new InstantAPIsOptions.TableOptions()) - { - ApiMethodsToGenerate = ApiMethodsToGenerate.All - }); - - if (!_IncludedTables.Any() && !_ExcludedTables.Any()) - { - _Config.Tables.UnionWith(tables); - return; - } - - // Add the Included tables - var outTables = _IncludedTables - .ToArray(); - - // If no tables were added, added them all - if (outTables.Length == 0) - { - outTables = tables.ToArray(); - } - - // Remove the Excluded tables - outTables = outTables.Where(t => !_ExcludedTables.Any(e => e.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase))).ToArray(); - - if (outTables == null || !outTables.Any()) throw new ArgumentException("All tables were excluded from this configuration"); - - _Config.Tables.UnionWith(outTables); - - } - -#endregion - - internal JsonAPIsConfig Build() - { - - if (string.IsNullOrEmpty(_FileName)) throw new ArgumentNullException("Missing Json Filename for configuration"); - if (!File.Exists(_FileName)) throw new ArgumentException($"Unable to locate the JSON file for APIs at {_FileName}"); - _Config.JsonFilename = _FileName; - - BuildTables(); - - return _Config; - } - - public class JsonContext - { - const string JSON_FILENAME = "mock.json"; - private readonly JsonNode _writableDoc; - - public JsonContext() - { - _writableDoc = JsonNode.Parse(File.ReadAllText(JSON_FILENAME)) - ?? throw new Exception("Invalid json file"); - } - - public JsonArray LoadTable(string name) - { - return _writableDoc?.Root.AsObject().AsEnumerable().First(elem => elem.Key == name).Value as JsonArray - ?? throw new Exception("No json array"); - } - - internal void SaveChanges() - { - File.WriteAllText(JSON_FILENAME, _writableDoc.ToString()); - } - } - -} \ No newline at end of file diff --git a/InstantAPIs/JsonApiExtensions.cs b/InstantAPIs/JsonApiExtensions.cs deleted file mode 100644 index 4d894ae..0000000 --- a/InstantAPIs/JsonApiExtensions.cs +++ /dev/null @@ -1,137 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using System.Text.Json.Nodes; - -namespace InstantAPIs; - -public static class JsonApiExtensions -{ - - static JsonAPIsConfig _Config = new JsonAPIsConfig(); - - public static WebApplication UseJsonRoutes(this WebApplication app, Action? options = null) - { - - var builder = new JsonAPIsConfigBuilder(); - if (options != null) - { - options(builder); - _Config = builder.Build(); - } - - var writableDoc = JsonNode.Parse(File.ReadAllText(_Config.JsonFilename)) - ?? throw new Exception("Missing json file"); - var sets = writableDoc.Root?.AsObject()?.AsEnumerable(); - if (sets == null || !sets.Any()) return app; - - // print API - foreach (var elem in sets) - { - - var thisEntity = _Config.Tables.FirstOrDefault(t => elem.Key.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)); - if (thisEntity == null) continue; - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Get) == ApiMethodsToGenerate.Get) - Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower())); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.GetById) == ApiMethodsToGenerate.GetById) - Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower()) + "/id"); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Insert) == ApiMethodsToGenerate.Insert) - Console.WriteLine(string.Format("POST /{0}", elem.Key.ToLower())); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Update) == ApiMethodsToGenerate.Update) - Console.WriteLine(string.Format("PUT /{0}", elem.Key.ToLower())); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Delete) == ApiMethodsToGenerate.Delete) - Console.WriteLine(string.Format("DELETE /{0}", elem.Key.ToLower()) + "/id"); - - Console.WriteLine(" "); - } - - // setup routes - foreach (var elem in sets) - { - - var thisEntity = _Config.Tables.FirstOrDefault(t => elem.Key.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)); - if (thisEntity == null) continue; - - var arr = elem.Value?.AsArray() ?? new JsonArray(); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Get) == ApiMethodsToGenerate.Get) - app.MapGet(string.Format("/{0}", elem.Key), () => elem.Value?.ToString()); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.GetById) == ApiMethodsToGenerate.GetById) - app.MapGet(string.Format("/{0}", elem.Key) + "/{id}", (int id) => - { - var matchedItem = arr == null ? null : - arr.SingleOrDefault(row => row != null && row - .AsObject() - .Any(o => o.Key.ToLower() == "id" && o.Value != null && int.Parse(o.Value.ToString()) == id) - ); - return matchedItem == null ? Results.NotFound() : Results.Ok(matchedItem); - }); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Insert) == ApiMethodsToGenerate.Insert) - app.MapPost(string.Format("/{0}", elem.Key), async (HttpRequest request) => - { - string content = string.Empty; - using (StreamReader reader = new StreamReader(request.Body)) - { - content = await reader.ReadToEndAsync(); - } - var newNode = JsonNode.Parse(content); - var array = elem.Value?.AsArray(); - if (newNode == null || array == null) return Results.NotFound(); - newNode.AsObject().Add("Id", array.Count() + 1); - array.Add(newNode); - - File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); - return Results.Ok(content); - }); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Update) == ApiMethodsToGenerate.Update) - app.MapPut(string.Format("/{0}", elem.Key), async (HttpRequest request) => - { - string content = string.Empty; - using (StreamReader reader = new StreamReader(request.Body)) - { - content = await reader.ReadToEndAsync(); - } - var newNode = JsonNode.Parse(content); - var array = elem.Value?.AsArray(); - if (array != null) - { - array.Add(newNode); - - File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); - } - - return "OK"; - }); - - if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Delete) == ApiMethodsToGenerate.Delete) - app.MapDelete(string.Format("/{0}", elem.Key) + "/{id}", (int id) => - { - - var matchedItem = arr - .Select((value, index) => new { value, index }) - .SingleOrDefault(row => row.value - ?.AsObject() - ?.Any(o => o.Key.ToLower() == "id" && o.Value != null && int.Parse(o.Value.ToString()) == id) - ?? false - ); - if (matchedItem != null) - { - arr.RemoveAt(matchedItem.index); - File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); - } - - return "OK"; - }); - - }; - - return app; - } -} \ No newline at end of file diff --git a/InstantAPIs/MapApiExtensions.cs b/InstantAPIs/MapApiExtensions.cs index abb43d0..468687f 100644 --- a/InstantAPIs/MapApiExtensions.cs +++ b/InstantAPIs/MapApiExtensions.cs @@ -2,154 +2,91 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Routing; using Microsoft.AspNetCore.Http; -using System.ComponentModel.DataAnnotations; -using System.Reflection; using Microsoft.Extensions.Logging; +using InstantAPIs.Repositories; using Microsoft.Extensions.Logging.Abstractions; namespace InstantAPIs; -internal class MapApiExtensions +internal partial class MapApiExtensions { - + public static ILogger Logger = NullLogger.Instance; // TODO: Authentication / Authorization - private static Dictionary _IdLookup = new(); - - private static ILogger Logger = NullLogger.Instance; - - internal static void Initialize(ILogger logger) - where D: DbContext - where C: class - { - - Logger = logger; - - var theType = typeof(C); - var idProp = theType.GetProperty("id", BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? theType.GetProperties().FirstOrDefault(p => p.CustomAttributes.Any(a => a.AttributeType == typeof(KeyAttribute))); - - if (idProp != null) - { - _IdLookup.Add(theType, idProp); - } - - } [ApiMethod(ApiMethodsToGenerate.Get)] - internal static void MapInstantGetAll(IEndpointRouteBuilder app, string url) - where D : DbContext where C : class + internal static void MapInstantGetAll(IEndpointRouteBuilder app, string url, string name) + where TContext : class + where TSet : class + where TEntity : class { - - Logger.LogInformation($"Created API: HTTP GET\t{url}"); - app.MapGet(url, ([FromServices] D db) => + app.MapGet(url, async (HttpRequest request, [FromServices] TContext context, [FromServices] IRepositoryHelperFactory repository, + CancellationToken cancellationToken) => { - return Results.Ok(db.Set()); + return Results.Ok(await repository.Get(request, context, name, cancellationToken)); }); - + Logger.LogInformation($"Created API: HTTP GET\t{url}"); } [ApiMethod(ApiMethodsToGenerate.GetById)] - internal static void MapGetById(IEndpointRouteBuilder app, string url) - where D: DbContext where C : class + internal static void MapGetById(IEndpointRouteBuilder app, string url, string name) + where TContext : class + where TSet : class + where TEntity : class { - - // identify the ID field - var theType = typeof(C); - var idProp = _IdLookup[theType]; - - if (idProp == null) return; - - Logger.LogInformation($"Created API: HTTP GET\t{url}/{{id}}"); - - app.MapGet($"{url}/{{id}}", async ([FromServices] D db, [FromRoute] string id) => + app.MapGet($"{url}/{{id}}", async (HttpRequest request, [FromServices] TContext context, [FromRoute] TKey id, + [FromServices] IRepositoryHelperFactory repository, CancellationToken cancellationToken) => { - - var outValue = default(C); - if (idProp.PropertyType == typeof(Guid)) - outValue = await db.Set().FindAsync(Guid.Parse(id)); - else if (idProp.PropertyType == typeof(int)) - outValue = await db.Set().FindAsync(int.Parse(id)); - else if (idProp.PropertyType == typeof(long)) - outValue = await db.Set().FindAsync(long.Parse(id)); - else //if (idProp.PropertyType == typeof(string)) - outValue = await db.Set().FindAsync(id); - + var outValue = await repository.GetById(request, context, name, id, cancellationToken); if (outValue is null) return Results.NotFound(); return Results.Ok(outValue); }); - - + Logger.LogInformation($"Created API: HTTP GET\t{url}/{{id}}"); } [ApiMethod(ApiMethodsToGenerate.Insert)] - internal static void MapInstantPost(IEndpointRouteBuilder app, string url) - where D : DbContext where C : class + internal static void MapInstantPost(IEndpointRouteBuilder app, string url, string name) + where TContext : class + where TSet : class + where TEntity : class { - - Logger.LogInformation($"Created API: HTTP POST\t{url}"); - - app.MapPost(url, async ([FromServices] D db, [FromBody] C newObj) => + app.MapPost(url, async (HttpRequest request, [FromServices] TContext context, [FromBody] TEntity newObj, + [FromServices] IRepositoryHelperFactory repository, CancellationToken cancellationToken) => { - db.Add(newObj); - await db.SaveChangesAsync(); - var id = _IdLookup[typeof(C)].GetValue(newObj); + var id = await repository.Insert(request, context, name, newObj, cancellationToken); return Results.Created($"{url}/{id}", newObj); }); - + Logger.LogInformation($"Created API: HTTP POST\t{url}"); } [ApiMethod(ApiMethodsToGenerate.Update)] - internal static void MapInstantPut(IEndpointRouteBuilder app, string url) - where D : DbContext where C : class + internal static void MapInstantPut(IEndpointRouteBuilder app, string url, string name) + where TContext : class + where TSet : class + where TEntity : class { - - Logger.LogInformation($"Created API: HTTP PUT\t{url}"); - - app.MapPut($"{url}/{{id}}", async ([FromServices] D db, [FromRoute] string id, [FromBody] C newObj) => + app.MapPut($"{url}/{{id}}", async (HttpRequest request, [FromServices] TContext context, [FromRoute] TKey id, [FromBody] TEntity newObj, + [FromServices] IRepositoryHelperFactory repository, CancellationToken cancellationToken) => { - db.Set().Attach(newObj); - db.Entry(newObj).State = EntityState.Modified; - await db.SaveChangesAsync(); + await repository.Update(request, context, name, id, newObj, cancellationToken); return Results.NoContent(); }); - + Logger.LogInformation($"Created API: HTTP PUT\t{url}"); } [ApiMethod(ApiMethodsToGenerate.Delete)] - internal static void MapDeleteById(IEndpointRouteBuilder app, string url) - where D : DbContext where C : class + internal static void MapDeleteById(IEndpointRouteBuilder app, string url, string name) + where TContext : class + where TSet : class + where TEntity : class { - - // identify the ID field - var theType = typeof(C); - var idProp = _IdLookup[theType]; - - if (idProp == null) return; - Logger.LogInformation($"Created API: HTTP DELETE\t{url}"); - - app.MapDelete($"{url}/{{id}}", async ([FromServices] D db, [FromRoute] string id) => + app.MapDelete($"{url}/{{id}}", async (HttpRequest request, [FromServices] TContext context, [FromRoute] TKey id, + [FromServices] IRepositoryHelperFactory repository, CancellationToken cancellationToken) => { - - var set = db.Set(); - C? obj; - - if (idProp.PropertyType == typeof(Guid)) - obj = await set.FindAsync(Guid.Parse(id)); - else if (idProp.PropertyType == typeof(int)) - obj = await set.FindAsync(int.Parse(id)); - else if (idProp.PropertyType == typeof(long)) - obj = await set.FindAsync(long.Parse(id)); - else //if (idProp.PropertyType == typeof(string)) - obj = await set.FindAsync(id); - - if (obj == null) return Results.NotFound(); - - db.Set().Remove(obj); - await db.SaveChangesAsync(); - return Results.NoContent(); - + return await repository.Delete(request, context, name, id, cancellationToken) + ? Results.NoContent() + : Results.NotFound(); }); - - + Logger.LogInformation($"Created API: HTTP DELETE\t{url}"); } } diff --git a/InstantAPIs/Repositories/ContextHelper.cs b/InstantAPIs/Repositories/ContextHelper.cs new file mode 100644 index 0000000..8013f7f --- /dev/null +++ b/InstantAPIs/Repositories/ContextHelper.cs @@ -0,0 +1,24 @@ +using System.Linq.Expressions; + +namespace InstantAPIs.Repositories; + +internal class ContextHelper + : IContextHelper + where TContext : class +{ + private readonly IContextHelper _context; + + public ContextHelper(IEnumerable contexts) + { + // need to inject the configuration with the list of table mappings as reference to read out the + var contextType = typeof(TContext); + _context = contexts + .First(x => x.IsValidFor(contextType)); + } + + public IEnumerable DiscoverFromContext(Uri baseUrl) + => _context.DiscoverFromContext(baseUrl); + + public string NameTable(Expression> setSelector) + => _context.NameTable(setSelector); +} diff --git a/InstantAPIs/Repositories/EntityFrameworkCore/ContextHelper.cs b/InstantAPIs/Repositories/EntityFrameworkCore/ContextHelper.cs new file mode 100644 index 0000000..bac7058 --- /dev/null +++ b/InstantAPIs/Repositories/EntityFrameworkCore/ContextHelper.cs @@ -0,0 +1,67 @@ +using System.Linq.Expressions; +using System.Reflection; + +namespace InstantAPIs.Repositories.EntityFrameworkCore; + +public class ContextHelper : + IContextHelper +{ + + public bool IsValidFor(Type contextType) => + contextType.IsAssignableTo(typeof(DbContext)); + + public IEnumerable DiscoverFromContext(Uri baseUrl) + { + var dbSet = typeof(DbSet<>); + return typeof(TContext) + .GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(x => (x.PropertyType.FullName?.StartsWith("Microsoft.EntityFrameworkCore.DbSet") ?? false) + && x.PropertyType.GenericTypeArguments.First().GetCustomAttributes(typeof(KeylessAttribute), true).Length <= 0) + .Select(x => CreateTable(x.Name, new Uri($"{baseUrl.OriginalString}/{x.Name}", UriKind.Relative), typeof(TContext), x.PropertyType, x.PropertyType.GenericTypeArguments.First())) + .Where(x => x != null).OfType(); + } + + private static InstantAPIsOptions.ITable? CreateTable(string name, Uri baseUrl, Type contextType, Type setType, Type entityType) + { + var keyProperty = entityType.GetProperties().Where(x => "id".Equals(x.Name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault(); + if (keyProperty == null) return null; + + var genericMethod = typeof(ContextHelper).GetMethod(nameof(CreateTableGeneric), BindingFlags.NonPublic | BindingFlags.Static) + ?? throw new Exception("Missing method"); + var concreteMethod = genericMethod.MakeGenericMethod(contextType, setType, entityType, keyProperty.PropertyType); + + var entitySelector = CreateExpression(contextType, name, setType); + var keySelector = CreateExpression(entityType, keyProperty.Name, keyProperty.PropertyType); + return concreteMethod.Invoke(null, new object?[] { name, baseUrl, entitySelector, keySelector, null }) as InstantAPIsOptions.ITable; + } + + private static object CreateExpression(Type memberOwnerType, string property, Type returnType) + { + var parameterExpression = Expression.Parameter(memberOwnerType, "x"); + var propertyExpression = Expression.Property(parameterExpression, property); + //var block = Expression.Block(propertyExpression, returnExpression); + return Expression.Lambda(typeof(Func<,>).MakeGenericType(memberOwnerType, returnType), propertyExpression, parameterExpression); + } + + private static InstantAPIsOptions.ITable CreateTableGeneric(string name, Uri baseUrl, + Expression> entitySelector, Expression>? keySelector, Expression>? orderBy) + where TContext : class + where TSet : class + where TEntity : class + { + return new InstantAPIsOptions.Table(name, baseUrl, entitySelector, + new InstantAPIsOptions.TableOptions() + { + KeySelector = keySelector, + OrderBy = orderBy + }); + } + + public string NameTable(Expression> setSelector) + { + return setSelector.Body.NodeType == ExpressionType.MemberAccess + && setSelector.Body is MemberExpression memberExpression + ? memberExpression.Member.Name + : throw new ArgumentException(nameof(setSelector.Body.DebugInfo), "Not a valid expression"); + } +} diff --git a/InstantAPIs/Repositories/EntityFrameworkCore/RepositoryHelper.cs b/InstantAPIs/Repositories/EntityFrameworkCore/RepositoryHelper.cs new file mode 100644 index 0000000..dff18fb --- /dev/null +++ b/InstantAPIs/Repositories/EntityFrameworkCore/RepositoryHelper.cs @@ -0,0 +1,87 @@ +using Microsoft.AspNetCore.Http; +using System.Linq.Expressions; + +namespace InstantAPIs.Repositories.EntityFrameworkCore; + +public class RepositoryHelper : + IRepositoryHelper + where TContext : DbContext + where TSet : DbSet + where TEntity : class +{ + private readonly Func _setSelector; + private readonly InstantAPIsOptions.TableOptions _config; + private readonly Func _keySelector; + private readonly string _keyName; + + /// + /// This constructor is called using reflection in order to have meaningfull context and set generic types + /// + /// + public RepositoryHelper(Func setSelector, InstantAPIsOptions.TableOptions config) + { + _setSelector = setSelector; + _config = config; + + // create predicate based on the key selector? + _keySelector = config.KeySelector?.Compile() ?? throw new Exception("Key selector required"); + // if no keyselector is found we need to find it? Or do we fall back to "id"? + _keyName = config.KeySelector.Body.NodeType == ExpressionType.MemberAccess + && config.KeySelector.Body is MemberExpression memberExpression + ? memberExpression.Member.Name + : throw new ArgumentException(nameof(config.KeySelector.Body.DebugInfo), "Not a valid expression"); + } + + private Expression> CreatePredicate(TKey key) + { + var parameterExpression = Expression.Parameter(typeof(TEntity), "x"); + var propertyExpression = Expression.Property(parameterExpression, _keyName); + var keyValueExpression = Expression.Constant(key); + return Expression.Lambda>(Expression.Equal(propertyExpression, keyValueExpression), parameterExpression); + } + + private TSet SelectSet(TContext context) + => _setSelector(context) ?? throw new ArgumentNullException("Empty set"); + + public bool IsValidFor(Type type) => type.IsAssignableFrom(typeof(DbSet<>)); + + public async Task> Get(HttpRequest request, TContext context, string name, CancellationToken cancellationToken) + { + var set = SelectSet(context); + return await set.ToListAsync(cancellationToken); + } + + public async Task GetById(HttpRequest request, TContext context, string name, TKey id, CancellationToken cancellationToken) + { + var set = SelectSet(context); + return await set.FirstOrDefaultAsync(CreatePredicate(id), cancellationToken); + } + + public async Task Insert(HttpRequest request, TContext context, string name, TEntity newObj, CancellationToken cancellationToken) + { + var set = SelectSet(context); + await set.AddAsync(newObj, cancellationToken); + await context.SaveChangesAsync(cancellationToken); + return _keySelector(newObj); + } + + public async Task Update(HttpRequest request, TContext context, string name, TKey id, TEntity newObj, CancellationToken cancellationToken) + { + var set = SelectSet(context); + var entity = set.Attach(newObj); + entity.State = EntityState.Modified; + await context.SaveChangesAsync(cancellationToken); + } + + public async Task Delete(HttpRequest request, TContext context, string name, TKey id, CancellationToken cancellationToken) + { + var set = SelectSet(context); + var entity = await set.FirstOrDefaultAsync(CreatePredicate(id), cancellationToken); + + if (entity == null) return false; + + set.Remove(entity); + await context.SaveChangesAsync(cancellationToken); + return true; + } +} diff --git a/InstantAPIs/Repositories/EntityFrameworkCore/RepositoryHelperFactory.cs b/InstantAPIs/Repositories/EntityFrameworkCore/RepositoryHelperFactory.cs new file mode 100644 index 0000000..b551ea0 --- /dev/null +++ b/InstantAPIs/Repositories/EntityFrameworkCore/RepositoryHelperFactory.cs @@ -0,0 +1,21 @@ +namespace InstantAPIs.Repositories.EntityFrameworkCore; + +public class RepositoryHelperFactory : + IRepositoryHelperFactory +{ + public bool IsValidFor(Type contextType, Type setType) => + contextType.IsAssignableTo(typeof(DbContext)) + && setType.IsGenericType && setType.GetGenericTypeDefinition().Equals(typeof(DbSet<>)); + + public IRepositoryHelper Create( + Func setSelector, InstantAPIsOptions.TableOptions config) + { + if (!typeof(TContext).IsAssignableTo(typeof(DbContext))) throw new ArgumentException("Context needs to derive from DbContext"); + + var newRepositoryType = typeof(RepositoryHelper<,,,>).MakeGenericType(typeof(TContext), typeof(TSet), typeof(TEntity), typeof(TKey)); + var returnValue = Activator.CreateInstance(newRepositoryType, setSelector, config) + ?? throw new Exception("Could not create an instance of the EFCoreRepository implementation"); + + return (IRepositoryHelper)returnValue; + } +} diff --git a/InstantAPIs/Repositories/IContextHelper.cs b/InstantAPIs/Repositories/IContextHelper.cs new file mode 100644 index 0000000..9af85c2 --- /dev/null +++ b/InstantAPIs/Repositories/IContextHelper.cs @@ -0,0 +1,16 @@ +using System.Linq.Expressions; + +namespace InstantAPIs.Repositories; + +public interface IContextHelper +{ + IEnumerable DiscoverFromContext(Uri baseUrl); + string NameTable(Expression> setSelector); +} + +public interface IContextHelper +{ + bool IsValidFor(Type contextType); + IEnumerable DiscoverFromContext(Uri baseUrl); + string NameTable(Expression> setSelector); +} \ No newline at end of file diff --git a/InstantAPIs/Repositories/IRepositoryHelper.cs b/InstantAPIs/Repositories/IRepositoryHelper.cs new file mode 100644 index 0000000..f47f725 --- /dev/null +++ b/InstantAPIs/Repositories/IRepositoryHelper.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Http; + +namespace InstantAPIs.Repositories; + +public interface IRepositoryHelper +{ + Task> Get(HttpRequest request, TContext context, string name, CancellationToken cancellationToken); + Task GetById(HttpRequest request, TContext context, string name, TKey id, CancellationToken cancellationToken); + Task Insert(HttpRequest request, TContext context, string name, TEntity newObj, CancellationToken cancellationToken); + Task Update(HttpRequest request, TContext context, string name, TKey id, TEntity newObj, CancellationToken cancellationToken); + Task Delete(HttpRequest request, TContext context, string name, TKey id, CancellationToken cancellationToken); +} diff --git a/InstantAPIs/Repositories/IRepositoryHelperFactory.cs b/InstantAPIs/Repositories/IRepositoryHelperFactory.cs new file mode 100644 index 0000000..cd34708 --- /dev/null +++ b/InstantAPIs/Repositories/IRepositoryHelperFactory.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; + +namespace InstantAPIs.Repositories; + +public interface IRepositoryHelperFactory + where TContext : class + where TSet : class + where TEntity : class +{ + public Task> Get(HttpRequest request, TContext context, string name, CancellationToken cancellationToken); + public Task GetById(HttpRequest request, TContext context, string name, TKey id, CancellationToken cancellationToken); + Task Insert(HttpRequest request, TContext context, string name, TEntity newObj, CancellationToken cancellationToken); + Task Update(HttpRequest request, TContext context, string name, TKey id, TEntity newObj, CancellationToken cancellationToken); + Task Delete(HttpRequest request, TContext context, string name, TKey id, CancellationToken cancellationToken); +} + +public interface IRepositoryHelperFactory +{ + bool IsValidFor(Type contextType, Type setType); + IRepositoryHelper Create(Func setSelector, InstantAPIsOptions.TableOptions config); +} diff --git a/InstantAPIs/Repositories/Json/Context.cs b/InstantAPIs/Repositories/Json/Context.cs new file mode 100644 index 0000000..cb5bd4d --- /dev/null +++ b/InstantAPIs/Repositories/Json/Context.cs @@ -0,0 +1,34 @@ +using Microsoft.Extensions.Options; +using System.Text.Json.Nodes; + +namespace InstantAPIs.Repositories.Json; + +public class Context +{ + private readonly Options _options; + private readonly JsonNode _writableDoc; + + public Context(IOptions options) + { + _options = options.Value; + _writableDoc = JsonNode.Parse(File.ReadAllText(_options.JsonFilename)) + ?? throw new Exception("Invalid json content"); + } + + public JsonArray LoadTable(string name) + { + return _writableDoc?.Root.AsObject().AsEnumerable().First(elem => elem.Key == name).Value as JsonArray + ?? throw new Exception("Not a json array"); + } + + internal void SaveChanges() + { + File.WriteAllText(_options.JsonFilename, _writableDoc.ToString()); + } + + public class Options + { + public string JsonFilename { get; set; } = "mock.json"; + } +} + diff --git a/InstantAPIs/Repositories/Json/ContextHelper.cs b/InstantAPIs/Repositories/Json/ContextHelper.cs new file mode 100644 index 0000000..bbb37ba --- /dev/null +++ b/InstantAPIs/Repositories/Json/ContextHelper.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Options; +using System.Linq.Expressions; +using System.Text.Json.Nodes; + +namespace InstantAPIs.Repositories.Json; + +public class ContextHelper : + IContextHelper +{ + private readonly IOptions _options; + + public ContextHelper(IOptions options) + { + _options = options; + } + + public bool IsValidFor(Type contextType) => contextType.IsAssignableTo(typeof(Context)); + + public IEnumerable DiscoverFromContext(Uri baseUrl) + { + var doc = JsonNode.Parse(File.ReadAllText(_options.Value.JsonFilename)); + var tables = doc?.Root.AsObject().AsEnumerable() ?? throw new Exception("No json file found"); + return tables.Select(x => new InstantAPIsOptions.Table( + x.Key, new Uri($"{baseUrl.OriginalString}/{x.Key}", UriKind.Relative), c => c.LoadTable(x.Key), + new InstantAPIsOptions.TableOptions())); + } + + public string NameTable(Expression> setSelector) + { + return setSelector.Body.NodeType == ExpressionType.Call + && setSelector.Body is MethodCallExpression methodExpression + && methodExpression.Arguments.Count == 1 + && methodExpression.Arguments.First() is ConstantExpression constantExpression + && constantExpression.Value != null + ? (constantExpression.Value.ToString() ?? string.Empty) + : throw new ArgumentException(nameof(setSelector.Body.DebugInfo), "Not a valid expression"); + } +} diff --git a/InstantAPIs/Repositories/Json/RepositoryHelper.cs b/InstantAPIs/Repositories/Json/RepositoryHelper.cs new file mode 100644 index 0000000..79f2432 --- /dev/null +++ b/InstantAPIs/Repositories/Json/RepositoryHelper.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Http; +using System.Net.Http.Json; +using System.Text.Json.Nodes; + +namespace InstantAPIs.Repositories.Json; + +internal class JsonRepositoryHelper : + IRepositoryHelper +{ + private readonly Func _setSelector; + private readonly InstantAPIsOptions.TableOptions _config; + + public JsonRepositoryHelper(Func setSelector, InstantAPIsOptions.TableOptions config) + { + _setSelector = setSelector; + _config = config; + } + + public Task> Get(HttpRequest request, Context context, string name, CancellationToken cancellationToken) + { + return Task.FromResult(_setSelector(context).OfType()); + } + + public Task GetById(HttpRequest request, Context context, string name, int id, CancellationToken cancellationToken) + { + var array = context.LoadTable(name); + var matchedItem = array.SingleOrDefault(row => (row ?? throw new Exception("No row found")) + .AsObject() + .Any(o => o.Key.ToLower() == "id" && o.Value?.ToString() == id.ToString()) + )?.AsObject(); + return Task.FromResult(matchedItem); + } + + + public Task Insert(HttpRequest request, Context context, string name, JsonObject newObj, CancellationToken cancellationToken) + { + + var array = context.LoadTable(name); + var key = array.Count + 1; + newObj.AsObject().Add("Id", key.ToString()); + array.Add(newObj); + context.SaveChanges(); + + return Task.FromResult(key); + } + + public Task Update(HttpRequest request, Context context, string name, int id, JsonObject newObj, CancellationToken cancellationToken) + { + var array = context.LoadTable(name); + array.Add(newObj); + context.SaveChanges(); + + return Task.CompletedTask; + } + + public Task Delete(HttpRequest request, Context context, string name, int id, CancellationToken cancellationToken) + { + var array = context.LoadTable(name); + var matchedItem = array + .Select((value, index) => new { value, index }) + .SingleOrDefault(row => (row.value ?? throw new Exception("No json value found")) + .AsObject() + .Any(o => o.Key.ToLower() == "id" && o.Value?.ToString() == id.ToString())); + if (matchedItem != null) + { + array.RemoveAt(matchedItem.index); + context.SaveChanges(); + } + + return Task.FromResult(true); + } +} diff --git a/InstantAPIs/Repositories/Json/RepositoryHelperFactory.cs b/InstantAPIs/Repositories/Json/RepositoryHelperFactory.cs new file mode 100644 index 0000000..58209bc --- /dev/null +++ b/InstantAPIs/Repositories/Json/RepositoryHelperFactory.cs @@ -0,0 +1,22 @@ +using System.Net.Http.Json; +using System.Text.Json.Nodes; + +namespace InstantAPIs.Repositories.Json; + +public class RepositoryHelperFactory : + IRepositoryHelperFactory +{ + public bool IsValidFor(Type contextType, Type setType) => + contextType.IsAssignableTo(typeof(Context)) && setType.Equals(typeof(JsonArray)); + + public IRepositoryHelper Create(Func setSelector, InstantAPIsOptions.TableOptions config) + { + if (!typeof(TContext).IsAssignableTo(typeof(Context))) throw new ArgumentException("Context needs to derive from JsonContext"); + + var newRepositoryType = typeof(JsonRepositoryHelper); + var returnValue = Activator.CreateInstance(newRepositoryType, setSelector, config) + ?? throw new Exception("Could not create an instance of the JsonRepository implementation"); + + return (IRepositoryHelper)returnValue; + } +} diff --git a/InstantAPIs/Repositories/RepositoryHelperFactory.cs b/InstantAPIs/Repositories/RepositoryHelperFactory.cs new file mode 100644 index 0000000..3ae963d --- /dev/null +++ b/InstantAPIs/Repositories/RepositoryHelperFactory.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace InstantAPIs.Repositories; + +internal class RepositoryHelperFactory + : IRepositoryHelperFactory + where TContext : class + where TSet : class + where TEntity : class +{ + private readonly IRepositoryHelper _repository; + + public RepositoryHelperFactory(IOptions options, IEnumerable repositories) + { + var option = options.Value.Tables.FirstOrDefault(x => x.InstanceType == typeof(TEntity)); + if (!(option is InstantAPIsOptions.Table tableOptions)) + throw new Exception("Configuration mismatch"); + + var contextType = typeof(TContext); + var setType = typeof(TSet); + _repository = repositories + .First(x => x.IsValidFor(contextType, setType)) + .Create(tableOptions.EntitySelector.Compile(), tableOptions.Config); + } + + public Task> Get(HttpRequest request, TContext context, string name, CancellationToken cancellationToken) + => _repository.Get(request, context, name, cancellationToken); + + public Task GetById(HttpRequest request, TContext context, string name, TKey id, CancellationToken cancellationToken) + => _repository.GetById(request, context, name, id, cancellationToken); + + public Task Insert(HttpRequest request, TContext context, string name, TEntity newObj, CancellationToken cancellationToken) + => _repository.Insert(request, context, name, newObj, cancellationToken); + + public Task Update(HttpRequest request, TContext context, string name, TKey id, TEntity newObj, CancellationToken cancellationToken) + => _repository.Update(request, context, name, id, newObj, cancellationToken); + + public Task Delete(HttpRequest request, TContext context, string name, TKey id, CancellationToken cancellationToken) + => _repository.Delete(request, context, name, id, cancellationToken); + +} diff --git a/InstantAPIs/WebApplicationExtensions.cs b/InstantAPIs/WebApplicationExtensions.cs index 4f9b6fd..207d686 100644 --- a/InstantAPIs/WebApplicationExtensions.cs +++ b/InstantAPIs/WebApplicationExtensions.cs @@ -1,4 +1,4 @@ -using Microsoft.AspNetCore.Builder; +using InstantAPIs.Repositories; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -8,35 +8,38 @@ using System.Linq; using System.Reflection; -namespace InstantAPIs; +namespace Microsoft.AspNetCore.Builder; public static class WebApplicationExtensions { - - internal const string LOGGER_CATEGORY_NAME = "InstantAPI"; - private static HashSet Configuration { get; set; } = new(); + internal const string LOGGER_CATEGORY_NAME = "InstantAPI"; - public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action>? options = null) where TContext : DbContext + public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action>? options = null) + where TContext : class { + var instantApiOptions = app.ServiceProvider.GetRequiredService>().Value; if (app is IApplicationBuilder applicationBuilder) { - AddOpenAPIConfiguration(app, options, applicationBuilder); + AddOpenAPIConfiguration(app, applicationBuilder); } - // Get the tables on the DbContext - var dbTables = GetDbTablesForContext(); - - var requestedTables = !Configuration.Any() ? - dbTables : - Configuration.Where(t => dbTables.Any(db => db.Name != null && db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray(); + // Get the tables on the TContext + var contextFactory = app.ServiceProvider.GetRequiredService>(); + var builder = new InstantAPIsBuilder(instantApiOptions, contextFactory); + if (options != null) + { + options(builder); + } + var requestedTables = builder.Build(); + instantApiOptions.Tables = requestedTables; MapInstantAPIsUsingReflection(app, requestedTables); return app; } - private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, IEnumerable requestedTables) where TContext : DbContext + private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, IEnumerable requestedTables) { ILogger logger = NullLogger.Instance; @@ -47,34 +50,35 @@ private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilde } var allMethods = typeof(MapApiExtensions).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(m => m.Name.StartsWith("Map")).ToArray(); - var initialize = typeof(MapApiExtensions).GetMethod("Initialize", BindingFlags.NonPublic | BindingFlags.Static) - ?? throw new Exception($"{nameof(MapApiExtensions)} doest have an {nameof(MapApiExtensions.Initialize)}, this should never happen"); foreach (var table in requestedTables) { - - // The default URL for an InstantAPI is /api/TABLENAME - //var url = $"/api/{table.Name}"; - - initialize.MakeGenericMethod(typeof(TContext), table.InstanceType ?? throw new Exception($"Instance type for table {table.Name} is null")).Invoke(null, new[] { logger }); - // The remaining private static methods in this class build out the Mapped API methods.. // let's use some reflection to get them foreach (var method in allMethods) { var sigAttr = method.CustomAttributes.First(x => x.AttributeType == typeof(ApiMethodAttribute)).ConstructorArguments.First(); - if (sigAttr.Value == null) continue; - var methodType = (ApiMethodsToGenerate)sigAttr.Value; + var methodType = (ApiMethodsToGenerate)(sigAttr.Value ?? throw new NullReferenceException("Missing attribute on method map")); if ((table.ApiMethodsToGenerate & methodType) != methodType) continue; - var genericMethod = method.MakeGenericMethod(typeof(TContext), table.InstanceType); - genericMethod.Invoke(null, new object[] { app, table.BaseUrl?.ToString() ?? throw new Exception($"BaseUrl for {table.Name} is null") }); + var url = table.BaseUrl.ToString(); + + if (table.EntitySelectorObject != null && table.ConfigObject != null) + { + var typesSelector = table.EntitySelectorObject.GetType().GetGenericArguments(); + if (typesSelector.Length == 1 && typesSelector[0].IsGenericType) + { + typesSelector = typesSelector[0].GetGenericArguments(); + } + var typesConfig = table.ConfigObject.GetType().GetGenericArguments(); + var genericMethod = method.MakeGenericMethod(typesSelector[0], typesSelector[1], typesConfig[0], typesConfig[1]); + genericMethod.Invoke(null, new object[] { app, url, table.Name }); + } } - } } - private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action>? options, IApplicationBuilder applicationBuilder) where TContext : DbContext + private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, IApplicationBuilder applicationBuilder) { // Check if AddInstantAPIs was called by getting the service options and evaluate EnableSwagger property var serviceOptions = applicationBuilder.ApplicationServices.GetRequiredService>().Value; @@ -90,24 +94,5 @@ private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, applicationBuilder.UseSwagger(); applicationBuilder.UseSwaggerUI(); } - - var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService(); - var builder = new InstantAPIsBuilder(ctx); - if (options != null) - { - options(builder); - Configuration = builder.Build(); - } } - - internal static IEnumerable GetDbTablesForContext() where TContext : DbContext - { - return typeof(TContext).GetProperties(BindingFlags.Instance | BindingFlags.Public) - .Where(x => (x.PropertyType.FullName?.StartsWith("Microsoft.EntityFrameworkCore.DbSet") ?? false) - && x.PropertyType.GenericTypeArguments.First().GetCustomAttributes(typeof(KeylessAttribute), true).Length <= 0) - .Select(x => InstantAPIsBuilder.CreateTable(x.Name, new Uri($"/api/{x.Name}", uriKind: UriKind.RelativeOrAbsolute), typeof(TContext), x.PropertyType, x.PropertyType.GenericTypeArguments.First())) - .Where(x => x != null).OfType() - .ToArray(); - } - } diff --git a/Test/Configuration/InstantAPIsConfigBuilderFixture.cs b/Test/Configuration/InstantAPIsConfigBuilderFixture.cs index 7ed6e2e..629650c 100644 --- a/Test/Configuration/InstantAPIsConfigBuilderFixture.cs +++ b/Test/Configuration/InstantAPIsConfigBuilderFixture.cs @@ -1,5 +1,9 @@ -using Microsoft.AspNetCore.Builder; +using InstantAPIs.Repositories; +using InstantAPIs; +using Microsoft.AspNetCore.Builder; using Microsoft.EntityFrameworkCore; +using Moq; +using System.Linq.Expressions; namespace Test.Configuration; @@ -9,11 +13,14 @@ public abstract class InstantAPIsConfigBuilderFixture : BaseFixture public InstantAPIsConfigBuilderFixture() { - - var _ContextOptions = new DbContextOptionsBuilder() - .UseInMemoryDatabase("TestDb") - .Options; - _Builder = new(new(_ContextOptions)); - + var contextMock = new Mock>(); + contextMock.Setup(x => x.NameTable(It.Is>>>(a => ((MemberExpression)a.Body).Member.Name == "Contacts"))).Returns("Contacts"); + contextMock.Setup(x => x.NameTable(It.Is>>>(a => ((MemberExpression)a.Body).Member.Name == "Addresses"))).Returns("Addresses"); + contextMock.Setup(x => x.DiscoverFromContext(It.IsAny())) + .Returns(new InstantAPIsOptions.ITable[] { + new InstantAPIsOptions.Table, Contact, int>("Contacts", new Uri("Contacts", UriKind.Relative), c => c.Contacts , new InstantAPIsOptions.TableOptions() { KeySelector = x => x.Id }), + new InstantAPIsOptions.Table, Address, int>("Addresses", new Uri("Addresses", UriKind.Relative), c => c.Addresses, new InstantAPIsOptions.TableOptions() { KeySelector = x => x.Id }) + }); + _Builder = new(new InstantAPIsOptions(), contextMock.Object); } } diff --git a/Test/Configuration/WithoutIncludes.cs b/Test/Configuration/WithoutIncludes.cs index fceb34e..010f6af 100644 --- a/Test/Configuration/WithoutIncludes.cs +++ b/Test/Configuration/WithoutIncludes.cs @@ -1,27 +1,10 @@ using InstantAPIs; -using Microsoft.AspNetCore.Builder; -using Microsoft.EntityFrameworkCore; -using System.Linq; using Xunit; namespace Test.Configuration; -public class WithoutIncludes : BaseFixture +public class WithoutIncludes : InstantAPIsConfigBuilderFixture { - - InstantAPIsBuilder _Builder; - - public WithoutIncludes() - { - - var _ContextOptions = new DbContextOptionsBuilder() - .UseInMemoryDatabase("TestDb") - .Options; - _Builder = new(new(_ContextOptions)); - - } - - [Fact] public void ShouldIncludeAllTables() { @@ -32,10 +15,9 @@ public void ShouldIncludeAllTables() var config = _Builder.Build(); // assert - Assert.Equal(2, config.Count); + Assert.Equal(2, config.Count()); Assert.Equal(ApiMethodsToGenerate.All, config.First().ApiMethodsToGenerate); Assert.Equal(ApiMethodsToGenerate.All, config.Skip(1).First().ApiMethodsToGenerate); } - } diff --git a/Test/InstantAPIs/WebApplicationExtensions.cs b/Test/InstantAPIs/WebApplicationExtensions.cs index 138f9d9..0186490 100644 --- a/Test/InstantAPIs/WebApplicationExtensions.cs +++ b/Test/InstantAPIs/WebApplicationExtensions.cs @@ -1,7 +1,12 @@ using InstantAPIs; +using InstantAPIs.Repositories; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; -using System.Collections.Generic; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Moq; +using System.Linq.Expressions; using Xunit; namespace Test.InstantAPIs; @@ -14,9 +19,25 @@ public void WhenMapInstantAPIsExpectedDefaultBehaviour() { // arrange + var serviceProviderMock = Mockery.Create(); + var optionsMock = Mockery.Create>(); var app = Mockery.Create(); var dataSources = new List(); + var contextMock = Mockery.Create>(); + + contextMock.Setup(x => x.NameTable(It.Is>>>(a => ((MemberExpression)a.Body).Member.Name == "Contacts"))).Returns("Contacts"); + contextMock.Setup(x => x.NameTable(It.Is>>>(a => ((MemberExpression)a.Body).Member.Name == "Addresses"))).Returns("Addresses"); + contextMock.Setup(x => x.DiscoverFromContext(It.IsAny())) + .Returns(new InstantAPIsOptions.ITable[] { + new InstantAPIsOptions.Table, Contact, int>("Contacts", new Uri("Contacts", UriKind.Relative), c => c.Contacts , new InstantAPIsOptions.TableOptions() { KeySelector = x => x.Id }), + new InstantAPIsOptions.Table, Address, int>("Addresses", new Uri("Addresses", UriKind.Relative), c => c.Addresses, new InstantAPIsOptions.TableOptions() { KeySelector = x => x.Id }) + }); app.Setup(x => x.DataSources).Returns(dataSources); + app.Setup(x => x.ServiceProvider).Returns(serviceProviderMock.Object); + serviceProviderMock.Setup(x => x.GetService(typeof(IOptions))).Returns(optionsMock.Object); + serviceProviderMock.Setup(x => x.GetService(typeof(IContextHelper))).Returns(contextMock.Object); + serviceProviderMock.Setup(x => x.GetService(typeof(ILoggerFactory))).Returns(Mockery.Create().Object); + optionsMock.Setup(x => x.Value).Returns(new InstantAPIsOptions()); // act app.Object.MapInstantAPIs(); diff --git a/Test/StubData/Contact.cs b/Test/StubData/Contact.cs index 7952382..ed558dd 100644 --- a/Test/StubData/Contact.cs +++ b/Test/StubData/Contact.cs @@ -1,6 +1,6 @@ namespace Test.StubData; -internal class Contact +public class Contact { public int Id { get; set; } diff --git a/Test/StubData/MyContext.cs b/Test/StubData/MyContext.cs index 66ed1e8..9f30e7c 100644 --- a/Test/StubData/MyContext.cs +++ b/Test/StubData/MyContext.cs @@ -2,7 +2,7 @@ namespace Test.StubData; -internal class MyContext : DbContext +public class MyContext : DbContext { public MyContext(DbContextOptions options) : base(options) { } diff --git a/TestJson/Program.cs b/TestJson/Program.cs index da20801..789faa4 100644 --- a/TestJson/Program.cs +++ b/TestJson/Program.cs @@ -1,8 +1,17 @@ -using InstantAPIs; +using InstantAPIs.Repositories.Json; +using System.Text.Json.Nodes; var builder = WebApplication.CreateBuilder(args); +builder.Services.AddInstantAPIs(); +builder.Services.Configure(x => x.JsonFilename = "mock.json"); +builder.Services.AddSingleton(); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); -app.UseJsonRoutes(); +app.MapInstantAPIs(builder => +{ + builder + .IncludeTable(context => context.LoadTable("products"), new InstantAPIs.InstantAPIsOptions.TableOptions(), baseUrl: "api/someproducts"); +}); +//app.MapInstantAPIs(); app.Run(); diff --git a/WorkingApi/Program.cs b/WorkingApi/Program.cs index 7180330..de55d58 100644 --- a/WorkingApi/Program.cs +++ b/WorkingApi/Program.cs @@ -1,6 +1,4 @@ using InstantAPIs; -using Microsoft.EntityFrameworkCore; -using Microsoft.OpenApi.Models; using System.Diagnostics; using WorkingApi; @@ -10,12 +8,9 @@ builder.Services.AddInstantAPIs(); var app = builder.Build(); - -var sw = Stopwatch.StartNew(); - -app.MapInstantAPIs(config => +app.MapInstantAPIs(builder => { - config.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions(), ApiMethodsToGenerate.All, "addressBook"); + builder.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions() { KeySelector = x => x.Id }, ApiMethodsToGenerate.All, "addressBook"); }); app.Run(); From 625abfc4731a509e97ebcdb728302e40bf473dae Mon Sep 17 00:00:00 2001 From: Dries Verbeke Date: Fri, 22 Jul 2022 01:31:16 +0200 Subject: [PATCH 9/9] Json Repository Helper fixes - implemented an update - insert should take max of the key + 1, instead of just counting - remove unused constructor prarameter --- .../Repositories/Json/RepositoryHelper.cs | 48 +++++++++++++------ .../Json/RepositoryHelperFactory.cs | 7 ++- 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/InstantAPIs/Repositories/Json/RepositoryHelper.cs b/InstantAPIs/Repositories/Json/RepositoryHelper.cs index 79f2432..db6c305 100644 --- a/InstantAPIs/Repositories/Json/RepositoryHelper.cs +++ b/InstantAPIs/Repositories/Json/RepositoryHelper.cs @@ -1,19 +1,16 @@ using Microsoft.AspNetCore.Http; -using System.Net.Http.Json; using System.Text.Json.Nodes; namespace InstantAPIs.Repositories.Json; -internal class JsonRepositoryHelper : +internal class RepositoryHelper : IRepositoryHelper { private readonly Func _setSelector; - private readonly InstantAPIsOptions.TableOptions _config; - public JsonRepositoryHelper(Func setSelector, InstantAPIsOptions.TableOptions config) + public RepositoryHelper(Func setSelector, InstantAPIsOptions.TableOptions config) { _setSelector = setSelector; - _config = config; } public Task> Get(HttpRequest request, Context context, string name, CancellationToken cancellationToken) @@ -24,9 +21,9 @@ public Task> Get(HttpRequest request, Context context, s public Task GetById(HttpRequest request, Context context, string name, int id, CancellationToken cancellationToken) { var array = context.LoadTable(name); - var matchedItem = array.SingleOrDefault(row => (row ?? throw new Exception("No row found")) + var matchedItem = array.SingleOrDefault(row => row != null && row .AsObject() - .Any(o => o.Key.ToLower() == "id" && o.Value?.ToString() == id.ToString()) + .Any(o => o.Key.ToLower() == "id" && o.Value?.GetValue() == id) )?.AsObject(); return Task.FromResult(matchedItem); } @@ -36,8 +33,13 @@ public Task Insert(HttpRequest request, Context context, string name, JsonO { var array = context.LoadTable(name); - var key = array.Count + 1; - newObj.AsObject().Add("Id", key.ToString()); + var lastKey = array + .Select(row => row?.AsObject().FirstOrDefault(o => o.Key.ToLower() == "id").Value?.GetValue()) + .Select(x => x.GetValueOrDefault()) + .Max(); + + var key = lastKey + 1; + newObj.AsObject().Add("id", key); array.Add(newObj); context.SaveChanges(); @@ -47,8 +49,25 @@ public Task Insert(HttpRequest request, Context context, string name, JsonO public Task Update(HttpRequest request, Context context, string name, int id, JsonObject newObj, CancellationToken cancellationToken) { var array = context.LoadTable(name); - array.Add(newObj); - context.SaveChanges(); + var matchedItem = array.SingleOrDefault(row => row != null + && row.AsObject().Any(o => o.Key.ToLower() == "id" && o.Value?.GetValue() == id) + )?.AsObject(); + if (matchedItem != null) + { + var updates = newObj + .GroupJoin(matchedItem, o => o.Key, i => i.Key, (o, i) => new { NewValue = o, OldValue = i.FirstOrDefault() }) + .Where(x => x.NewValue.Key.ToLower() != "id") + .ToList(); + foreach (var newField in updates) + { + if (newField.OldValue.Value != null) + { + matchedItem.Remove(newField.OldValue.Key); + } + matchedItem.Add(newField.NewValue.Key, JsonValue.Create(newField.NewValue.Value?.GetValue())); + } + context.SaveChanges(); + } return Task.CompletedTask; } @@ -58,9 +77,9 @@ public Task Delete(HttpRequest request, Context context, string name, int var array = context.LoadTable(name); var matchedItem = array .Select((value, index) => new { value, index }) - .SingleOrDefault(row => (row.value ?? throw new Exception("No json value found")) - .AsObject() - .Any(o => o.Key.ToLower() == "id" && o.Value?.ToString() == id.ToString())); + .SingleOrDefault(row => row.value == null + ? false + : row.value.AsObject().Any(o => o.Key.ToLower() == "id" && o.Value?.GetValue() == id)); if (matchedItem != null) { array.RemoveAt(matchedItem.index); @@ -69,4 +88,5 @@ public Task Delete(HttpRequest request, Context context, string name, int return Task.FromResult(true); } + } diff --git a/InstantAPIs/Repositories/Json/RepositoryHelperFactory.cs b/InstantAPIs/Repositories/Json/RepositoryHelperFactory.cs index 58209bc..01a45b5 100644 --- a/InstantAPIs/Repositories/Json/RepositoryHelperFactory.cs +++ b/InstantAPIs/Repositories/Json/RepositoryHelperFactory.cs @@ -1,5 +1,4 @@ -using System.Net.Http.Json; -using System.Text.Json.Nodes; +using System.Text.Json.Nodes; namespace InstantAPIs.Repositories.Json; @@ -13,8 +12,8 @@ public IRepositoryHelper Create)returnValue;