Skip to content

Commit 3ebe5d5

Browse files
author
Dries Verbeke
committed
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
1 parent dab302c commit 3ebe5d5

File tree

9 files changed

+184
-52
lines changed

9 files changed

+184
-52
lines changed

InstantAPIs/InstantAPIsBuilder.cs

Lines changed: 75 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
1-
namespace Microsoft.AspNetCore.Builder;
1+
using System.Linq.Expressions;
2+
using System.Reflection;
23

3-
public class InstantAPIsBuilder<D> where D : DbContext
4+
namespace Microsoft.AspNetCore.Builder;
5+
6+
public class InstantAPIsBuilder<TContext>
7+
where TContext : DbContext
48
{
59

6-
private HashSet<InstantAPIsOptions.Table> _Config = new();
7-
private Type _ContextType = typeof(D);
8-
private D _TheContext;
9-
private readonly HashSet<InstantAPIsOptions.Table> _IncludedTables = new();
10+
private HashSet<InstantAPIsOptions.ITable> _Config = new();
11+
private Type _ContextType = typeof(TContext);
12+
private TContext _TheContext;
13+
private readonly HashSet<InstantAPIsOptions.ITable> _IncludedTables = new();
1014
private readonly List<string> _ExcludedTables = new();
1115
private const string DEFAULT_URI = "/api/";
1216

13-
public InstantAPIsBuilder(D theContext)
17+
public InstantAPIsBuilder(TContext theContext)
1418
{
1519
this._TheContext = theContext;
1620
}
@@ -20,13 +24,16 @@ public InstantAPIsBuilder(D theContext)
2024
/// <summary>
2125
/// Specify individual tables to include in the API generation with the methods requested
2226
/// </summary>
23-
/// <param name="entitySelector">Select the EntityFramework DbSet to include - Required</param>
27+
/// <param name="setSelector">Select the EntityFramework DbSet to include - Required</param>
2428
/// <param name="methodsToGenerate">A flags enumerable indicating the methods to generate. By default ALL are generated</param>
2529
/// <returns>Configuration builder with this configuration applied</returns>
26-
public InstantAPIsBuilder<D> IncludeTable<T>(Func<D, DbSet<T>> entitySelector, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All, string baseUrl = "") where T : class
30+
public InstantAPIsBuilder<TContext> IncludeTable<TSet, TEntity, TKey>(Expression<Func<TContext, TSet>> setSelector,
31+
InstantAPIsOptions.TableOptions<TEntity, TKey> config, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All, string baseUrl = "")
32+
where TSet : DbSet<TEntity>
33+
where TEntity : class
2734
{
2835

29-
var theSetType = entitySelector(_TheContext).GetType().BaseType;
36+
var theSetType = setSelector.Compile()(_TheContext).GetType().BaseType;
3037
var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType);
3138

3239
if (!string.IsNullOrEmpty(baseUrl))
@@ -46,7 +53,10 @@ public InstantAPIsBuilder<D> IncludeTable<T>(Func<D, DbSet<T>> entitySelector, A
4653
baseUrl = string.Concat(DEFAULT_URI, property.Name);
4754
}
4855

49-
var tableApiMapping = new InstantAPIsOptions.Table(property.Name, new Uri(baseUrl), typeof(T)) { ApiMethodsToGenerate = methodsToGenerate };
56+
var tableApiMapping = new InstantAPIsOptions.Table<TContext, TSet, TEntity, TKey>(property.Name, new Uri(baseUrl, UriKind.Relative), setSelector, config)
57+
{
58+
ApiMethodsToGenerate = methodsToGenerate
59+
};
5060
_IncludedTables.Add(tableApiMapping);
5161

5262
if (_ExcludedTables.Contains(tableApiMapping.Name)) _ExcludedTables.Remove(tableApiMapping.Name);
@@ -61,7 +71,7 @@ public InstantAPIsBuilder<D> IncludeTable<T>(Func<D, DbSet<T>> entitySelector, A
6171
/// </summary>
6272
/// <param name="entitySelector">Select the entity to exclude from generation</param>
6373
/// <returns>Configuration builder with this configuraiton applied</returns>
64-
public InstantAPIsBuilder<D> ExcludeTable<T>(Func<D, DbSet<T>> entitySelector) where T : class
74+
public InstantAPIsBuilder<TContext> ExcludeTable<T>(Func<TContext, DbSet<T>> entitySelector) where T : class
6575
{
6676

6777
var theSetType = entitySelector(_TheContext).GetType().BaseType;
@@ -76,20 +86,28 @@ public InstantAPIsBuilder<D> ExcludeTable<T>(Func<D, DbSet<T>> entitySelector) w
7686

7787
private void BuildTables()
7888
{
79-
80-
var tables = WebApplicationExtensions.GetDbTablesForContext<D>().ToArray();
81-
InstantAPIsOptions.Table[]? outTables;
89+
var tables = WebApplicationExtensions.GetDbTablesForContext<TContext>().ToArray();
90+
InstantAPIsOptions.ITable[]? outTables;
8291

8392
// Add the Included tables
8493
if (_IncludedTables.Any())
8594
{
8695
outTables = tables.Where(t => _IncludedTables.Any(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)))
87-
.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)
88-
{
89-
ApiMethodsToGenerate = _IncludedTables.First(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).ApiMethodsToGenerate
90-
}).ToArray();
96+
.Select(t => {
97+
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);
98+
if (table != null)
99+
{
100+
table.ApiMethodsToGenerate = _IncludedTables.First(i => i.Name.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).ApiMethodsToGenerate;
101+
}
102+
return table;
103+
})
104+
.Where(x => x != null).OfType<InstantAPIsOptions.ITable>()
105+
.ToArray();
91106
} else {
92-
outTables = tables.Select(t => new InstantAPIsOptions.Table(t.Name, new Uri(DEFAULT_URI + t.Name, uriKind: UriKind.Relative), t.InstanceType)).ToArray();
107+
outTables = tables
108+
.Select(t => CreateTable(t.Name, new Uri(DEFAULT_URI + t.Name, uriKind: UriKind.Relative), typeof(TContext), typeof(DbSet<>).MakeGenericType(t.InstanceType), t.InstanceType))
109+
.Where(x => x != null).OfType<InstantAPIsOptions.ITable>()
110+
.ToArray();
93111
}
94112

95113
// Exit now if no tables were excluded
@@ -108,9 +126,44 @@ private void BuildTables()
108126

109127
}
110128

111-
#endregion
129+
public static InstantAPIsOptions.ITable? CreateTable(string name, Uri baseUrl, Type contextType, Type setType, Type entityType)
130+
{
131+
var keyProperty = entityType.GetProperties().Where(x => "id".Equals(x.Name, StringComparison.InvariantCultureIgnoreCase)).FirstOrDefault();
132+
if (keyProperty == null) return null;
133+
134+
var genericMethod = typeof(InstantAPIsBuilder<>).MakeGenericType(contextType).GetMethod(nameof(CreateTableGeneric), BindingFlags.NonPublic | BindingFlags.Static)
135+
?? throw new Exception("Missing method");
136+
var concreteMethod = genericMethod.MakeGenericMethod(contextType, setType, entityType, keyProperty.PropertyType);
137+
138+
var entitySelector = CreateExpression(contextType, name, setType);
139+
var keySelector = CreateExpression(entityType, keyProperty.Name, keyProperty.PropertyType);
140+
return concreteMethod.Invoke(null, new object?[] { name, baseUrl, entitySelector, keySelector, null }) as InstantAPIsOptions.ITable;
141+
}
142+
143+
private static object CreateExpression(Type memberOwnerType, string property, Type returnType)
144+
{
145+
var parameterExpression = Expression.Parameter(memberOwnerType, "x");
146+
var propertyExpression = Expression.Property(parameterExpression, property);
147+
//var block = Expression.Block(propertyExpression, returnExpression);
148+
return Expression.Lambda(typeof(Func<,>).MakeGenericType(memberOwnerType, returnType), propertyExpression, parameterExpression);
149+
}
150+
151+
private static InstantAPIsOptions.ITable CreateTableGeneric<TContextStatic, TSet, TEntity, TKey>(string name, Uri baseUrl,
152+
Expression<Func<TContextStatic, TSet>> entitySelector, Expression<Func<TEntity, TKey>>? keySelector, Expression<Func<TEntity, TKey>>? orderBy)
153+
where TContextStatic : class
154+
where TSet : class
155+
where TEntity : class
156+
{
157+
return new InstantAPIsOptions.Table<TContextStatic, TSet, TEntity, TKey>(name, baseUrl, entitySelector,
158+
new InstantAPIsOptions.TableOptions<TEntity, TKey>()
159+
{
160+
KeySelector = keySelector,
161+
OrderBy = orderBy
162+
});
163+
}
164+
#endregion
112165

113-
internal HashSet<InstantAPIsOptions.Table> Build()
166+
internal HashSet<InstantAPIsOptions.ITable> Build()
114167
{
115168

116169
BuildTables();

InstantAPIs/InstantAPIsOptions.cs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using Swashbuckle.AspNetCore.SwaggerGen;
2+
using System.Linq.Expressions;
23

34
namespace InstantAPIs;
45

@@ -14,4 +15,52 @@ public class InstantAPIsOptions
1415

1516
public EnableSwagger? EnableSwagger { get; set; }
1617
public Action<SwaggerGenOptions>? Swagger { get; set; }
18+
19+
internal class Table<TContext, TSet, TEntity, TKey>
20+
: ITable
21+
{
22+
public Table(string name, Uri baseUrl, Expression<Func<TContext, TSet>> entitySelector, TableOptions<TEntity, TKey> config)
23+
{
24+
Name = name;
25+
BaseUrl = baseUrl;
26+
EntitySelector = entitySelector;
27+
Config = config;
28+
29+
RepoType = typeof(TContext);
30+
InstanceType = typeof(TEntity);
31+
}
32+
33+
public string Name { get; }
34+
public Type RepoType { get; }
35+
public Type InstanceType { get; }
36+
public Uri BaseUrl { get; set; }
37+
38+
public Expression<Func<TContext, TSet>> EntitySelector { get; }
39+
public TableOptions<TEntity, TKey> Config { get; }
40+
41+
public ApiMethodsToGenerate ApiMethodsToGenerate { get; set; } = ApiMethodsToGenerate.All;
42+
43+
public object EntitySelectorObject => EntitySelector;
44+
public object ConfigObject => Config;
45+
}
46+
47+
public interface ITable
48+
{
49+
public string Name { get; }
50+
public Type RepoType { get; }
51+
public Type InstanceType { get; }
52+
public Uri BaseUrl { get; set; }
53+
public ApiMethodsToGenerate ApiMethodsToGenerate { get; set; }
54+
55+
public object EntitySelectorObject { get; }
56+
public object ConfigObject { get; }
57+
58+
}
59+
60+
public record TableOptions<TEntity, TKey>()
61+
{
62+
public Expression<Func<TEntity, TKey>>? KeySelector { get; set; }
63+
64+
public Expression<Func<TEntity, TKey>>? OrderBy { get; set; }
65+
}
1766
}

InstantAPIs/JsonAPIsConfig.cs

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
using System.Linq;
1+
using Microsoft.Extensions.Options;
22
using System.Text.Json.Nodes;
33

44
namespace InstantAPIs;
55

66
internal class JsonAPIsConfig
77
{
88

9-
internal HashSet<InstantAPIsOptions.Table> Tables { get; } = new HashSet<InstantAPIsOptions.Table>();
9+
internal HashSet<InstantAPIsOptions.ITable> Tables { get; } = new HashSet<InstantAPIsOptions.ITable>();
1010

1111
internal string JsonFilename = "mock.json";
1212

@@ -18,7 +18,7 @@ public class JsonAPIsConfigBuilder
1818

1919
private JsonAPIsConfig _Config = new();
2020
private string? _FileName;
21-
private readonly HashSet<InstantAPIsOptions.Table> _IncludedTables = new();
21+
private readonly HashSet<InstantAPIsOptions.ITable> _IncludedTables = new();
2222
private readonly List<string> _ExcludedTables = new();
2323

2424
public JsonAPIsConfigBuilder SetFilename(string fileName)
@@ -38,7 +38,8 @@ public JsonAPIsConfigBuilder SetFilename(string fileName)
3838
public JsonAPIsConfigBuilder IncludeEntity(string entityName, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All)
3939
{
4040

41-
var tableApiMapping = new InstantAPIsOptions.Table(entityName, new Uri(entityName, UriKind.Relative), typeof(JsonObject)) { ApiMethodsToGenerate = methodsToGenerate };
41+
var tableApiMapping = new InstantAPIsOptions.Table<JsonContext, JsonArray, JsonObject, int>(entityName, new Uri(entityName, UriKind.Relative), c => c.LoadTable(entityName),
42+
new InstantAPIsOptions.TableOptions<JsonObject, int>()) { ApiMethodsToGenerate = methodsToGenerate };
4243
_IncludedTables.Add(tableApiMapping);
4344

4445
if (_ExcludedTables.Contains(entityName)) _ExcludedTables.Remove(tableApiMapping.Name);
@@ -77,7 +78,8 @@ private void BuildTables()
7778
{
7879

7980
var tables = IdentifyEntities()
80-
.Select(t => new InstantAPIsOptions.Table(t, new Uri(t, UriKind.Relative), typeof(JsonObject))
81+
.Select(t => new InstantAPIsOptions.Table<JsonContext, JsonArray, JsonObject, int>(t, new Uri(t, UriKind.Relative), c => c.LoadTable(t),
82+
new InstantAPIsOptions.TableOptions<JsonObject, int>())
8183
{
8284
ApiMethodsToGenerate = ApiMethodsToGenerate.All
8385
});
@@ -121,4 +123,27 @@ internal JsonAPIsConfig Build()
121123
return _Config;
122124
}
123125

126+
public class JsonContext
127+
{
128+
const string JSON_FILENAME = "mock.json";
129+
private readonly JsonNode _writableDoc;
130+
131+
public JsonContext()
132+
{
133+
_writableDoc = JsonNode.Parse(File.ReadAllText(JSON_FILENAME))
134+
?? throw new Exception("Invalid json file");
135+
}
136+
137+
public JsonArray LoadTable(string name)
138+
{
139+
return _writableDoc?.Root.AsObject().AsEnumerable().First(elem => elem.Key == name).Value as JsonArray
140+
?? throw new Exception("No json array");
141+
}
142+
143+
internal void SaveChanges()
144+
{
145+
File.WriteAllText(JSON_FILENAME, _writableDoc.ToString());
146+
}
147+
}
148+
124149
}

InstantAPIs/WebApplicationExtensions.cs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using Microsoft.Extensions.Logging;
66
using Microsoft.Extensions.Logging.Abstractions;
77
using Microsoft.Extensions.Options;
8+
using System.Linq;
89
using System.Reflection;
910

1011
namespace InstantAPIs;
@@ -14,28 +15,28 @@ public static class WebApplicationExtensions
1415

1516
internal const string LOGGER_CATEGORY_NAME = "InstantAPI";
1617

17-
private static HashSet<InstantAPIsOptions.Table> Configuration { get; set; } = new();
18+
private static HashSet<InstantAPIsOptions.ITable> Configuration { get; set; } = new();
1819

19-
public static IEndpointRouteBuilder MapInstantAPIs<D>(this IEndpointRouteBuilder app, Action<InstantAPIsBuilder<D>>? options = null) where D : DbContext
20+
public static IEndpointRouteBuilder MapInstantAPIs<TContext>(this IEndpointRouteBuilder app, Action<InstantAPIsBuilder<TContext>>? options = null) where TContext : DbContext
2021
{
2122
if (app is IApplicationBuilder applicationBuilder)
2223
{
2324
AddOpenAPIConfiguration(app, options, applicationBuilder);
2425
}
2526

2627
// Get the tables on the DbContext
27-
var dbTables = GetDbTablesForContext<D>();
28+
var dbTables = GetDbTablesForContext<TContext>();
2829

2930
var requestedTables = !Configuration.Any() ?
3031
dbTables :
3132
Configuration.Where(t => dbTables.Any(db => db.Name != null && db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray();
3233

33-
MapInstantAPIsUsingReflection<D>(app, requestedTables);
34+
MapInstantAPIsUsingReflection<TContext>(app, requestedTables);
3435

3536
return app;
3637
}
3738

38-
private static void MapInstantAPIsUsingReflection<D>(IEndpointRouteBuilder app, IEnumerable<InstantAPIsOptions.Table> requestedTables) where D : DbContext
39+
private static void MapInstantAPIsUsingReflection<TContext>(IEndpointRouteBuilder app, IEnumerable<InstantAPIsOptions.ITable> requestedTables) where TContext : DbContext
3940
{
4041

4142
ILogger logger = NullLogger.Instance;
@@ -54,7 +55,7 @@ private static void MapInstantAPIsUsingReflection<D>(IEndpointRouteBuilder app,
5455
// The default URL for an InstantAPI is /api/TABLENAME
5556
//var url = $"/api/{table.Name}";
5657

57-
initialize.MakeGenericMethod(typeof(D), table.InstanceType ?? throw new Exception($"Instance type for table {table.Name} is null")).Invoke(null, new[] { logger });
58+
initialize.MakeGenericMethod(typeof(TContext), table.InstanceType ?? throw new Exception($"Instance type for table {table.Name} is null")).Invoke(null, new[] { logger });
5859

5960
// The remaining private static methods in this class build out the Mapped API methods..
6061
// let's use some reflection to get them
@@ -66,14 +67,14 @@ private static void MapInstantAPIsUsingReflection<D>(IEndpointRouteBuilder app,
6667
var methodType = (ApiMethodsToGenerate)sigAttr.Value;
6768
if ((table.ApiMethodsToGenerate & methodType) != methodType) continue;
6869

69-
var genericMethod = method.MakeGenericMethod(typeof(D), table.InstanceType);
70+
var genericMethod = method.MakeGenericMethod(typeof(TContext), table.InstanceType);
7071
genericMethod.Invoke(null, new object[] { app, table.BaseUrl?.ToString() ?? throw new Exception($"BaseUrl for {table.Name} is null") });
7172
}
7273

7374
}
7475
}
7576

76-
private static void AddOpenAPIConfiguration<D>(IEndpointRouteBuilder app, Action<InstantAPIsBuilder<D>>? options, IApplicationBuilder applicationBuilder) where D : DbContext
77+
private static void AddOpenAPIConfiguration<TContext>(IEndpointRouteBuilder app, Action<InstantAPIsBuilder<TContext>>? options, IApplicationBuilder applicationBuilder) where TContext : DbContext
7778
{
7879
// Check if AddInstantAPIs was called by getting the service options and evaluate EnableSwagger property
7980
var serviceOptions = applicationBuilder.ApplicationServices.GetRequiredService<IOptions<InstantAPIsOptions>>().Value;
@@ -90,21 +91,22 @@ private static void AddOpenAPIConfiguration<D>(IEndpointRouteBuilder app, Action
9091
applicationBuilder.UseSwaggerUI();
9192
}
9293

93-
var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService<D>();
94-
var builder = new InstantAPIsBuilder<D>(ctx);
94+
var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetRequiredService<TContext>();
95+
var builder = new InstantAPIsBuilder<TContext>(ctx);
9596
if (options != null)
9697
{
9798
options(builder);
9899
Configuration = builder.Build();
99100
}
100101
}
101102

102-
internal static IEnumerable<InstantAPIsOptions.Table> GetDbTablesForContext<D>() where D : DbContext
103+
internal static IEnumerable<InstantAPIsOptions.ITable> GetDbTablesForContext<TContext>() where TContext : DbContext
103104
{
104-
return typeof(D).GetProperties(BindingFlags.Instance | BindingFlags.Public)
105-
.Where(x => (x.PropertyType.FullName?.StartsWith("Microsoft.EntityFrameworkCore.DbSet") ?? false)
106-
&& x.PropertyType.GenericTypeArguments.First().GetCustomAttributes(typeof(KeylessAttribute), true).Length <= 0)
107-
.Select(x => new InstantAPIsOptions.Table(x.Name, new Uri($"/api/{x.Name}", uriKind: UriKind.RelativeOrAbsolute), x.PropertyType.GenericTypeArguments.First()))
105+
return typeof(TContext).GetProperties(BindingFlags.Instance | BindingFlags.Public)
106+
.Where(x => (x.PropertyType.FullName?.StartsWith("Microsoft.EntityFrameworkCore.DbSet") ?? false)
107+
&& x.PropertyType.GenericTypeArguments.First().GetCustomAttributes(typeof(KeylessAttribute), true).Length <= 0)
108+
.Select(x => InstantAPIsBuilder<TContext>.CreateTable(x.Name, new Uri($"/api/{x.Name}", uriKind: UriKind.RelativeOrAbsolute), typeof(TContext), x.PropertyType, x.PropertyType.GenericTypeArguments.First()))
109+
.Where(x => x != null).OfType<InstantAPIsOptions.ITable>()
108110
.ToArray();
109111
}
110112

Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Xunit;
1+
using InstantAPIs;
2+
using Xunit;
23

34
namespace Test.Configuration;
45

@@ -12,7 +13,7 @@ public void ShouldSpecifyDefaultUrl()
1213
// arrange
1314

1415
// act
15-
_Builder.IncludeTable(db => db.Contacts);
16+
_Builder.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions<Contact, int>());
1617
var config = _Builder.Build();
1718

1819
// assert

0 commit comments

Comments
 (0)