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(x => x.JsonFilename = "mock.json"); +builder.Services.AddSingleton(); var app = builder.Build(); app.MapGet("/", () => "Hello World!"); -app.UseJsonRoutes(); +app.MapInstantAPIs(builder => +{ + builder + .IncludeTable(context => context.LoadTable("products"), new InstantAPIs.InstantAPIsOptions.TableOptions(), baseUrl: "api/someproducts"); +}); +//app.MapInstantAPIs(); app.Run(); diff --git a/WorkingApi/Program.cs b/WorkingApi/Program.cs index 9452016..de55d58 100644 --- a/WorkingApi/Program.cs +++ b/WorkingApi/Program.cs @@ -1,6 +1,4 @@ using InstantAPIs; -using Microsoft.EntityFrameworkCore; -using Microsoft.OpenApi.Models; using System.Diagnostics; using WorkingApi; @@ -10,12 +8,9 @@ builder.Services.AddInstantAPIs(); var app = builder.Build(); - -var sw = Stopwatch.StartNew(); - -app.MapInstantAPIs(config => +app.MapInstantAPIs(builder => { - config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.All, "addressBook"); + builder.IncludeTable(db => db.Contacts, new InstantAPIsOptions.TableOptions() { KeySelector = x => x.Id }, ApiMethodsToGenerate.All, "addressBook"); }); app.Run();