diff --git a/README.md b/README.md index 6d92617..7cde786 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,22 @@ The easiest way to install EntityDock in your project is to install the latest E It's possible that more packages may be added in the future. + + +You can install these package following the next example: + +```bash +Install-Package EntityDock.Lib.Auto +``` + +or using dotnet CLI: + +```bash +dotnet add package EntityDock.Lib.Auto +``` + + + # Key questions **What's mean "generate controller"?** Yes, without writing a line of code or declaring a class you can have API Controllers base on ASP.NET Core MVC from declared entities. The code required for this is as follows: @@ -76,6 +92,7 @@ When you are setting up your MVC options in ASP.NET Core, you must call this met ```c# [SetRouteAttibute("data/students")] public class StudentEntity{ + public uint Id {get;set;} public string Name {get;set;} @@ -88,11 +105,9 @@ public class StudentEntity{ } ``` -Then you will have a complete API Rest about this entity with full methods, Crud, search, filters, sort and more. - -**How works the `AutoDbContext`?** It's simple, this is a class that derived from `DbContext` in Entity Framework, then using this class, you can create a context from external assemblies or types collections that will has these types as entities and this context can be used like other any context of Entity Framework. Using this way you cannot setup via `ModelBuilder` API fluent methods inside context class, you just have conventions and annotations for `AutoDbContext`. This is a natural limitations 'cause its job consists of including different entities without declare specific `DbContext`. - +Then you will have a complete API Rest about this entity with full methods, Crud, search, filters, sort and more. Of course you should care about your DB connections and migrations if you are using relational database. This package is completely compatible with Entity Framework Core. +**How works the `AutoDbContext`?** It's simple, this is a class that derived from `DbContext` in Entity Framework Core, then using this class, you can create a context from external assemblies or types collections that will has these types as entities and this context can be used like other any context of Entity Framework. Using this way you cannot setup via `ModelBuilder` API fluent methods inside context class, you just have conventions and annotations for `AutoDbContext`. This is a natural limitations 'cause its job consists of including different entities without declare specific `DbContext`. ## Contributing diff --git a/src/Libs/Auto/AutoApiOption.cs b/src/Libs/Auto/AutoApiOption.cs new file mode 100644 index 0000000..856c4ff --- /dev/null +++ b/src/Libs/Auto/AutoApiOption.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace EntityDock.Lib.Auto +{ + public class AutoApiOption + { + public bool ApiUsageService { get; set; } = false; + + public bool SchemaShareEnabled { get; set; } = false; + } +} diff --git a/src/Libs/Auto/AutoDbExtensions.cs b/src/Libs/Auto/AutoDbExtensions.cs new file mode 100644 index 0000000..ecd2e3f --- /dev/null +++ b/src/Libs/Auto/AutoDbExtensions.cs @@ -0,0 +1,133 @@ +using EntityDock.Lib.Base; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Reflection; +using EntityDock.Lib.Auto; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using System.Linq; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class AutoDbExtensions + { + /// + /// Add auto controllers features in services collections + /// + /// + /// + public static void AddDataControllers(this IMvcCoreBuilder services, Type[] types, AutoApiOption options = null) + { + // feature to add controller frome ntity types + services.PartManager.FeatureProviders.Add(new ControllerMakerFeatureProvider(types, options)); + + // convention to use in routes + services.AddMvcOptions(x => x.Conventions.Add(new GenericControllerFeatureConvention())); + } + + /// + /// Add auto controllers features in services collections + /// + /// + /// + public static void AddDataControllers(this IMvcBuilder services, Type[] types, AutoApiOption options = null) + { + // feature to add controller frome ntity types + services.PartManager.FeatureProviders.Add(new ControllerMakerFeatureProvider(types, options)); + + // convention to use in routes + services.AddMvcOptions(x => x.Conventions.Add(new GenericControllerFeatureConvention())); + } + + /// + /// Add auto controllers features in services collections + /// + /// + /// + public static void AddDataControllers(this IMvcCoreBuilder services, + Type dbContext, + bool deepScan = false, + AutoApiOption options = null) + { + if (!dbContext.IsAssignableFrom(typeof(DbContext))) + { + throw new Exception(); + } + + var types = dbContext.GetProperties() + .Where(x => x.PropertyType.Name.Equals(typeof(DbSet<>).Name)) + .Select(x => x.PropertyType.GenericTypeArguments[0]) + .ToArray(); + + // feature to add controller frome ntity types + services.PartManager.FeatureProviders.Add(new ControllerMakerFeatureProvider(types, options)); + + // convention to use in routes + services.AddMvcOptions(x => x.Conventions.Add(new GenericControllerFeatureConvention())); + } + + /// + /// Add auto controllers features in services collections + /// + /// + /// + public static void AddDataControllers(this IMvcBuilder services, + Type dbContext, + bool deepScan = false, + AutoApiOption options = null) + { + if (services is null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (dbContext is null) + { + throw new ArgumentNullException(nameof(dbContext)); + } + + var types = dbContext.GetProperties() + .Where(x => x.PropertyType.Name.Equals(typeof(DbSet<>).Name)) + .Select(x => x.PropertyType.GenericTypeArguments[0]) + .ToArray(); + + // feature to add controller frome ntity types + services.PartManager.FeatureProviders.Add(new ControllerMakerFeatureProvider(types, options)); + + // convention to use in routes + services.AddMvcOptions(x => x.Conventions.Add(new GenericControllerFeatureConvention())); + } + + /// + /// Add auto controllers features in services collections using a passed type argument. + /// + /// + /// + /// + public static void AddDataControllers(this IMvcCoreBuilder services, bool deepScan = false) + { + services.AddDataControllers(typeof(TContext), deepScan); + } + + /// + /// Add auto controllers features in services collections using a passed type argument. + /// + /// + /// + /// + public static void AddDataControllers(this IMvcBuilder services, bool deepScan = false) + { + services.AddDataControllers(typeof(TContext), deepScan); + } + + /// + /// Add auto controllers features in services collections + /// + /// + public static void AddFilterTriggers(this IMvcCoreBuilder services, Type[] types) + { + services.AddMvcOptions(opt => opt.Filters.Add()); + } + } +} diff --git a/src/Libs/Auto/ControllerMakerFeatureProvider.cs b/src/Libs/Auto/ControllerMakerFeatureProvider.cs index cb51f8c..4be1874 100644 --- a/src/Libs/Auto/ControllerMakerFeatureProvider.cs +++ b/src/Libs/Auto/ControllerMakerFeatureProvider.cs @@ -10,7 +10,6 @@ namespace EntityDock.Lib.Auto { - /// /// Create controller by passed types as controller /// @@ -25,7 +24,6 @@ public static ControllerMakerFeatureProvider FromAssembly(Assembly assembly) { // filter and take an array of the candidates return new ControllerMakerFeatureProvider(types: assembly.GetExportedTypes() - .Where(t => t.IsEntity()) .Where(t => t.IsDefined(typeof(SetRouteAttibute))) .ToArray() ); @@ -35,15 +33,22 @@ public static ControllerMakerFeatureProvider FromAssembly(Assembly assembly) /// Require entry types for mapping /// /// - public ControllerMakerFeatureProvider(Type[] types) + public ControllerMakerFeatureProvider(Type[] types, AutoApiOption options = default) { - TargetTypes = types; + if (options is null) + { + options = new AutoApiOption(); + } + + TargetTypes = types ?? throw new ArgumentNullException(nameof(types)); + Options = options; } /// /// All passed types /// public Type[] TargetTypes { get; } + public AutoApiOption Options { get; } /// /// Populate all controller created from passed types @@ -57,7 +62,7 @@ public void PopulateFeature(IEnumerable parts, ControllerFeatur { // push new controller from route feature.Controllers.Add(item: GetCandidateController(route) - .MakeGenericType(route.Model) + .MakeGenericType(route.Model, HelpersExtensions.FindKeyType(route.Model)) .GetTypeInfo() ); } @@ -71,9 +76,10 @@ public void PopulateFeature(IEnumerable parts, ControllerFeatur private Type GetCandidateController(UnitRoute item) => item.ModelType switch { - ModelType.FullyFeatures => typeof(FullyFeatureController<,>), - ModelType.Record => typeof(RecordController<,>), - //ModelType.Record => typeof(), + ModelType.FullyFeatures when(Options.ApiUsageService) => typeof(FullyFeatureController<,>), + ModelType.Record when(Options.ApiUsageService) => typeof(RecordController<,>), + ModelType.FullyFeatures => typeof(RepoFullyFeatureController<,>), + ModelType.Record => typeof(RepoRecordController<,>), _ => null }; @@ -87,17 +93,31 @@ private IEnumerable GetRoutes() { var attr = target.GetCustomAttribute(); - // make an route from attributes specifications - return new UnitRoute { - Model = target, - ModelType = attr.Usage switch + // if has attribute + if (attr != null) + { + // make an route from attributes specifications + return new UnitRoute + { + Model = target, + ModelType = attr.Usage switch + { + EntityUsage.Readonly => ModelType.Readonly, + EntityUsage.Record => ModelType.Record, + EntityUsage.FullyUsage => ModelType.FullyFeatures, + _ => throw new InvalidOperationException() + } + }; + } + else + { + // from static definition + return new UnitRoute { - EntityUsage.Readonly => ModelType.Readonly, - EntityUsage.Record => ModelType.Record, - EntityUsage.FullyUsage => ModelType.FullyFeatures, - _ => throw new InvalidOperationException() - } - }; + Model = target, + ModelType = ModelType.FullyFeatures + }; + } }); } } diff --git a/src/Libs/Auto/Controllers/FullyFeatureController.cs b/src/Libs/Auto/Controllers/FullyFeatureController.cs index 24fa910..71cbca8 100644 --- a/src/Libs/Auto/Controllers/FullyFeatureController.cs +++ b/src/Libs/Auto/Controllers/FullyFeatureController.cs @@ -1,45 +1,30 @@ using System; using System.Linq; - using System.Threading.Tasks; using EntityDock.Persistence; using Microsoft.AspNetCore.Mvc; using EntityDock.Entities.Base; -using System.Collections.Generic; using EntityDock.Extensions.Query; using Microsoft.EntityFrameworkCore; using Swashbuckle.AspNetCore.Annotations; using System.Linq.Dynamic.Core; -using EntityDock; namespace EntityDock.Lib.Auto.Controllers { /// /// Markets crud example with functional Api Systems /// - [ApiController] - public class FullyFeatureController : ControllerBase + public class FullyFeatureController : OperationsController where T: AggregateRoot { /// /// Require basic data service /// /// - public FullyFeatureController(DataService service) + public FullyFeatureController(DataService service) : base(service) { - if (service is null) - { - throw new ArgumentNullException(nameof(service)); - } - - DataService = service; } - /// - /// Reference of the active service for this entity - /// - public DataService DataService { get; set; } - /// /// Devuelve un listado de todos los registros de datos /// @@ -55,7 +40,7 @@ public FullyFeatureController(DataService service) /// /// [HttpGet] - public async Task QueryMarkets( + public async Task Query( [SwaggerParameter(" Array with rules to filter"), FromQuery] string[] filter, [SwaggerParameter(" Array with rules to allow select other records"), FromQuery] string[] or, [SwaggerParameter(" Array with rules to include relations"), FromQuery] string[] join, @@ -201,7 +186,204 @@ public async Task GetCountAsync( /// /// [HttpDelete] +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + public async Task DeleteQueryResultAsync( +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + [SwaggerParameter(" Array with rules to filter"), FromQuery] string[] filter, + [SwaggerParameter(" Array with rules to allow select other records"), FromQuery] string[] or, + [SwaggerParameter(" Array with rules to include relations"), FromQuery] string[] join, + [SwaggerParameter(" Array with rules to sort"), FromQuery] string[] select, + [SwaggerParameter(" Array with rules to sort"), FromQuery] string[] sort, + [SwaggerParameter(" Page that will show")] int page, + [SwaggerParameter(" Limit in query or page size if the {page} > 0")] int limit, + [SwaggerParameter(" Simple offset parameters to skip records")] int offset, + [SwaggerParameter(" Search keywords")] string search, + [SwaggerParameter(" Search method")] string searchMethod, + [SwaggerParameter(" Case sensitive for Search")] bool caseSensitive) + { + return null; + } + } + + /// + /// Markets crud example with functional Api Systems + /// + public class RepoFullyFeatureController : RepoOperationsController + where T : AggregateRoot + { + /// + /// Require basic data service + /// + /// + public RepoFullyFeatureController(IRepository service) : base(service) + { + } + + /// + /// Devuelve un listado de todos los registros de datos + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + public async Task Query( + [SwaggerParameter(" Array with rules to filter"), FromQuery] string[] filter, + [SwaggerParameter(" Array with rules to allow select other records"), FromQuery] string[] or, + [SwaggerParameter(" Array with rules to include relations"), FromQuery] string[] join, + [SwaggerParameter(" Array with rules to sort"), FromQuery] string[] select, + [SwaggerParameter(" Array with rules to sort"), FromQuery] string[] sort, + [SwaggerParameter(" Page that will show")] int page, + [SwaggerParameter(" Limit in query or page size if the {page} > 0")] int limit, + [SwaggerParameter(" Simple offset parameters to skip records")] int offset, + [SwaggerParameter(" Search keywords")] string search, + [SwaggerParameter(" Search method")] string searchMethod, + [SwaggerParameter(" Case sensitive for Search")] bool caseSensitive, + [SwaggerParameter(" Cache enable")] bool cache) + { + // full initialize + var queryModel = new FundamentalQueryModel(filter, or, join, + select: select, sorts: sort, page: page, limit: limit, + offset: offset, search: search, searchMethod: searchMethod, + caseSensitive: caseSensitive, cache: cache); + + // apply filter base on model parameters + var query = Repo.Get() + .ApplyFilter(queryModel); + + // this variable is declared to save the record number + int count = 0; + + // check + if (queryModel.Page > 0) + { + var pageNumber = queryModel.Page; + var pageSize = queryModel.Limit; + pageNumber = pageNumber == 0 ? 1 : pageNumber; + pageSize = pageSize == 0 ? 10 : pageSize; + + // is necesary do this before apply pagination but with filter applied + // because the filters determine the total record if has filter in query + count = await query.CountAsync(); + query = query.Skip((pageNumber - 1) * pageSize).Take(pageSize); + } + else + { + if (queryModel.Limit > 0) + { + query = query.Take(queryModel.Limit); + } + + if (queryModel.Limit > 0) + { + query = query.Skip(queryModel.Offset); + } + } + + // container to store results + object result; + + // define the result base on pagination request + if (queryModel.Select is not null && queryModel.Select.Length > 0) + { + result = await query.SelectFields(queryModel.Select) + .ToDynamicListAsync(); + } + else + { + result = await query.ToListAsync(); + } + + // this variable is necesary to store paginated result or simple result + var finalResult = page > 0 ? new PaginatedResult(true, result, count, page, limit) : result; + + // flush result in response + return Ok(finalResult); + } + + /// + /// Devuelve un unico registro basado en su id + /// + /// + /// + [HttpGet("{id}")] + [SwaggerResponse(404, "The market data not found by id")] + + public async Task GetByIdAsync([FromRoute] TID id) + { + var result = await Repo.GetOne(id); + return result is null ? NotFound() : Ok(value: result); + } + + /// + /// Insert many records by batch income in Body Request + /// + /// + /// + [HttpGet("query/get-text")] + [SwaggerResponse(404, "If the entity not has text fields")] + public async Task GetTextAsync() + { + var query = await Repo.Get().SelectText().ToDynamicListAsync(); + return Ok(QueryHelpers.CountAllText(query)); + } + + /// + /// Simple download a number of records + /// + /// + /// + [HttpGet("query/count")] + [SwaggerResponse(404, "If the entity not has text fields")] + public async Task GetCountAsync( + [SwaggerParameter(" Array with rules to filter"), FromQuery] string[] filters, + [SwaggerParameter(" Array with rules to allow select other records"), FromQuery] string[] or, + [SwaggerParameter(" Search keywords")] string search, + [SwaggerParameter(" Search method")] string searchMethod, + [SwaggerParameter(" Case sensitive for Search")] bool caseSensitive) + { + var queryModel = new FundamentalQueryModel(filters, or, search, searchMethod, caseSensitive); + + // get count from filter passed + long count = await Repo.Get() + .ApplyFilter(queryModel) + .LongCountAsync(); + + // flush count data + return base.Ok(new + { + count, + withFilter = filters.Length > 0 || or.Length > 0 || (string.IsNullOrWhiteSpace(search) is false) + }); + } + + + /// + /// Delete all record that match with income query + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + [HttpDelete] +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously public async Task DeleteQueryResultAsync( +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously [SwaggerParameter(" Array with rules to filter"), FromQuery] string[] filter, [SwaggerParameter(" Array with rules to allow select other records"), FromQuery] string[] or, [SwaggerParameter(" Array with rules to include relations"), FromQuery] string[] join, diff --git a/src/Libs/Auto/Controllers/OperationsController.cs b/src/Libs/Auto/Controllers/OperationsController.cs index f23cbd2..7e224d4 100644 --- a/src/Libs/Auto/Controllers/OperationsController.cs +++ b/src/Libs/Auto/Controllers/OperationsController.cs @@ -12,7 +12,7 @@ namespace EntityDock.Lib.Auto.Controllers /// /// Markets crud example with functional Api Systems /// - [ApiController] + //[ApiController] public abstract class OperationsController : ControllerBase where T : AggregateRoot { diff --git a/src/Libs/Auto/Controllers/RecordController.cs b/src/Libs/Auto/Controllers/RecordController.cs index 9022b95..a6deda7 100644 --- a/src/Libs/Auto/Controllers/RecordController.cs +++ b/src/Libs/Auto/Controllers/RecordController.cs @@ -39,7 +39,7 @@ public RecordController(DataService service) : base(service) /// /// [HttpGet] - public async Task QueryMarkets( + public async Task Query( [SwaggerParameter(" Page that will show")] int page, [SwaggerParameter(" Limit in query or page size if the {page} > 0")] int limit, [SwaggerParameter(" Cache enable")] bool cache) diff --git a/src/Libs/Auto/Controllers/RepoOperationsController.cs b/src/Libs/Auto/Controllers/RepoOperationsController.cs new file mode 100644 index 0000000..514473c --- /dev/null +++ b/src/Libs/Auto/Controllers/RepoOperationsController.cs @@ -0,0 +1,122 @@ +using EntityDock.Entities.Base; +using Microsoft.AspNetCore.Mvc; +using Swashbuckle.AspNetCore.Annotations; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace EntityDock.Lib.Auto.Controllers +{ + /// + /// Markets crud example with functional Api Systems + /// + //[ApiController] + public abstract class RepoOperationsController : ControllerBase + where T : AggregateRoot + { + /// + /// Require basic data service + /// + /// + public RepoOperationsController(IRepository service) + { + if (service is null) + { + throw new ArgumentNullException(nameof(service)); + } + + Repo = service; + } + + /// + /// Reference of the active service for this entity + /// + public IRepository Repo { get; set; } + + /// + /// Actualiza un registro con datos que entran en el cuerpo de la peticion + /// + /// + /// + [HttpPut("{id}")] + [SwaggerResponse(204, "The market is updated")] + [SwaggerResponse(400, "The market data is invalid")] + public async Task UpdateAsync([FromQuery] TID id, [FromBody] T data) + { + try + { + await Repo.UpdateAsync(id, data); + } + catch (Exception) + { + // ignore for now + } + return NoContent(); + } + + /// + /// Elimina un registro basado en su id + /// + /// + /// + [HttpDelete("{id}")] + public async Task DeleteAsync( + [SwaggerParameter("Id to delete a market record"), FromRoute] TID id) + { + try + { + await Repo.DeleteAsync(id); + } + catch (Exception) + { + // ignore for now + } + return NoContent(); + } + + /// + /// Agrega un registro con datos que entran en el cuerpo de la peticion + /// + /// + /// + [HttpPost] + [SwaggerResponse(400, "The market data is invalid")] + public async Task AddAsync([FromBody] T data) + { + try + { + var result = await Repo.StoreAnsyc(data); + return StatusCode(201, new + { + data.Id, + }); + } + catch (Exception) + { + // ignore for now + return StatusCode(400); + } + } + + /// + /// Insert many records by batch income in Body Request + /// + /// + /// + [HttpPost("batch")] + [SwaggerResponse(204, "If the code is 204 then the operation is successful")] + public async Task AddBatchAsync([FromBody] IEnumerable data) + { + try + { + await Repo.BulkStore(data); + return NoContent(); + } + catch (Exception) + { + // ignore for now + return StatusCode(400); + } + } + } +} diff --git a/src/Libs/Auto/Controllers/RepoRecordController.cs b/src/Libs/Auto/Controllers/RepoRecordController.cs new file mode 100644 index 0000000..29d91f5 --- /dev/null +++ b/src/Libs/Auto/Controllers/RepoRecordController.cs @@ -0,0 +1,67 @@ +using EntityDock.Entities.Base; +using EntityDock.Extensions.Query; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Swashbuckle.AspNetCore.Annotations; +using System.Threading.Tasks; + +namespace EntityDock.Lib.Auto.Controllers +{ + /// + /// Simple readed controller. + /// + /// + /// + public class RepoRecordController : RepoOperationsController where T : AggregateRoot + { + public RepoRecordController(IRepository service) : base(service) + { + } + + /// + /// Devuelve un listado de todos los registros de datos + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + [HttpGet] + public async Task Query( + [SwaggerParameter(" Page that will show")] int page, + [SwaggerParameter(" Limit in query or page size if the {page} > 0")] int limit, + [SwaggerParameter(" Cache enable")] bool cache) + { + // full initialize + var queryModel = new FundamentalQueryModel(); + + // apply filter base on model parameters + var query = Repo.Get() + .ApplyFilter(queryModel); + + + // flush result in response + return Ok(await query.ToListAsync()); + } + + /// + /// Devuelve un unico registro basado en su id + /// + /// + /// + [HttpGet("{id}")] + [SwaggerResponse(404, "The entity data not found by id")] + + public async Task GetByIdAsync([FromRoute] TID id) + { + var result = await Repo.GetOne(id); + return result is null ? NotFound() : Ok(value: result); + } + } +} diff --git a/src/Libs/Auto/Extensions.cs b/src/Libs/Auto/Extensions.cs deleted file mode 100644 index fe1f37b..0000000 --- a/src/Libs/Auto/Extensions.cs +++ /dev/null @@ -1,43 +0,0 @@ -using EntityDock.Lib.Base; -using Microsoft.Extensions.DependencyInjection; -using System; -using System.Collections.Generic; -using System.Reflection; - -namespace EntityDock.Lib.Auto -{ - public static class Extensions - { - /// - /// Add auto controllers features in services collections - /// - /// - public static void AddDataControllers(this IMvcCoreBuilder services, Type[] types) - { - // feature to add controller frome ntity types - services.PartManager.FeatureProviders.Add(new ControllerMakerFeatureProvider(types)); - - // convention to use in routes - services.AddMvcOptions(x => x.Conventions.Add(new GenericAppRouteConvention())); - } - - /// - /// Add auto controllers features in services collections - /// - /// - public static void AddFilterTriggers(this IMvcCoreBuilder services, Type[] types) - { - services.AddMvcOptions(opt => opt.Filters.Add()); - } - - /// - /// Return true if is an entity class - /// - /// - /// - public static bool IsEntity(this Type type) - { - return type.IsDefined(typeof(EntityAttribute), false); - } - } -} diff --git a/src/Libs/Auto/GenericAppRouteConvention.cs b/src/Libs/Auto/GenericControllerFeatureConvention.cs similarity index 52% rename from src/Libs/Auto/GenericAppRouteConvention.cs rename to src/Libs/Auto/GenericControllerFeatureConvention.cs index 0d068f3..33b411d 100644 --- a/src/Libs/Auto/GenericAppRouteConvention.cs +++ b/src/Libs/Auto/GenericControllerFeatureConvention.cs @@ -1,4 +1,5 @@ -using Microsoft.AspNetCore.Mvc; +using EntityDock.Lib.Base; +using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; using System.Reflection; @@ -7,7 +8,7 @@ namespace EntityDock.Lib.Auto /// /// Genenric app route controller by application convention /// - public class GenericAppRouteConvention : IApplicationModelConvention + public class GenericControllerFeatureConvention : IApplicationModelConvention { /// /// Applied routing selector for generic controllers @@ -30,15 +31,27 @@ private static void PutRoute(ControllerModel controller) { if (controller.ControllerType.IsGenericType) { - var genericType = controller.ControllerType.GenericTypeArguments[0]; - var customNameAttribute = genericType.GetCustomAttribute(); + var entityPayload = controller.ControllerType.GenericTypeArguments[0]; + var customNameAttribute = entityPayload.GetCustomAttribute(); - if (customNameAttribute?.Route != null) + controller.ControllerName = entityPayload.Name; + + // verify what convention should be used + if (customNameAttribute != null) + { + if (customNameAttribute.Route != null) + { + // put at the modeling + controller.Selectors.Add(new SelectorModel + { + AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(customNameAttribute.Route)), + }); + } + }else if (entityPayload.GetCustomAttribute() != null) { - // put at the modeling controller.Selectors.Add(new SelectorModel { - AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(customNameAttribute.Route)), + AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(entityPayload.Name)), }); } } diff --git a/src/Libs/Auto/GenericControllerRouteConvention.cs b/src/Libs/Auto/GenericControllerRouteConvention.cs deleted file mode 100644 index bb061cd..0000000 --- a/src/Libs/Auto/GenericControllerRouteConvention.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.AspNetCore.Mvc; -using Microsoft.AspNetCore.Mvc.ApplicationModels; -using System.Reflection; - -namespace EntityDock.Lib.Auto -{ - - /// - /// Route controllers by generic type uses an attribute: - /// - public class GenericControllerRouteConvention : IControllerModelConvention - { - /// - /// Applied routing selector for generic controllers - /// - /// - public void Apply(ControllerModel controller) - { - if (controller.ControllerType.IsGenericType) - { - var genericType = controller.ControllerType.GenericTypeArguments[0]; - var customNameAttribute = genericType.GetCustomAttribute(); - - if (customNameAttribute?.Route != null) - { - // put - controller.Selectors.Add(new SelectorModel - { - AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(customNameAttribute.Route)), - }); - } - } - } - } -} diff --git a/src/Libs/Auto/HelpersExtensions.cs b/src/Libs/Auto/HelpersExtensions.cs new file mode 100644 index 0000000..dd30c35 --- /dev/null +++ b/src/Libs/Auto/HelpersExtensions.cs @@ -0,0 +1,35 @@ +using EntityDock.Lib.Base; +using System; +using System.Collections.Generic; +using System.Text; + +namespace EntityDock.Lib.Auto +{ + public static class HelpersExtensions + { + /// + /// Return true if is an entity class + /// + /// + /// + public static bool IsEntity(this Type type) + { + return type.IsDefined(typeof(EntityAttribute), false); + } + + public static Type FindKeyType(Type model) + { + var baseClass = model.BaseType; + + // verify that base class is AggregateRoot + if (baseClass.FullName.StartsWith("EntityDock.Entities.Base.AggregateRoot")) + { + return baseClass.GenericTypeArguments[0]; + } + else + { + return null; + } + } + } +} diff --git a/src/Libs/EntityDock.Lib.sln b/src/Libs/EntityDock.Lib.sln index 33eb5ba..381a314 100644 --- a/src/Libs/EntityDock.Lib.sln +++ b/src/Libs/EntityDock.Lib.sln @@ -15,6 +15,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityDock.Extensions.Query EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityDock.Lib.StarterKit", "StarterKit\EntityDock.Lib.StarterKit.csproj", "{41B62D05-224A-4C30-B863-D26417932B94}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MarketDemo", "..\Samples\market-demo\MarketDemo\MarketDemo.csproj", "{8490D061-329B-4AE0-8DBD-690AB7B81DE7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,6 +47,10 @@ Global {41B62D05-224A-4C30-B863-D26417932B94}.Debug|Any CPU.Build.0 = Debug|Any CPU {41B62D05-224A-4C30-B863-D26417932B94}.Release|Any CPU.ActiveCfg = Release|Any CPU {41B62D05-224A-4C30-B863-D26417932B94}.Release|Any CPU.Build.0 = Release|Any CPU + {8490D061-329B-4AE0-8DBD-690AB7B81DE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8490D061-329B-4AE0-8DBD-690AB7B81DE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8490D061-329B-4AE0-8DBD-690AB7B81DE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8490D061-329B-4AE0-8DBD-690AB7B81DE7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Libs/Query/EntityDock.Extensions.Query.csproj b/src/Libs/Query/EntityDock.Extensions.Query.csproj index 92a1cc8..0dc730e 100644 --- a/src/Libs/Query/EntityDock.Extensions.Query.csproj +++ b/src/Libs/Query/EntityDock.Extensions.Query.csproj @@ -11,6 +11,7 @@ + diff --git a/src/Samples/market-demo/Data/AppDbContext.cs b/src/Samples/market-demo/Data/AppDbContext.cs new file mode 100644 index 0000000..f052953 --- /dev/null +++ b/src/Samples/market-demo/Data/AppDbContext.cs @@ -0,0 +1,19 @@ +using Microsoft.EntityFrameworkCore; +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Threading.Tasks; + +namespace MarketDemo.Data +{ + public class AppDbContext : DbContext + { + public DbSet Assets { get; set; } + + public AppDbContext([NotNull] DbContextOptions options) : base(options) + { + + } + } +} diff --git a/src/Samples/market-demo/Data/MarketAsset.cs b/src/Samples/market-demo/Data/MarketAsset.cs new file mode 100644 index 0000000..01c54d1 --- /dev/null +++ b/src/Samples/market-demo/Data/MarketAsset.cs @@ -0,0 +1,23 @@ +using EntityDock.Entities.Base; +using EntityDock.Lib.Auto; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MarketDemo.Data +{ + [SetRouteAttibute("assets")] + public class MarketAsset : AggregateRoot + { + public uint Stock { get; set; } + + public uint Price { get; set; } + + public string Name { get; set; } + + public string Code { get; set; } + + public string Description { get; set; } + } +} diff --git a/src/Samples/market-demo/MarketDemo.csproj b/src/Samples/market-demo/MarketDemo.csproj new file mode 100644 index 0000000..4ebed23 --- /dev/null +++ b/src/Samples/market-demo/MarketDemo.csproj @@ -0,0 +1,20 @@ + + + + net5.0 + Linux + ..\..\..\Libs + + + + + + + + + + + + + + diff --git a/src/Samples/market-demo/MarketDemo.sln b/src/Samples/market-demo/MarketDemo.sln new file mode 100644 index 0000000..a4c5adf --- /dev/null +++ b/src/Samples/market-demo/MarketDemo.sln @@ -0,0 +1,55 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31727.386 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MarketDemo", "MarketDemo.csproj", "{015C8289-ED18-4070-9C6B-7D6C55FC955B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityDock.Lib.Persistence", "..\..\Libs\Persistence\EntityDock.Lib.Persistence.csproj", "{070C606D-7ABA-4800-909C-FF359634D94B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityDock.Lib.Auto", "..\..\Libs\Auto\EntityDock.Lib.Auto.csproj", "{FBAA155C-9690-4F76-9E32-460051F2E9CF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityDock.Lib.Base", "..\..\Libs\Base\EntityDock.Lib.Base.csproj", "{1BE5D0B2-8CE4-46E7-99FC-924B91197483}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityDock.Extensions.Query", "..\..\Libs\Query\EntityDock.Extensions.Query.csproj", "{593F487B-E9F1-4B76-8C5A-1ED85F3C5A23}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EntityDock.Extensions.Query.Abstractions", "..\..\Libs\Query.Abstractions\EntityDock.Extensions.Query.Abstractions.csproj", "{E3C9A1A2-1A11-4030-A8F2-FB6809AA899D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {015C8289-ED18-4070-9C6B-7D6C55FC955B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {015C8289-ED18-4070-9C6B-7D6C55FC955B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {015C8289-ED18-4070-9C6B-7D6C55FC955B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {015C8289-ED18-4070-9C6B-7D6C55FC955B}.Release|Any CPU.Build.0 = Release|Any CPU + {070C606D-7ABA-4800-909C-FF359634D94B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {070C606D-7ABA-4800-909C-FF359634D94B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {070C606D-7ABA-4800-909C-FF359634D94B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {070C606D-7ABA-4800-909C-FF359634D94B}.Release|Any CPU.Build.0 = Release|Any CPU + {FBAA155C-9690-4F76-9E32-460051F2E9CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FBAA155C-9690-4F76-9E32-460051F2E9CF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FBAA155C-9690-4F76-9E32-460051F2E9CF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FBAA155C-9690-4F76-9E32-460051F2E9CF}.Release|Any CPU.Build.0 = Release|Any CPU + {1BE5D0B2-8CE4-46E7-99FC-924B91197483}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1BE5D0B2-8CE4-46E7-99FC-924B91197483}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1BE5D0B2-8CE4-46E7-99FC-924B91197483}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1BE5D0B2-8CE4-46E7-99FC-924B91197483}.Release|Any CPU.Build.0 = Release|Any CPU + {593F487B-E9F1-4B76-8C5A-1ED85F3C5A23}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {593F487B-E9F1-4B76-8C5A-1ED85F3C5A23}.Debug|Any CPU.Build.0 = Debug|Any CPU + {593F487B-E9F1-4B76-8C5A-1ED85F3C5A23}.Release|Any CPU.ActiveCfg = Release|Any CPU + {593F487B-E9F1-4B76-8C5A-1ED85F3C5A23}.Release|Any CPU.Build.0 = Release|Any CPU + {E3C9A1A2-1A11-4030-A8F2-FB6809AA899D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E3C9A1A2-1A11-4030-A8F2-FB6809AA899D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E3C9A1A2-1A11-4030-A8F2-FB6809AA899D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E3C9A1A2-1A11-4030-A8F2-FB6809AA899D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {1AFF82C3-6B88-4C0B-8276-72A1B0C04BC9} + EndGlobalSection +EndGlobal diff --git a/src/Samples/market-demo/Program.cs b/src/Samples/market-demo/Program.cs new file mode 100644 index 0000000..a08ac42 --- /dev/null +++ b/src/Samples/market-demo/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MarketDemo +{ + public class Program + { + public static void Main(string[] args) + { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); + } +} diff --git a/src/Samples/market-demo/Properties/launchSettings.json b/src/Samples/market-demo/Properties/launchSettings.json new file mode 100644 index 0000000..32d379a --- /dev/null +++ b/src/Samples/market-demo/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:44846", + "sslPort": 44367 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "MarketDemo": { + "commandName": "Project", + "dotnetRunMessages": "true", + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:5001;http://localhost:5000", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Samples/market-demo/Startup.cs b/src/Samples/market-demo/Startup.cs new file mode 100644 index 0000000..4932faf --- /dev/null +++ b/src/Samples/market-demo/Startup.cs @@ -0,0 +1,59 @@ +using MarketDemo.Data; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.HttpsPolicy; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.OpenApi.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace MarketDemo +{ + public class Startup + { + public Startup(IConfiguration configuration) + { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + + services.AddControllers().AddDataControllers(); + services.AddDbContext(db => db.UseInMemoryDatabase("local")); + services.AddPersistenceLayer(); + services.AddSwaggerGen(c => + { + c.SwaggerDoc("v1", new OpenApiInfo { Title = "MarketDemo", Version = "v1" }); + }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "MarketDemo v1")); + } + + app.UseRouting(); + + app.UseEndpoints(endpoints => + { + endpoints.MapControllers(); + }); + } + } +} diff --git a/src/Samples/market-demo/appsettings.Development.json b/src/Samples/market-demo/appsettings.Development.json new file mode 100644 index 0000000..8983e0f --- /dev/null +++ b/src/Samples/market-demo/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/src/Samples/market-demo/appsettings.json b/src/Samples/market-demo/appsettings.json new file mode 100644 index 0000000..d9d9a9b --- /dev/null +++ b/src/Samples/market-demo/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +}