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/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
new file mode 100644
index 0000000..8553cf8
--- /dev/null
+++ b/InstantAPIs/InstantAPIsBuilder.cs
@@ -0,0 +1,110 @@
+using InstantAPIs.Repositories;
+using System.Linq.Expressions;
+
+namespace Microsoft.AspNetCore.Builder;
+
+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();
+
+ public InstantAPIsBuilder(InstantAPIsOptions instantApiOptions, IContextHelper contextFactory)
+ {
+ _instantApiOptions = instantApiOptions;
+ _contextFactory = contextFactory;
+ }
+
+ private IEnumerable DiscoverTables()
+ {
+ return _contextFactory != null
+ ? _contextFactory.DiscoverFromContext(_instantApiOptions.DefaultUri)
+ : Array.Empty();
+ }
+
+ #region Table Inclusion/Exclusion
+
+ ///
+ /// Specify individual tables to include in the API generation with the methods requested
+ ///
+ /// 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 : class
+ where TEntity : class
+ {
+ var propertyName = _contextFactory.NameTable(setSelector);
+
+ if (!string.IsNullOrEmpty(baseUrl))
+ {
+ try
+ {
+ var testUri = new Uri(baseUrl, UriKind.RelativeOrAbsolute);
+ baseUrl = testUri.IsAbsoluteUri ? testUri.LocalPath : baseUrl;
+ }
+ catch
+ {
+ throw new ArgumentException(nameof(baseUrl), "Not a valid Uri");
+ }
+ }
+ else
+ {
+ baseUrl = string.Concat(_instantApiOptions.DefaultUri.ToString(), "/", propertyName);
+ }
+
+ var tableApiMapping = new InstantAPIsOptions.Table(propertyName, new Uri(baseUrl, UriKind.Relative), setSelector, config)
+ {
+ ApiMethodsToGenerate = methodsToGenerate
+ };
+
+ _tables.RemoveWhere(x => x.Name == tableApiMapping.Name);
+ _tables.Add(tableApiMapping);
+
+ return this;
+
+ }
+
+ ///
+ /// Exclude individual tables from the API generation. Exclusion takes priority over inclusion
+ ///
+ /// Select the entity to exclude from generation
+ /// Configuration builder with this configuraiton applied
+ public InstantAPIsBuilder ExcludeTable(Expression> setSelector) where TSet : class
+ {
+ var propertyName = _contextFactory.NameTable(setSelector);
+ _excludedTables.Add(propertyName);
+
+ return this;
+ }
+
+ private void BuildTables()
+ {
+ if (!_tables.Any())
+ {
+ var discoveredTables = DiscoverTables();
+ foreach (var discoveredTable in discoveredTables)
+ {
+ _tables.Add(discoveredTable);
+ }
+ }
+
+ _tables.RemoveWhere(t => _excludedTables.Any(e => t.Name.Equals(e, StringComparison.InvariantCultureIgnoreCase)));
+
+ if (!_tables.Any()) throw new ArgumentException("All tables were excluded from this configuration");
+ }
+
+ #endregion
+
+ internal IEnumerable Build()
+ {
+ BuildTables();
+
+ return _tables;
+ }
+
+}
diff --git a/InstantAPIs/InstantAPIsConfig.cs b/InstantAPIs/InstantAPIsConfig.cs
deleted file mode 100644
index f7ce505..0000000
--- a/InstantAPIs/InstantAPIsConfig.cs
+++ /dev/null
@@ -1,137 +0,0 @@
-namespace InstantAPIs;
-
-internal class InstantAPIsConfig
-{
-
- internal HashSet Tables { get; } = new HashSet();
-
-}
-
-
-public class InstantAPIsConfigBuilder where D : DbContext
-{
-
- private InstantAPIsConfig _Config = new();
- private Type _ContextType = typeof(D);
- private D _TheContext;
- private readonly HashSet _IncludedTables = new();
- private readonly List _ExcludedTables = new();
- private const string DEFAULT_URI = "/api/";
-
- public InstantAPIsConfigBuilder(D theContext)
- {
- this._TheContext = theContext;
- }
-
- #region Table Inclusion/Exclusion
-
- ///
- /// Specify individual tables to include in the API generation with the methods requested
- ///
- /// 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
- {
-
- var theSetType = entitySelector(_TheContext).GetType().BaseType;
- var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType);
-
- if (!string.IsNullOrEmpty(baseUrl))
- {
- try
- {
- var testUri = new Uri(baseUrl, UriKind.RelativeOrAbsolute);
- baseUrl = testUri.IsAbsoluteUri ? testUri.LocalPath : baseUrl;
- }
- catch
- {
- throw new ArgumentException(nameof(baseUrl), "Not a valid Uri");
- }
- }
- else
- {
- baseUrl = String.Concat(DEFAULT_URI, property.Name);
- }
-
- var tableApiMapping = new TableApiMapping(property.Name, methodsToGenerate, baseUrl);
- _IncludedTables.Add(tableApiMapping);
-
- if (_ExcludedTables.Contains(tableApiMapping.TableName)) _ExcludedTables.Remove(tableApiMapping.TableName);
- _IncludedTables.Add(tableApiMapping);
-
- return this;
-
- }
-
- ///
- /// Exclude individual tables from the API generation. Exclusion takes priority over inclusion
- ///
- /// Select the entity to exclude from generation
- /// Configuration builder with this configuraiton applied
- public InstantAPIsConfigBuilder ExcludeTable(Func> 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().ToArray();
- WebApplicationExtensions.TypeTable[]? 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
- {
- 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)
- }).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();
- }
-
- // Exit now if no tables were excluded
- if (!_ExcludedTables.Any())
- {
- _Config.Tables.UnionWith(outTables);
- return;
- }
-
- // 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;
- }
-
-}
\ No newline at end of file
diff --git a/InstantAPIs/InstantAPIsOptions.cs b/InstantAPIs/InstantAPIsOptions.cs
new file mode 100644
index 0000000..e8f520c
--- /dev/null
+++ b/InstantAPIs/InstantAPIsOptions.cs
@@ -0,0 +1,69 @@
+using Swashbuckle.AspNetCore.SwaggerGen;
+using System.Linq.Expressions;
+
+namespace InstantAPIs;
+
+public enum EnableSwagger
+{
+ None,
+ DevelopmentOnly,
+ Always
+}
+
+public class InstantAPIsOptions
+{
+ public Uri DefaultUri = new Uri("/api", UriKind.Relative);
+
+ public EnableSwagger? EnableSwagger { get; set; }
+ public Action? Swagger { get; set; }
+
+ public IEnumerable Tables { get; internal set; } = new HashSet();
+
+ 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/InstantAPIsServiceCollectionExtensions.cs b/InstantAPIs/InstantAPIsServiceCollectionExtensions.cs
index c2ed9cb..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 InstantAPIsServiceOptions();
-
- // 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/InstantAPIsServiceOptions.cs b/InstantAPIs/InstantAPIsServiceOptions.cs
deleted file mode 100644
index 6c540ce..0000000
--- a/InstantAPIs/InstantAPIsServiceOptions.cs
+++ /dev/null
@@ -1,17 +0,0 @@
-using Swashbuckle.AspNetCore.SwaggerGen;
-
-namespace InstantAPIs;
-
-public enum EnableSwagger
-{
- None,
- DevelopmentOnly,
- Always
-}
-
-public class InstantAPIsServiceOptions
-{
-
- public EnableSwagger? EnableSwagger { get; set; }
- public Action? Swagger { get; set; }
-}
\ No newline at end of file
diff --git a/InstantAPIs/JsonAPIsConfig.cs b/InstantAPIs/JsonAPIsConfig.cs
deleted file mode 100644
index 6c4a90f..0000000
--- a/InstantAPIs/JsonAPIsConfig.cs
+++ /dev/null
@@ -1,131 +0,0 @@
-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 TableApiMapping(entityName, methodsToGenerate);
- _IncludedTables.Add(tableApiMapping);
-
- if (_ExcludedTables.Contains(entityName)) _ExcludedTables.Remove(tableApiMapping.TableName);
-
- 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.TableName).Contains(entityName)) _IncludedTables.Remove(_IncludedTables.First(t => t.TableName == entityName));
- _ExcludedTables.Add(entityName);
-
- return this;
-
- }
-
- private HashSet IdentifyEntities()
- {
- var writableDoc = JsonNode.Parse(File.ReadAllText(_FileName));
-
- // print API
- return writableDoc?.Root.AsObject()
- .AsEnumerable().Select(x => x.Key)
- .ToHashSet();
-
- }
-
- private void BuildTables()
- {
-
- var tables = IdentifyEntities();
-
- if (!_IncludedTables.Any() && !_ExcludedTables.Any())
- {
- _Config.Tables.UnionWith(tables.Select(t => new WebApplicationExtensions.TypeTable
- {
- Name = t,
- ApiMethodsToGenerate = ApiMethodsToGenerate.All
- }));
- return;
- }
-
- // Add the Included tables
- var outTables = _IncludedTables
- .Select(t => new WebApplicationExtensions.TypeTable
- {
- Name = t.TableName,
- ApiMethodsToGenerate = t.MethodsToGenerate
- }).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();
- }
-
- // 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 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;
- }
-
-}
\ No newline at end of file
diff --git a/InstantAPIs/JsonApiExtensions.cs b/InstantAPIs/JsonApiExtensions.cs
deleted file mode 100644
index 5e2b740..0000000
--- a/InstantAPIs/JsonApiExtensions.cs
+++ /dev/null
@@ -1,129 +0,0 @@
-using Microsoft.AspNetCore.Builder;
-using Microsoft.AspNetCore.Http;
-using System.Text.Json.Nodes;
-
-namespace InstantAPIs;
-
-public static class JsonApiExtensions
-{
-
- static JsonAPIsConfig _Config;
-
- public static WebApplication UseJsonRoutes(this WebApplication app, Action options = null)
- {
-
- var builder = new JsonAPIsConfigBuilder();
- _Config = new JsonAPIsConfig();
- 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) =>
- {
- 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;
- }
-}
\ No newline at end of file
diff --git a/InstantAPIs/MapApiExtensions.cs b/InstantAPIs/MapApiExtensions.cs
index 2df9539..468687f 100644
--- a/InstantAPIs/MapApiExtensions.cs
+++ b/InstantAPIs/MapApiExtensions.cs
@@ -2,153 +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;
-
- 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) =>
{
-
- C 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);
- return Results.Created($"{url}/{id.ToString()}", 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..db6c305
--- /dev/null
+++ b/InstantAPIs/Repositories/Json/RepositoryHelper.cs
@@ -0,0 +1,92 @@
+using Microsoft.AspNetCore.Http;
+using System.Text.Json.Nodes;
+
+namespace InstantAPIs.Repositories.Json;
+
+internal class RepositoryHelper :
+ IRepositoryHelper
+{
+ private readonly Func _setSelector;
+
+ public RepositoryHelper(Func setSelector, InstantAPIsOptions.TableOptions config)
+ {
+ _setSelector = setSelector;
+ }
+
+ 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 != null && row
+ .AsObject()
+ .Any(o => o.Key.ToLower() == "id" && o.Value?.GetValue() == id)
+ )?.AsObject();
+ return Task.FromResult(matchedItem);
+ }
+
+
+ public Task Insert(HttpRequest request, Context context, string name, JsonObject newObj, CancellationToken cancellationToken)
+ {
+
+ var array = context.LoadTable(name);
+ 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();
+
+ return Task.FromResult(key);
+ }
+
+ public Task Update(HttpRequest request, Context context, string name, int id, JsonObject newObj, CancellationToken cancellationToken)
+ {
+ var array = context.LoadTable(name);
+ 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;
+ }
+
+ 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 == null
+ ? false
+ : row.value.AsObject().Any(o => o.Key.ToLower() == "id" && o.Value?.GetValue() == id));
+ 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..01a45b5
--- /dev/null
+++ b/InstantAPIs/Repositories/Json/RepositoryHelperFactory.cs
@@ -0,0 +1,21 @@
+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(RepositoryHelper);
+ var returnValue = Activator.CreateInstance(newRepositoryType, setSelector)
+ ?? 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 f52f6b6..207d686 100644
--- a/InstantAPIs/WebApplicationExtensions.cs
+++ b/InstantAPIs/WebApplicationExtensions.cs
@@ -1,41 +1,45 @@
-using Microsoft.AspNetCore.Builder;
+using InstantAPIs.Repositories;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
+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 InstantAPIsConfig Configuration { get; set; } = new();
+ internal const string LOGGER_CATEGORY_NAME = "InstantAPI";
- 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 : 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.Tables.Any() ?
- dbTables :
- Configuration.Tables.Where(t => dbTables.Any(db => 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);
+ MapInstantAPIsUsingReflection(app, requestedTables);
return app;
}
- private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, IEnumerable requestedTables) where D : DbContext
+ private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, IEnumerable requestedTables)
{
ILogger logger = NullLogger.Instance;
@@ -46,35 +50,38 @@ 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);
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 });
-
// 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();
- 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(D), table.InstanceType);
- genericMethod.Invoke(null, new object[] { app, table.BaseUrl.ToString() });
+ 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 D : 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;
+ var serviceOptions = applicationBuilder.ApplicationServices.GetRequiredService>().Value;
if (serviceOptions == null || serviceOptions.EnableSwagger == null)
{
throw new ArgumentException("Call builder.Services.AddInstantAPIs(options) before MapInstantAPIs.");
@@ -87,35 +94,5 @@ private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action
applicationBuilder.UseSwagger();
applicationBuilder.UseSwaggerUI();
}
-
- var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetService(typeof(D)) as D;
- var builder = new InstantAPIsConfigBuilder(ctx);
- if (options != null)
- {
- options(builder);
- Configuration = builder.Build();
- }
}
-
- 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)
- .Select(x => new TypeTable {
- Name = x.Name,
- InstanceType = x.PropertyType.GenericTypeArguments.First(),
- BaseUrl = new Uri($"/api/{x.Name}", uriKind: UriKind.RelativeOrAbsolute)
- })
- .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; }
- }
-
}
diff --git a/Test/Configuration/InstantAPIsConfigBuilderFixture.cs b/Test/Configuration/InstantAPIsConfigBuilderFixture.cs
new file mode 100644
index 0000000..629650c
--- /dev/null
+++ b/Test/Configuration/InstantAPIsConfigBuilderFixture.cs
@@ -0,0 +1,26 @@
+using InstantAPIs.Repositories;
+using InstantAPIs;
+using Microsoft.AspNetCore.Builder;
+using Microsoft.EntityFrameworkCore;
+using Moq;
+using System.Linq.Expressions;
+
+namespace Test.Configuration;
+
+public abstract class InstantAPIsConfigBuilderFixture : BaseFixture
+{
+ internal InstantAPIsBuilder _Builder;
+
+ public InstantAPIsConfigBuilderFixture()
+ {
+ 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/WhenIncludeDoesNotSpecifyBaseUrl.cs b/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs
index 0acaa0c..252e81a 100644
--- a/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs
+++ b/Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs
@@ -1,25 +1,11 @@
using InstantAPIs;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.EntityFrameworkCore;
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()
{
@@ -27,16 +13,12 @@ public void ShouldSpecifyDefaultUrl()
// arrange
// act
- _Builder.IncludeTable(db => db.Contacts);
+ _Builder.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions());
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 ef4ed08..9491a47 100644
--- a/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs
+++ b/Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs
@@ -1,31 +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 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()
{
@@ -34,16 +14,12 @@ 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
- 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 dad6b54..292d47e 100644
--- a/Test/Configuration/WithIncludesAndExcludes.cs
+++ b/Test/Configuration/WithIncludesAndExcludes.cs
@@ -1,24 +1,11 @@
using InstantAPIs;
-using Microsoft.AspNetCore.Builder;
-using Microsoft.EntityFrameworkCore;
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()
@@ -27,16 +14,15 @@ 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();
// 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 d40c587..fe50448 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()
{
@@ -32,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);
}
@@ -52,4 +36,3 @@ public void ShouldThrowAnErrorIfAllTablesExcluded()
}
}
-
diff --git a/Test/Configuration/WithOnlyIncludes.cs b/Test/Configuration/WithOnlyIncludes.cs
index 765da73..b6ca155 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()
{
@@ -32,12 +13,12 @@ public void ShouldNotIncludeAllTables()
// arrange
// act
- _Builder.IncludeTable(db => db.Contacts);
+ _Builder.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions());
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);
}
@@ -51,11 +32,11 @@ 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
- 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 11e4f1a..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
{
-
- InstantAPIsConfigBuilder _Builder;
-
- public WithoutIncludes()
- {
-
- var _ContextOptions = new DbContextOptionsBuilder()
- .UseInMemoryDatabase("TestDb")
- .Options;
- _Builder = new(new(_ContextOptions));
-
- }
-
-
[Fact]
public void ShouldIncludeAllTables()
{
@@ -32,11 +15,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);
}
-
}
-
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/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()
+ {
+ }
}
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