Skip to content

Commit 0846f32

Browse files
authored
Config improvements - fixes #2 (#34)
* Started work on options config * Completed Include(table).. needs the filter for the API methods applied * Cleaned up IncludeTable syntax * Fixed spelling of test * Added unit tests and docs for Include/Exclude * Added unit tests for final Include/Exclude configuration
1 parent 4581270 commit 0846f32

19 files changed

+489
-79
lines changed

.dockerignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
**/.classpath
2+
**/.dockerignore
3+
**/.env
4+
**/.git
5+
**/.gitignore
6+
**/.project
7+
**/.settings
8+
**/.toolstarget
9+
**/.vs
10+
**/.vscode
11+
**/*.*proj.user
12+
**/*.dbmdl
13+
**/*.jfm
14+
**/azds.yaml
15+
**/bin
16+
**/charts
17+
**/docker-compose*
18+
**/Dockerfile*
19+
**/node_modules
20+
**/npm-debug.log
21+
**/obj
22+
**/secrets.dev.yaml
23+
**/values.dev.yaml
24+
LICENSE
25+
README.md

Fritz.InstantAPIs/ApiMethodsToGenerate.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
namespace Microsoft.AspNetCore.Builder;
1+
namespace Fritz.InstantAPIs;
22

33
[Flags]
44
public enum ApiMethodsToGenerate
@@ -9,4 +9,16 @@ public enum ApiMethodsToGenerate
99
Update = 8,
1010
Delete = 16,
1111
All = 31
12+
}
13+
14+
public record TableApiMapping(string TableName, ApiMethodsToGenerate MethodsToGenerate = ApiMethodsToGenerate.All);
15+
16+
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
17+
public class ApiMethodAttribute : Attribute
18+
{
19+
public ApiMethodsToGenerate MethodsToGenerate { get; set; }
20+
public ApiMethodAttribute(ApiMethodsToGenerate apiMethodsToGenerate)
21+
{
22+
this.MethodsToGenerate = apiMethodsToGenerate;
23+
}
1224
}
Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,44 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

3-
<PropertyGroup>
4-
<TargetFramework>net6.0</TargetFramework>
5-
<ImplicitUsings>enable</ImplicitUsings>
6-
<Nullable>enable</Nullable>
7-
<Authors>csharpfritz</Authors>
8-
<Description>A library that generates Minimal API endpoints for an Entity Framework context.</Description>
9-
<PackageLicenseExpression>MIT</PackageLicenseExpression>
10-
<PackageTags>entity framework, ef, webapi</PackageTags>
11-
<RepositoryType>git</RepositoryType>
12-
<RepositoryUrl>https://github.com/csharpfritz/InstantAPIs</RepositoryUrl>
13-
<PackageReadmeFile>README.md</PackageReadmeFile>
14-
<PackageProjectUrl>https://github.com/csharpfritz/InstantAPIs</PackageProjectUrl>
15-
<PublishRepositoryUrl>true</PublishRepositoryUrl>
16-
<EmbedUntrackedSources>true</EmbedUntrackedSources>
17-
<DebugType>embedded</DebugType>
18-
<Version>0.1.0</Version>
19-
<PackageReleaseNotes>Initial release</PackageReleaseNotes>
20-
</PropertyGroup>
3+
<PropertyGroup>
4+
<TargetFramework>net6.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<Nullable>enable</Nullable>
7+
<Authors>csharpfritz</Authors>
8+
<Description>A library that generates Minimal API endpoints for an Entity Framework context.</Description>
9+
<PackageLicenseExpression>MIT</PackageLicenseExpression>
10+
<PackageTags>entity framework, ef, webapi</PackageTags>
11+
<RepositoryType>git</RepositoryType>
12+
<RepositoryUrl>https://github.com/csharpfritz/InstantAPIs</RepositoryUrl>
13+
<PackageReadmeFile>README.md</PackageReadmeFile>
14+
<PackageProjectUrl>https://github.com/csharpfritz/InstantAPIs</PackageProjectUrl>
15+
<PublishRepositoryUrl>true</PublishRepositoryUrl>
16+
<EmbedUntrackedSources>true</EmbedUntrackedSources>
17+
<DebugType>embedded</DebugType>
18+
<Version>0.1.0</Version>
19+
<PackageReleaseNotes>Initial release</PackageReleaseNotes>
20+
</PropertyGroup>
21+
22+
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
23+
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
24+
</PropertyGroup>
2125

22-
<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
23-
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
24-
</PropertyGroup>
25-
2626
<ItemGroup>
2727
<FrameworkReference Include="Microsoft.AspNetCore.App" />
2828
<None Include="..\README.md">
29-
<Pack>True</Pack>
30-
<PackagePath>\</PackagePath>
29+
<Pack>True</Pack>
30+
<PackagePath>\</PackagePath>
3131
</None>
3232
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" />
3333
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
34-
</ItemGroup>
34+
</ItemGroup>
3535

3636
<ItemGroup>
3737
<Using Include="Fritz.InstantAPIs" />
3838
<Using Include="Microsoft.EntityFrameworkCore"></Using>
39+
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
40+
<_Parameter1>Test</_Parameter1>
41+
</AssemblyAttribute>
3942
</ItemGroup>
4043

4144
</Project>
Lines changed: 115 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,121 @@
11
namespace Microsoft.AspNetCore.Builder;
22

3-
public class InstantAPIsConfig
3+
internal class InstantAPIsConfig
44
{
55

6-
public static readonly string[] DefaultTables = new[] { "all" };
7-
8-
public string[] Tables { get; set; } = DefaultTables;
6+
internal HashSet<WebApplicationExtensions.TypeTable> Tables { get; } = new HashSet<WebApplicationExtensions.TypeTable>();
97

108
}
9+
10+
11+
public class InstantAPIsConfigBuilder<D> where D : DbContext
12+
{
13+
14+
private InstantAPIsConfig _Config = new();
15+
private Type _ContextType = typeof(D);
16+
private D _TheContext;
17+
private readonly HashSet<TableApiMapping> _IncludedTables = new();
18+
private readonly List<string> _ExcludedTables = new();
19+
20+
public InstantAPIsConfigBuilder(D theContext)
21+
{
22+
this._TheContext = theContext;
23+
}
24+
25+
#region Table Inclusion/Exclusion
26+
27+
/// <summary>
28+
/// Specify individual tables to include in the API generation with the methods requested
29+
/// </summary>
30+
/// <param name="entitySelector">Select the EntityFramework DbSet to include - Required</param>
31+
/// <param name="methodsToGenerate">A flags enumerable indicating the methods to generate. By default ALL are generated</param>
32+
/// <returns>Configuration builder with this configuration applied</returns>
33+
public InstantAPIsConfigBuilder<D> IncludeTable<T>(Func<D, DbSet<T>> entitySelector, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All) where T : class
34+
{
35+
36+
var theSetType = entitySelector(_TheContext).GetType().BaseType;
37+
var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType);
38+
39+
var tableApiMapping = new TableApiMapping(property.Name, methodsToGenerate);
40+
_IncludedTables.Add(tableApiMapping);
41+
42+
if (_ExcludedTables.Contains(tableApiMapping.TableName)) _ExcludedTables.Remove(tableApiMapping.TableName);
43+
_IncludedTables.Add(tableApiMapping);
44+
45+
return this;
46+
47+
}
48+
49+
/// <summary>
50+
/// Exclude individual tables from the API generation. Exclusion takes priority over inclusion
51+
/// </summary>
52+
/// <param name="entitySelector">Select the entity to exclude from generation</param>
53+
/// <returns>Configuration builder with this configuraiton applied</returns>
54+
public InstantAPIsConfigBuilder<D> ExcludeTable<T>(Func<D, DbSet<T>> entitySelector) where T : class
55+
{
56+
57+
var theSetType = entitySelector(_TheContext).GetType().BaseType;
58+
var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType);
59+
60+
if (_IncludedTables.Select(t => t.TableName).Contains(property.Name)) _IncludedTables.Remove(_IncludedTables.First(t => t.TableName == property.Name));
61+
_ExcludedTables.Add(property.Name);
62+
63+
return this;
64+
65+
}
66+
67+
private void BuildTables()
68+
{
69+
70+
var tables = WebApplicationExtensions.GetDbTablesForContext<D>().ToArray();
71+
72+
if (!_IncludedTables.Any() && !_ExcludedTables.Any())
73+
{
74+
_Config.Tables.UnionWith(tables.Select(t => new WebApplicationExtensions.TypeTable
75+
{
76+
Name = t.Name,
77+
InstanceType = t.InstanceType,
78+
ApiMethodsToGenerate = ApiMethodsToGenerate.All
79+
}));
80+
return;
81+
}
82+
83+
// Add the Included tables
84+
var outTables = tables.Where(t => _IncludedTables.Any(i => i.TableName.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)))
85+
.Select(t => new WebApplicationExtensions.TypeTable
86+
{
87+
Name = t.Name,
88+
InstanceType = t.InstanceType,
89+
ApiMethodsToGenerate = _IncludedTables.First(i => i.TableName.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).MethodsToGenerate
90+
}).ToArray();
91+
92+
// If no tables were added, added them all
93+
if (outTables.Length == 0)
94+
{
95+
outTables = tables.Select(t => new WebApplicationExtensions.TypeTable
96+
{
97+
Name = t.Name,
98+
InstanceType = t.InstanceType
99+
}).ToArray();
100+
}
101+
102+
// Remove the Excluded tables
103+
outTables = outTables.Where(t => !_ExcludedTables.Any(e => t.Name.Equals(e, StringComparison.InvariantCultureIgnoreCase))).ToArray();
104+
105+
if (outTables == null || !outTables.Any()) throw new ArgumentException("All tables were excluded from this configuration");
106+
107+
_Config.Tables.UnionWith(outTables);
108+
109+
}
110+
111+
#endregion
112+
113+
internal InstantAPIsConfig Build()
114+
{
115+
116+
BuildTables();
117+
118+
return _Config;
119+
}
120+
121+
}

Fritz.InstantAPIs/MapApiExtensions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ internal class MapApiExtensions
1111

1212
// TODO: Authentication / Authorization
1313

14+
[ApiMethod(ApiMethodsToGenerate.Get)]
1415
internal static void MapInstantGetAll<D, C>(IEndpointRouteBuilder app, string url)
1516
where D : DbContext where C : class
1617
{
@@ -22,6 +23,7 @@ internal static void MapInstantGetAll<D, C>(IEndpointRouteBuilder app, string ur
2223

2324
}
2425

26+
[ApiMethod(ApiMethodsToGenerate.GetById)]
2527
internal static void MapGetById<D,C>(IEndpointRouteBuilder app, string url)
2628
where D: DbContext where C : class
2729
{
@@ -49,6 +51,7 @@ internal static void MapGetById<D,C>(IEndpointRouteBuilder app, string url)
4951

5052
}
5153

54+
[ApiMethod(ApiMethodsToGenerate.Insert)]
5255
internal static void MapInstantPost<D, C>(IEndpointRouteBuilder app, string url)
5356
where D : DbContext where C : class
5457
{
@@ -62,6 +65,7 @@ internal static void MapInstantPost<D, C>(IEndpointRouteBuilder app, string url)
6265

6366
}
6467

68+
[ApiMethod(ApiMethodsToGenerate.Update)]
6569
internal static void MapInstantPut<D, C>(IEndpointRouteBuilder app, string url)
6670
where D : DbContext where C : class
6771
{
@@ -76,6 +80,7 @@ internal static void MapInstantPut<D, C>(IEndpointRouteBuilder app, string url)
7680

7781
}
7882

83+
[ApiMethod(ApiMethodsToGenerate.Delete)]
7984
internal static void MapDeleteById<D, C>(IEndpointRouteBuilder app, string url)
8085
where D : DbContext where C : class
8186
{
Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,51 @@
11
using Microsoft.AspNetCore.Routing;
2+
using Microsoft.Extensions.DependencyInjection;
23
using System.Reflection;
34

45
namespace Microsoft.AspNetCore.Builder;
56

67
public static class WebApplicationExtensions
78
{
89

9-
public static InstantAPIsConfig Configuration { get; set; } = new();
10+
private static InstantAPIsConfig Configuration { get; set; } = new();
1011

11-
public static IEndpointRouteBuilder MapInstantAPIs<D>(this IEndpointRouteBuilder app, Action<InstantAPIsConfig> configAction = null) where D: DbContext
12+
public static IEndpointRouteBuilder MapInstantAPIs<D>(this IEndpointRouteBuilder app, Action<InstantAPIsConfigBuilder<D>> options = null) where D: DbContext
1213
{
1314

14-
if (configAction != null) configAction(Configuration);
15+
if (app is IApplicationBuilder applicationBuilder)
16+
{
17+
var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetService(typeof(D)) as D;
18+
var builder = new InstantAPIsConfigBuilder<D>(ctx);
19+
if (options != null)
20+
{
21+
options(builder);
22+
Configuration = builder.Build();
23+
}
24+
}
1525

1626
// Get the tables on the DbContext
17-
var dbTables = typeof(D).GetProperties(BindingFlags.Instance | BindingFlags.Public)
18-
.Where(x => x.PropertyType.FullName.StartsWith("Microsoft.EntityFrameworkCore.DbSet"))
19-
.Select(x => new TypeTable { Name = x.Name, InstanceType = x.PropertyType.GenericTypeArguments.First() });
27+
var dbTables = GetDbTablesForContext<D>();
2028

21-
var requestedTables = Configuration?.Tables ?? InstantAPIsConfig.DefaultTables;
29+
var requestedTables = !Configuration.Tables.Any() ?
30+
dbTables :
31+
Configuration.Tables.Where(t => dbTables.Any(db => db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray();
2232

23-
foreach (var table in dbTables.Where(
24-
x => (requestedTables == InstantAPIsConfig.DefaultTables) || requestedTables.Any(t => t.Equals(x.Name, StringComparison.InvariantCultureIgnoreCase))
25-
)
26-
)
33+
var allMethods = typeof(MapApiExtensions).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(m => m.Name.StartsWith("Map")).ToArray();
34+
foreach (var table in requestedTables)
2735
{
2836

2937
// The default URL for an InstantAPI is /api/TABLENAME
3038
var url = $"/api/{table.Name}";
3139

3240
// The remaining private static methods in this class build out the Mapped API methods..
3341
// let's use some reflection to get them
34-
var allMethods = typeof(MapApiExtensions).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(m => m.Name.StartsWith("Map"));
3542
foreach (var method in allMethods)
3643
{
44+
45+
var sigAttr = method.CustomAttributes.First(x => x.AttributeType == typeof(ApiMethodAttribute)).ConstructorArguments.First();
46+
var methodType = (ApiMethodsToGenerate)sigAttr.Value;
47+
if ((table.ApiMethodsToGenerate & methodType) != methodType) continue;
48+
3749
var genericMethod = method.MakeGenericMethod(typeof(D), table.InstanceType);
3850
genericMethod.Invoke(null, new object[] { app, url });
3951
}
@@ -43,10 +55,19 @@ public static IEndpointRouteBuilder MapInstantAPIs<D>(this IEndpointRouteBuilder
4355
return app;
4456
}
4557

58+
internal static IEnumerable<TypeTable> GetDbTablesForContext<D>() where D : DbContext
59+
{
60+
return typeof(D).GetProperties(BindingFlags.Instance | BindingFlags.Public)
61+
.Where(x => x.PropertyType.FullName.StartsWith("Microsoft.EntityFrameworkCore.DbSet"))
62+
.Select(x => new TypeTable { Name = x.Name, InstanceType = x.PropertyType.GenericTypeArguments.First() })
63+
.ToArray();
64+
}
65+
4666
internal class TypeTable
4767
{
4868
public string Name { get; set; }
4969
public Type InstanceType { get; set; }
70+
public ApiMethodsToGenerate ApiMethodsToGenerate { get; set; } = ApiMethodsToGenerate.All;
5071
}
5172

5273
}

Test/BaseFixture.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
using Moq;
1+
using Microsoft.AspNetCore.Builder;
2+
using Microsoft.EntityFrameworkCore;
3+
using Microsoft.Extensions.Options;
4+
using Moq;
25

36
namespace Test;
47

58
public abstract class BaseFixture
69
{
7-
public BaseFixture()
8-
{
9-
Mockery = new MockRepository(MockBehavior.Loose);
10-
}
1110

12-
protected MockRepository Mockery { get; private set; }
11+
protected MockRepository Mockery { get; private set; } = new MockRepository(MockBehavior.Loose);
12+
1313
}

0 commit comments

Comments
 (0)