Skip to content

Commit

Permalink
Config improvements - fixes #2 (#34)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
csharpfritz committed Feb 23, 2022
1 parent 4581270 commit 0846f32
Show file tree
Hide file tree
Showing 19 changed files with 489 additions and 79 deletions.
25 changes: 25 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md
14 changes: 13 additions & 1 deletion Fritz.InstantAPIs/ApiMethodsToGenerate.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Microsoft.AspNetCore.Builder;
namespace Fritz.InstantAPIs;

[Flags]
public enum ApiMethodsToGenerate
Expand All @@ -9,4 +9,16 @@ public enum ApiMethodsToGenerate
Update = 8,
Delete = 16,
All = 31
}

public record TableApiMapping(string TableName, ApiMethodsToGenerate MethodsToGenerate = ApiMethodsToGenerate.All);

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ApiMethodAttribute : Attribute
{
public ApiMethodsToGenerate MethodsToGenerate { get; set; }
public ApiMethodAttribute(ApiMethodsToGenerate apiMethodsToGenerate)
{
this.MethodsToGenerate = apiMethodsToGenerate;
}
}
53 changes: 28 additions & 25 deletions Fritz.InstantAPIs/Fritz.InstantAPIs.csproj
Original file line number Diff line number Diff line change
@@ -1,41 +1,44 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>csharpfritz</Authors>
<Description>A library that generates Minimal API endpoints for an Entity Framework context.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>entity framework, ef, webapi</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/csharpfritz/InstantAPIs</RepositoryUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://github.com/csharpfritz/InstantAPIs</PackageProjectUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>embedded</DebugType>
<Version>0.1.0</Version>
<PackageReleaseNotes>Initial release</PackageReleaseNotes>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<Authors>csharpfritz</Authors>
<Description>A library that generates Minimal API endpoints for an Entity Framework context.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>entity framework, ef, webapi</PackageTags>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/csharpfritz/InstantAPIs</RepositoryUrl>
<PackageReadmeFile>README.md</PackageReadmeFile>
<PackageProjectUrl>https://github.com/csharpfritz/InstantAPIs</PackageProjectUrl>
<PublishRepositoryUrl>true</PublishRepositoryUrl>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
<DebugType>embedded</DebugType>
<Version>0.1.0</Version>
<PackageReleaseNotes>Initial release</PackageReleaseNotes>
</PropertyGroup>

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

<PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
</PropertyGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<None Include="..\README.md">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="6.0.2" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.0.0" PrivateAssets="All" />
</ItemGroup>
</ItemGroup>

<ItemGroup>
<Using Include="Fritz.InstantAPIs" />
<Using Include="Microsoft.EntityFrameworkCore"></Using>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleToAttribute">
<_Parameter1>Test</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

</Project>
119 changes: 115 additions & 4 deletions Fritz.InstantAPIs/InstantAPIsConfig.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,121 @@
namespace Microsoft.AspNetCore.Builder;

public class InstantAPIsConfig
internal class InstantAPIsConfig
{

public static readonly string[] DefaultTables = new[] { "all" };

public string[] Tables { get; set; } = DefaultTables;
internal HashSet<WebApplicationExtensions.TypeTable> Tables { get; } = new HashSet<WebApplicationExtensions.TypeTable>();

}


public class InstantAPIsConfigBuilder<D> where D : DbContext
{

private InstantAPIsConfig _Config = new();
private Type _ContextType = typeof(D);
private D _TheContext;
private readonly HashSet<TableApiMapping> _IncludedTables = new();
private readonly List<string> _ExcludedTables = new();

public InstantAPIsConfigBuilder(D theContext)
{
this._TheContext = theContext;
}

#region Table Inclusion/Exclusion

/// <summary>
/// Specify individual tables to include in the API generation with the methods requested
/// </summary>
/// <param name="entitySelector">Select the EntityFramework DbSet to include - Required</param>
/// <param name="methodsToGenerate">A flags enumerable indicating the methods to generate. By default ALL are generated</param>
/// <returns>Configuration builder with this configuration applied</returns>
public InstantAPIsConfigBuilder<D> IncludeTable<T>(Func<D, DbSet<T>> entitySelector, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All) where T : class
{

var theSetType = entitySelector(_TheContext).GetType().BaseType;
var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType);

var tableApiMapping = new TableApiMapping(property.Name, methodsToGenerate);
_IncludedTables.Add(tableApiMapping);

if (_ExcludedTables.Contains(tableApiMapping.TableName)) _ExcludedTables.Remove(tableApiMapping.TableName);
_IncludedTables.Add(tableApiMapping);

return this;

}

/// <summary>
/// Exclude individual tables from the API generation. Exclusion takes priority over inclusion
/// </summary>
/// <param name="entitySelector">Select the entity to exclude from generation</param>
/// <returns>Configuration builder with this configuraiton applied</returns>
public InstantAPIsConfigBuilder<D> ExcludeTable<T>(Func<D, DbSet<T>> entitySelector) where T : class
{

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));
_ExcludedTables.Add(property.Name);

return this;

}

private void BuildTables()
{

var tables = WebApplicationExtensions.GetDbTablesForContext<D>().ToArray();

if (!_IncludedTables.Any() && !_ExcludedTables.Any())
{
_Config.Tables.UnionWith(tables.Select(t => new WebApplicationExtensions.TypeTable
{
Name = t.Name,
InstanceType = t.InstanceType,
ApiMethodsToGenerate = ApiMethodsToGenerate.All
}));
return;
}

// Add the Included tables
var outTables = tables.Where(t => _IncludedTables.Any(i => i.TableName.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)))
.Select(t => new WebApplicationExtensions.TypeTable
{
Name = t.Name,
InstanceType = t.InstanceType,
ApiMethodsToGenerate = _IncludedTables.First(i => i.TableName.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).MethodsToGenerate
}).ToArray();

// If no tables were added, added them all
if (outTables.Length == 0)
{
outTables = tables.Select(t => new WebApplicationExtensions.TypeTable
{
Name = t.Name,
InstanceType = t.InstanceType
}).ToArray();
}

// Remove the Excluded tables
outTables = outTables.Where(t => !_ExcludedTables.Any(e => t.Name.Equals(e, StringComparison.InvariantCultureIgnoreCase))).ToArray();

if (outTables == null || !outTables.Any()) throw new ArgumentException("All tables were excluded from this configuration");

_Config.Tables.UnionWith(outTables);

}

#endregion

internal InstantAPIsConfig Build()
{

BuildTables();

return _Config;
}

}
5 changes: 5 additions & 0 deletions Fritz.InstantAPIs/MapApiExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ internal class MapApiExtensions

// TODO: Authentication / Authorization

[ApiMethod(ApiMethodsToGenerate.Get)]
internal static void MapInstantGetAll<D, C>(IEndpointRouteBuilder app, string url)
where D : DbContext where C : class
{
Expand All @@ -22,6 +23,7 @@ internal class MapApiExtensions

}

[ApiMethod(ApiMethodsToGenerate.GetById)]
internal static void MapGetById<D,C>(IEndpointRouteBuilder app, string url)
where D: DbContext where C : class
{
Expand Down Expand Up @@ -49,6 +51,7 @@ internal class MapApiExtensions

}

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

}

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

}

[ApiMethod(ApiMethodsToGenerate.Delete)]
internal static void MapDeleteById<D, C>(IEndpointRouteBuilder app, string url)
where D : DbContext where C : class
{
Expand Down
45 changes: 33 additions & 12 deletions Fritz.InstantAPIs/WebApplicationExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,39 +1,51 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using System.Reflection;

namespace Microsoft.AspNetCore.Builder;

public static class WebApplicationExtensions
{

public static InstantAPIsConfig Configuration { get; set; } = new();
private static InstantAPIsConfig Configuration { get; set; } = new();

public static IEndpointRouteBuilder MapInstantAPIs<D>(this IEndpointRouteBuilder app, Action<InstantAPIsConfig> configAction = null) where D: DbContext
public static IEndpointRouteBuilder MapInstantAPIs<D>(this IEndpointRouteBuilder app, Action<InstantAPIsConfigBuilder<D>> options = null) where D: DbContext
{

if (configAction != null) configAction(Configuration);
if (app is IApplicationBuilder applicationBuilder)
{
var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetService(typeof(D)) as D;
var builder = new InstantAPIsConfigBuilder<D>(ctx);
if (options != null)
{
options(builder);
Configuration = builder.Build();
}
}

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

var requestedTables = Configuration?.Tables ?? InstantAPIsConfig.DefaultTables;
var requestedTables = !Configuration.Tables.Any() ?
dbTables :
Configuration.Tables.Where(t => dbTables.Any(db => db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray();

foreach (var table in dbTables.Where(
x => (requestedTables == InstantAPIsConfig.DefaultTables) || requestedTables.Any(t => t.Equals(x.Name, StringComparison.InvariantCultureIgnoreCase))
)
)
var allMethods = typeof(MapApiExtensions).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(m => m.Name.StartsWith("Map")).ToArray();
foreach (var table in requestedTables)
{

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

// The remaining private static methods in this class build out the Mapped API methods..
// let's use some reflection to get them
var allMethods = typeof(MapApiExtensions).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(m => m.Name.StartsWith("Map"));
foreach (var method in allMethods)
{

var sigAttr = method.CustomAttributes.First(x => x.AttributeType == typeof(ApiMethodAttribute)).ConstructorArguments.First();
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, url });
}
Expand All @@ -43,10 +55,19 @@ public static class WebApplicationExtensions
return app;
}

internal static IEnumerable<TypeTable> GetDbTablesForContext<D>() where D : DbContext
{
return typeof(D).GetProperties(BindingFlags.Instance | BindingFlags.Public)
.Where(x => x.PropertyType.FullName.StartsWith("Microsoft.EntityFrameworkCore.DbSet"))
.Select(x => new TypeTable { Name = x.Name, InstanceType = 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;
}

}
12 changes: 6 additions & 6 deletions Test/BaseFixture.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
using Moq;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Options;
using Moq;

namespace Test;

public abstract class BaseFixture
{
public BaseFixture()
{
Mockery = new MockRepository(MockBehavior.Loose);
}

protected MockRepository Mockery { get; private set; }
protected MockRepository Mockery { get; private set; } = new MockRepository(MockBehavior.Loose);

}
Loading

0 comments on commit 0846f32

Please sign in to comment.