From d57c10475693d536b3b8c45179249e0d0135f0e2 Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Tue, 29 Mar 2022 11:42:15 +0200 Subject: [PATCH 01/17] ci/cd test --- .github/workflows/apigenerator.yml | 76 ++++++++++ .../apigenerator/apis1.arm.json | 131 ++++++++++++++++++ .../serviceDependencies.apigenerator.json | 8 ++ .../Properties/serviceDependencies.json | 7 + 4 files changed, 222 insertions(+) create mode 100644 .github/workflows/apigenerator.yml create mode 100644 sample/ApiGeneratorSampleApp/Properties/ServiceDependencies/apigenerator/apis1.arm.json create mode 100644 sample/ApiGeneratorSampleApp/Properties/serviceDependencies.apigenerator.json create mode 100644 sample/ApiGeneratorSampleApp/Properties/serviceDependencies.json diff --git a/.github/workflows/apigenerator.yml b/.github/workflows/apigenerator.yml new file mode 100644 index 0000000..3262364 --- /dev/null +++ b/.github/workflows/apigenerator.yml @@ -0,0 +1,76 @@ +name: Build and deploy .NET Core application to windows webapp apigenerator with API Management Service ApiGeneratorSampleApIapi +on: + push: + branches: + - vnext +env: + AZURE_WEBAPP_NAME: apigenerator + DOTNET_CORE_VERSION: 6.0.x + WORKING_DIRECTORY: sample\ApiGeneratorSampleApp + CONFIGURATION: Release + AZURE_WEBAPP_PACKAGE_PATH: sample\ApiGeneratorSampleApp/publish + AZURE_APIM_RESOURCE_PATH: /generated + AZURE_APIM_RESOURCEGROUP: apigenerator_group + AZURE_APIM_SERVICENAME: ApiGeneratorSampleApIapi + AZURE_APIM_API_ID: ApiGeneratorSampleApI + AZURE_APIM_APPSERVICEURL: https://apigenerator.azurewebsites.net:80/ + SWASHBUCLE_ASPNET_CORE_CLI_PACKAGE_VERSION: 5.6.3 + SWASHBUCKLE_DOTNET_CORE_VERSION: 3.1.x + API_IMPORT_SPECIFICATION_PATH: sample\ApiGeneratorSampleApp/publish/swagger.json + API_IMPORT_DLL: sample\ApiGeneratorSampleApp/publish/ApiGeneratorSampleApI.dll + API_IMPORT_VERSION: v1 +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_CORE_VERSION }} + - name: Setup SwashBuckle .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.SWASHBUCKLE_DOTNET_CORE_VERSION }} + - name: Restore + run: dotnet restore ${{ env.WORKING_DIRECTORY }} + - name: Build + run: dotnet build ${{ env.WORKING_DIRECTORY }} --configuration ${{ env.CONFIGURATION }} --no-restore + - name: Test + run: dotnet test ${{ env.WORKING_DIRECTORY }} --no-build + - name: Publish + run: dotnet publish ${{ env.WORKING_DIRECTORY }} --configuration ${{ env.CONFIGURATION }} --no-build --output ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + - name: Install Swashbuckle CLI .NET Global Tool + run: dotnet tool install --global Swashbuckle.AspNetCore.Cli --version ${{ env.SWASHBUCLE_ASPNET_CORE_CLI_PACKAGE_VERSION }} + working-directory: ${{ env.WORKING_DIRECTORY }} + - name: Generate Open API Specification Document + run: swagger tofile --output "${{ env.API_IMPORT_SPECIFICATION_PATH }}" "${{ env.API_IMPORT_DLL }}" "${{ env.API_IMPORT_VERSION }}" + - name: Publish Artifacts + uses: actions/upload-artifact@v1.0.0 + with: + name: webapp + path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + deploy: + runs-on: windows-latest + needs: build + steps: + - name: Download artifact from build job + uses: actions/download-artifact@v2 + with: + name: webapp + path: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + - name: Deploy to Azure WebApp + uses: azure/webapps-deploy@v2 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} + package: ${{ env.AZURE_WEBAPP_PACKAGE_PATH }} + publish-profile: ${{ secrets.apigenerator_c1d7 }} + - name: Azure Login + uses: azure/login@v1 + with: + creds: ${{ secrets.ApiGeneratorSampleApIapi_spn }} + - name: Import API into Azure API Management + run: az apim api import --path "${{ env.AZURE_APIM_RESOURCE_PATH }}" --resource-group "${{ env.AZURE_APIM_RESOURCEGROUP }}" --service-name "${{ env.AZURE_APIM_SERVICENAME }}" --api-id "${{ env.AZURE_APIM_API_ID }}" --service-url "${{ env.AZURE_APIM_APPSERVICEURL }}" --specification-path "${{ env.API_IMPORT_SPECIFICATION_PATH }}" --specification-format OpenApi --subscription-required false + - name: logout + run: > + az logout diff --git a/sample/ApiGeneratorSampleApp/Properties/ServiceDependencies/apigenerator/apis1.arm.json b/sample/ApiGeneratorSampleApp/Properties/ServiceDependencies/apigenerator/apis1.arm.json new file mode 100644 index 0000000..7dd1f08 --- /dev/null +++ b/sample/ApiGeneratorSampleApp/Properties/ServiceDependencies/apigenerator/apis1.arm.json @@ -0,0 +1,131 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "resourceGroupName": { + "type": "string", + "defaultValue": "apigenerator_group", + "metadata": { + "_parameterType": "resourceGroup", + "description": "Der Name der Ressourcengruppe für die Ressource. Es wird empfohlen, Ressourcen für eine bessere Nachverfolgung in derselben Ressourcengruppe zu platzieren." + } + }, + "resourceGroupLocation": { + "type": "string", + "defaultValue": "westeurope", + "metadata": { + "_parameterType": "location", + "description": "Der Standort der Ressourcengruppe. Ressourcengruppen können andere Standorte als Ressourcen aufweisen." + } + }, + "resourceLocation": { + "type": "string", + "defaultValue": "[parameters('resourceGroupLocation')]", + "metadata": { + "_parameterType": "location", + "description": "Der Standort der Ressource. Verwenden Sie standardmäßig den Standort der Ressourcengruppe, sofern der Ressourcenanbieter dort unterstützt wird." + } + } + }, + "resources": [ + { + "type": "Microsoft.Resources/resourceGroups", + "name": "[parameters('resourceGroupName')]", + "location": "[parameters('resourceGroupLocation')]", + "apiVersion": "2019-10-01" + }, + { + "type": "Microsoft.Resources/deployments", + "name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat('ApiGeneratorSampleApI', subscription().subscriptionId)))]", + "resourceGroup": "[parameters('resourceGroupName')]", + "apiVersion": "2019-10-01", + "dependsOn": [ + "[parameters('resourceGroupName')]" + ], + "properties": { + "mode": "Incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [ + { + "name": "ApiGeneratorSampleApIapi", + "type": "Microsoft.ApiManagement/service", + "location": "[parameters('resourceLocation')]", + "properties": { + "publisherEmail": "tim.cadenbach@outlook.com", + "publisherName": "Tim Cadenbach", + "notificationSenderEmail": "apimgmt-noreply@mail.windowsazure.com", + "hostnameConfigurations": [ + { + "type": "Proxy", + "hostName": "apigeneratorsampleapiapi.azure-api.net", + "encodedCertificate": null, + "keyVaultId": null, + "certificatePassword": null, + "negotiateClientCertificate": false, + "certificate": null, + "defaultSslBinding": true + } + ], + "publicIPAddresses": null, + "privateIPAddresses": null, + "additionalLocations": null, + "virtualNetworkConfiguration": null, + "customProperties": { + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls10": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Protocols.Tls11": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls10": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Tls11": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Security.Backend.Protocols.Ssl30": "False", + "Microsoft.WindowsAzure.ApiManagement.Gateway.Protocols.Server.Http2": "False" + }, + "virtualNetworkType": "None", + "certificates": null, + "apiVersionConstraint": { + "minApiVersion": null + } + }, + "sku": { + "name": "Consumption", + "capacity": 0 + }, + "apiVersion": "2019-12-01" + }, + { + "type": "Microsoft.ApiManagement/service/apis", + "name": "ApiGeneratorSampleApIapi/ApiGeneratorSampleApI", + "properties": { + "displayName": "ApiGeneratorSampleApI", + "apiRevision": "1", + "description": null, + "subscriptionRequired": true, + "serviceUrl": null, + "path": "generated", + "protocols": [ + "https" + ], + "authenticationSettings": { + "oAuth2": null, + "openid": null + }, + "subscriptionKeyParameterNames": { + "header": "Ocp-Apim-Subscription-Key", + "query": "subscription-key" + }, + "isCurrent": true + }, + "apiVersion": "2019-12-01", + "dependsOn": [ + "ApiGeneratorSampleApIapi" + ] + } + ] + } + } + } + ], + "metadata": { + "_dependencyType": "apis.azure" + } +} \ No newline at end of file diff --git a/sample/ApiGeneratorSampleApp/Properties/serviceDependencies.apigenerator.json b/sample/ApiGeneratorSampleApp/Properties/serviceDependencies.apigenerator.json new file mode 100644 index 0000000..74df1f5 --- /dev/null +++ b/sample/ApiGeneratorSampleApp/Properties/serviceDependencies.apigenerator.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "apis1": { + "resourceId": "/subscriptions/[parameters('subscriptionId')]/resourceGroups/[parameters('resourceGroupName')]/providers/Microsoft.ApiManagement/service/ApiGeneratorSampleApIapi/apis/ApiGeneratorSampleApI", + "type": "apis.azure" + } + } +} \ No newline at end of file diff --git a/sample/ApiGeneratorSampleApp/Properties/serviceDependencies.json b/sample/ApiGeneratorSampleApp/Properties/serviceDependencies.json new file mode 100644 index 0000000..e32266d --- /dev/null +++ b/sample/ApiGeneratorSampleApp/Properties/serviceDependencies.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "apis1": { + "type": "apis" + } + } +} \ No newline at end of file From 226d5f6ec639a270fe1cd0bbd5a6c4ec21a46ca5 Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Tue, 29 Mar 2022 12:33:51 +0200 Subject: [PATCH 02/17] Added new options: * Enable / Disable ODATA completely * Enable XML Comments for swagger Fixed * Fixed a bug that prevented the service to start when the connectionstring wasn't set despite using InMemory --- sample/ApiGeneratorSampleApp/Model/Person.cs | 2 ++ sample/ApiGeneratorSampleApp/appsettings.json | 11 +++------- .../Data/GenericDbContext.cs | 11 +++++----- .../Extension/ApiGeneratorConfig.cs | 14 ++++++++++++ .../Extension/ApiGeneratorExtension.cs | 22 ++++++++++++++----- .../TCDev.APIGenerator.csproj | 2 +- 6 files changed, 43 insertions(+), 19 deletions(-) diff --git a/sample/ApiGeneratorSampleApp/Model/Person.cs b/sample/ApiGeneratorSampleApp/Model/Person.cs index 06d456c..c6f38db 100644 --- a/sample/ApiGeneratorSampleApp/Model/Person.cs +++ b/sample/ApiGeneratorSampleApp/Model/Person.cs @@ -22,6 +22,8 @@ public class Person : Trackable, IEntityTypeConfiguration // Configure Table Options yourself { public string Name { get; set; } + + public DateTime Date { get; set; } public string Description { get; set; } public int Age { get; set; } diff --git a/sample/ApiGeneratorSampleApp/appsettings.json b/sample/ApiGeneratorSampleApp/appsettings.json index d9adf60..dcd7605 100644 --- a/sample/ApiGeneratorSampleApp/appsettings.json +++ b/sample/ApiGeneratorSampleApp/appsettings.json @@ -12,9 +12,7 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform "CallbackPath": "/signin-oidc" }, - "ConnectionStrings": { - "ApiGeneratorDatabase": "Server=localhost;database=tcdev_dev_222;user=sa;password=Password!23;" - }, + "Logging": { "LogLevel": { "Default": "Information", @@ -25,10 +23,6 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform "AllowedHosts": "*", //Sample Config for API Generator "Api": { - "Database": { - "DatabaseType": "SQL" - } - } "Swagger": { "EnableProduction": "false", // Enable/Disable for production builds "Description": "Sample Swagger Config", @@ -38,9 +32,10 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform "ContactUri": "https://www.myuri.com" }, "Database": { - "DatabaseType": "SQL|InMemory|SQLite" + "DatabaseType": "InMemory" }, "Odata": { + "EnableOData": false, "EnableSelect": true, "EnableFilter": true, "EnableSort": true diff --git a/src/TCDev.APIGenerator/Data/GenericDbContext.cs b/src/TCDev.APIGenerator/Data/GenericDbContext.cs index 711ed46..f93e93f 100644 --- a/src/TCDev.APIGenerator/Data/GenericDbContext.cs +++ b/src/TCDev.APIGenerator/Data/GenericDbContext.cs @@ -43,13 +43,12 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { if (!optionsBuilder.IsConfigured) { - var config = new ApiGeneratorConfig(null); var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") + .AddJsonFile("secrets.json") .Build(); - var connectionString = configuration.GetConnectionString("ApiGeneratorDatabase"); - + var config = new ApiGeneratorConfig(configuration); // Add Database Context switch (config.DatabaseOptions.DatabaseType) @@ -58,10 +57,12 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) optionsBuilder.UseInMemoryDatabase("ApiGeneratorDB"); break; case DBType.SQL: - optionsBuilder.UseSqlServer(connectionString); + var connectionStringSQL = configuration.GetConnectionString("ApiGeneratorDatabase"); + optionsBuilder.UseSqlServer(connectionStringSQL); break; case DBType.SQLite: - optionsBuilder.UseSqlite(connectionString); + var connectionStringSQLite = configuration.GetConnectionString("ApiGeneratorDatabase"); + optionsBuilder.UseSqlite(connectionStringSQLite); break; default: throw new Exception("Database Type Unkown"); diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs index a8d9854..36461b9 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs @@ -24,19 +24,32 @@ public ApiGeneratorConfig(IConfiguration config) } //Load Options + configuration.Bind("API:Basic", APIOptions); configuration.Bind("Api:Cache", CacheOptions); configuration.Bind("Api:Swagger", SwaggerOptions); configuration.Bind("Api:Database", DatabaseOptions); + configuration.Bind("Api:Odata", ODataOptions); } private readonly IConfigurationRoot Configuration; public CacheOptions CacheOptions { get; set; } = new CacheOptions(); + + public APIOptions APIOptions { get; set; } = new APIOptions(); + public SwaggerOptions SwaggerOptions { get; set; } = new SwaggerOptions(); public DatabaseOptions DatabaseOptions { get; set; } = new DatabaseOptions(); + public ODataFunctions ODataOptions { get; set; } = new ODataFunctions(); public string MetadataRoute { get; set; } = "odata"; } + + public class APIOptions + { + public bool UseXMLComments { get; set; } = false; + public string XMLCommentsFile { get; set; } = string.Empty; + } + public class CacheOptions { public bool Enabled { get; set; } = true; @@ -61,6 +74,7 @@ public class DatabaseOptions public class ODataFunctions { + public bool EnableOData = false; public bool EnableSelect { get; set; } = true; public bool EnableFilter { get; set; } = true; public bool EnableSort { get; set; } = true; diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs index 1760972..d008096 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -87,19 +87,31 @@ public static class ApiGeneratorExtension }); c.DocumentFilter(); - //c.IncludeXmlComments($"{assembly.GetName().Name}.xml", true); + if(ApiGeneratorConfig.APIOptions.UseXMLComments) + { + if (!string.IsNullOrEmpty(ApiGeneratorConfig.APIOptions.XMLCommentsFile)) + { + throw new Exception("You need to set XMLCommentsFile option when using XMl Comments"); + } else { + c.IncludeXmlComments(ApiGeneratorConfig.APIOptions.XMLCommentsFile, true); + } + } + }); - services.AddControllers().AddOData(opt => + if(ApiGeneratorConfig.ODataOptions.EnableOData) + { + services.AddControllers().AddOData(opt => { opt.AddRouteComponents("odata", GenericDbContext.EdmModel); opt.EnableNoDollarQueryOptions = true; opt.EnableQueryFeatures(20000); opt.Select().Expand().Filter(); - } - ); - + }); + } else { + services.AddControllers(); + } return services; } diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj index c5a54c4..b148216 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj @@ -3,7 +3,7 @@ net6.0 TCDev.APIGenerator - 0.0.5-alpha + 0.0.8-alpha Tim Cadenbach TCDev Creates fully working CRUD Apis from just class files From 44e9c9943a84055f948a584de0b759d0395a6493 Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Tue, 29 Mar 2022 12:39:28 +0200 Subject: [PATCH 03/17] Update apigenerator.yml --- .github/workflows/apigenerator.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/apigenerator.yml b/.github/workflows/apigenerator.yml index 3262364..d86858a 100644 --- a/.github/workflows/apigenerator.yml +++ b/.github/workflows/apigenerator.yml @@ -43,8 +43,6 @@ jobs: - name: Install Swashbuckle CLI .NET Global Tool run: dotnet tool install --global Swashbuckle.AspNetCore.Cli --version ${{ env.SWASHBUCLE_ASPNET_CORE_CLI_PACKAGE_VERSION }} working-directory: ${{ env.WORKING_DIRECTORY }} - - name: Generate Open API Specification Document - run: swagger tofile --output "${{ env.API_IMPORT_SPECIFICATION_PATH }}" "${{ env.API_IMPORT_DLL }}" "${{ env.API_IMPORT_VERSION }}" - name: Publish Artifacts uses: actions/upload-artifact@v1.0.0 with: From bf5e7e08be3d5cba12859b5a9e2729f51f43f1fc Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Thu, 31 Mar 2022 17:05:07 +0200 Subject: [PATCH 04/17] Various fixes Fixed various issues, swagger now ignores read only props, few more things --- TCDev.APIGenerator.sln | 1 + .../ApiGeneratorSampleApI.csproj | 13 ++++--- sample/ApiGeneratorSampleApp/Model/Car.cs | 25 +++++++++++++ .../Model/MinimalSample.cs | 13 ++++++- sample/ApiGeneratorSampleApp/Model/Person.cs | 2 - sample/ApiGeneratorSampleApp/appsettings.json | 12 +++--- sample/SampleAppNuget/SampleAPis/Person.cs | 10 ++--- sample/SampleAppNuget/SampleAppNuget.csproj | 4 +- sample/SampleAppNuget/appsettings.json | 1 + .../Interfaces/IObjectBase.cs | 3 ++ .../TCDev.APIGenerator.Schema.csproj | 1 + .../Attributes/SwaggerIgnoreAttribute.cs | 18 +++++++++ .../Controller/GenericController.cs | 1 + .../Data/GenericDbContext.cs | 7 +++- .../Extension/ApiGeneratorConfig.cs | 4 +- .../Extension/ApiGeneratorExtension.cs | 33 ++++++++++++----- .../Extension/SwaggerSchemaFilter.cs | 37 +++++++++++++++++++ .../TCDev.APIGenerator.csproj | 4 +- 18 files changed, 153 insertions(+), 36 deletions(-) create mode 100644 sample/ApiGeneratorSampleApp/Model/Car.cs create mode 100644 src/TCDev.APIGenerator/Attributes/SwaggerIgnoreAttribute.cs create mode 100644 src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs diff --git a/TCDev.APIGenerator.sln b/TCDev.APIGenerator.sln index b748b30..6a6cdac 100644 --- a/TCDev.APIGenerator.sln +++ b/TCDev.APIGenerator.sln @@ -40,6 +40,7 @@ Global {FE869C02-6C9A-4D9B-BBE2-56F1B21B2A55}.Release|Any CPU.Build.0 = Release|Any CPU {FE869C02-6C9A-4D9B-BBE2-56F1B21B2A55}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU {303BF897-594C-4911-91CF-3887A8B8E839}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {303BF897-594C-4911-91CF-3887A8B8E839}.Debug|Any CPU.Build.0 = Debug|Any CPU {303BF897-594C-4911-91CF-3887A8B8E839}.DebugWithSampleApp|Any CPU.ActiveCfg = DebugWithSampleApp|Any CPU {303BF897-594C-4911-91CF-3887A8B8E839}.DebugWithSampleApp|Any CPU.Build.0 = DebugWithSampleApp|Any CPU {303BF897-594C-4911-91CF-3887A8B8E839}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj index d25d36b..8cfd33d 100644 --- a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj +++ b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj @@ -16,22 +16,23 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + - + diff --git a/sample/ApiGeneratorSampleApp/Model/Car.cs b/sample/ApiGeneratorSampleApp/Model/Car.cs new file mode 100644 index 0000000..a437724 --- /dev/null +++ b/sample/ApiGeneratorSampleApp/Model/Car.cs @@ -0,0 +1,25 @@ +using Swashbuckle.AspNetCore.Annotations; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Text.Json.Serialization; +using TCDev.ApiGenerator.Attributes; +using TCDev.ApiGenerator.Interfaces; + +namespace ApiGeneratorSampleApI.Model +{ + + [Api("/car")] + public class Car : IObjectBase + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [SwaggerSchema(ReadOnly = true)] + [SwaggerIgnore] + public Guid Id { get; set; } = Guid.NewGuid(); + + + [SwaggerSchema(ReadOnly = true)] + public string Name { get; set; } + } +} diff --git a/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs b/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs index 6526985..f1f2661 100644 --- a/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs +++ b/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs @@ -1,4 +1,6 @@ -using TCDev.ApiGenerator.Attributes; +using System; +using System.Text.Json.Serialization; +using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Interfaces; namespace ApiGeneratorSampleApI.Model @@ -14,4 +16,13 @@ public class MinimalSample : IObjectBase public string Name { get; set; } public int Value { get; set; } } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum OperationEnum + { + INSERT, + UPDATE, + DELETE + } + } diff --git a/sample/ApiGeneratorSampleApp/Model/Person.cs b/sample/ApiGeneratorSampleApp/Model/Person.cs index c6f38db..06d456c 100644 --- a/sample/ApiGeneratorSampleApp/Model/Person.cs +++ b/sample/ApiGeneratorSampleApp/Model/Person.cs @@ -22,8 +22,6 @@ public class Person : Trackable, IEntityTypeConfiguration // Configure Table Options yourself { public string Name { get; set; } - - public DateTime Date { get; set; } public string Description { get; set; } public int Age { get; set; } diff --git a/sample/ApiGeneratorSampleApp/appsettings.json b/sample/ApiGeneratorSampleApp/appsettings.json index dcd7605..8137544 100644 --- a/sample/ApiGeneratorSampleApp/appsettings.json +++ b/sample/ApiGeneratorSampleApp/appsettings.json @@ -12,7 +12,9 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform "CallbackPath": "/signin-oidc" }, - + "ConnectionStrings": { + "ApiGeneratorDatabase": "Server=localhost;database=tcdev_dev_222;user=sa;password=Password!23;" + }, "Logging": { "LogLevel": { "Default": "Information", @@ -27,7 +29,7 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform "EnableProduction": "false", // Enable/Disable for production builds "Description": "Sample Swagger Config", "Version": "v1", - "Title": "Sample Swagger Config Title", + "Title": "ssass Swagger Config Title", "ContactMail": "Me@me.de", "ContactUri": "https://www.myuri.com" }, @@ -35,10 +37,10 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform "DatabaseType": "InMemory" }, "Odata": { - "EnableOData": false, + "Enabled": true, "EnableSelect": true, - "EnableFilter": true, - "EnableSort": true + "EnableFilter": false, + "EnableSort": false } } } diff --git a/sample/SampleAppNuget/SampleAPis/Person.cs b/sample/SampleAppNuget/SampleAPis/Person.cs index 69895c7..a2f57b0 100644 --- a/sample/SampleAppNuget/SampleAPis/Person.cs +++ b/sample/SampleAppNuget/SampleAPis/Person.cs @@ -28,12 +28,6 @@ public class Person : Trackable, public Guid Id { get; set; } - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("MyFancyTableName"); - //....all the other EF Core Options - } - /// /// Before Delete Hook /// @@ -58,6 +52,10 @@ public Task BeforeUpdate(Person newPerson, Person oldPerson) return Task.FromResult(newPerson); } + public void Configure(EntityTypeBuilder builder) + { + + } } } \ No newline at end of file diff --git a/sample/SampleAppNuget/SampleAppNuget.csproj b/sample/SampleAppNuget/SampleAppNuget.csproj index 67ff66c..9c4ba98 100644 --- a/sample/SampleAppNuget/SampleAppNuget.csproj +++ b/sample/SampleAppNuget/SampleAppNuget.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -13,7 +13,7 @@ - + diff --git a/sample/SampleAppNuget/appsettings.json b/sample/SampleAppNuget/appsettings.json index 5d67d73..ab2ebdd 100644 --- a/sample/SampleAppNuget/appsettings.json +++ b/sample/SampleAppNuget/appsettings.json @@ -25,6 +25,7 @@ "Connection": "" }, "Odata": { + "Enabled": true, "EnableSelect": true, "EnableFilter": true, "EnableSort": true diff --git a/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs b/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs index 3623713..f672412 100644 --- a/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs +++ b/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs @@ -2,15 +2,18 @@ // Apache 2.0 License // https://www.github.com/deejaytc/dotnet-utils +using Swashbuckle.AspNetCore.Annotations; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; + namespace TCDev.ApiGenerator.Interfaces { public interface IObjectBase { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [SwaggerSchema(ReadOnly = true)] TId Id { get; set; } } } \ No newline at end of file diff --git a/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj b/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj index 00b4e92..1ad43bb 100644 --- a/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj +++ b/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj @@ -7,6 +7,7 @@ + diff --git a/src/TCDev.APIGenerator/Attributes/SwaggerIgnoreAttribute.cs b/src/TCDev.APIGenerator/Attributes/SwaggerIgnoreAttribute.cs new file mode 100644 index 0000000..e1e0159 --- /dev/null +++ b/src/TCDev.APIGenerator/Attributes/SwaggerIgnoreAttribute.cs @@ -0,0 +1,18 @@ +// TCDev 2022/03/16 +// Apache 2.0 License +// https://www.github.com/deejaytc/dotnet-utils + +using System; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace TCDev.ApiGenerator.Attributes +{ + [AttributeUsage(AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Enum)] + public class SwaggerIgnoreAttribute : Attribute + { + public SwaggerIgnoreAttribute() + { + } + } +} \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Controller/GenericController.cs b/src/TCDev.APIGenerator/Controller/GenericController.cs index 14d2110..31a142f 100644 --- a/src/TCDev.APIGenerator/Controller/GenericController.cs +++ b/src/TCDev.APIGenerator/Controller/GenericController.cs @@ -18,6 +18,7 @@ namespace TCDev.ApiGenerator { [Route("api/[controller]")] [Produces("application/json")] + public class GenericController : ODataController where T : class, IObjectBase diff --git a/src/TCDev.APIGenerator/Data/GenericDbContext.cs b/src/TCDev.APIGenerator/Data/GenericDbContext.cs index f93e93f..de7c265 100644 --- a/src/TCDev.APIGenerator/Data/GenericDbContext.cs +++ b/src/TCDev.APIGenerator/Data/GenericDbContext.cs @@ -20,8 +20,13 @@ namespace TCDev.ApiGenerator.Data { + + public class GenericDbContext : DbContext { + + + public GenericDbContext() { } @@ -46,7 +51,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) var configuration = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") - .AddJsonFile("secrets.json") + .AddJsonFile("secrets.json", true) .Build(); var config = new ApiGeneratorConfig(configuration); // Add Database Context diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs index 36461b9..09bf20e 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs @@ -24,7 +24,7 @@ public ApiGeneratorConfig(IConfiguration config) } //Load Options - configuration.Bind("API:Basic", APIOptions); + configuration.Bind("Api:Basic", APIOptions); configuration.Bind("Api:Cache", CacheOptions); configuration.Bind("Api:Swagger", SwaggerOptions); configuration.Bind("Api:Database", DatabaseOptions); @@ -74,7 +74,7 @@ public class DatabaseOptions public class ODataFunctions { - public bool EnableOData = false; + public bool Enabled { get; set; } = false; public bool EnableSelect { get; set; } = true; public bool EnableFilter { get; set; } = true; public bool EnableSort { get; set; } = true; diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs index d008096..82c6aee 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -87,7 +87,9 @@ public static class ApiGeneratorExtension }); c.DocumentFilter(); - if(ApiGeneratorConfig.APIOptions.UseXMLComments) + c.SchemaFilter(); + + if (ApiGeneratorConfig.APIOptions.UseXMLComments) { if (!string.IsNullOrEmpty(ApiGeneratorConfig.APIOptions.XMLCommentsFile)) { @@ -100,17 +102,30 @@ public static class ApiGeneratorExtension }); - if(ApiGeneratorConfig.ODataOptions.EnableOData) + if(ApiGeneratorConfig.ODataOptions.Enabled) { - services.AddControllers().AddOData(opt => + services + .AddControllers() + .AddOData(opt => + { + opt.AddRouteComponents("odata", GenericDbContext.EdmModel); + opt.EnableNoDollarQueryOptions = true; + opt.EnableQueryFeatures(20000); + opt.Select().Expand().Filter(); + }) + .AddJsonOptions(o => + { + o.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); + } + ); + + } else { + services + .AddControllers() + .AddJsonOptions(o => { - opt.AddRouteComponents("odata", GenericDbContext.EdmModel); - opt.EnableNoDollarQueryOptions = true; - opt.EnableQueryFeatures(20000); - opt.Select().Expand().Filter(); + o.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); }); - } else { - services.AddControllers(); } return services; diff --git a/src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs b/src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs new file mode 100644 index 0000000..c91f4b9 --- /dev/null +++ b/src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs @@ -0,0 +1,37 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using TCDev.ApiGenerator.Attributes; + +namespace TCDev.APIGenerator.Extension +{ + public class SwaggerSchemaFilter : ISchemaFilter + { + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (schema?.Properties == null) + { + return; + } + + var ignoreDataMemberProperties = context.Type.GetProperties() + .Where(t => t.GetCustomAttribute() != null); + + foreach (var ignoreDataMemberProperty in ignoreDataMemberProperties) + { + var propertyToHide = schema.Properties.Keys + .SingleOrDefault(x => x.ToLower() == ignoreDataMemberProperty.Name.ToLower()); + + if (propertyToHide != null) + { + schema.Properties.Remove(propertyToHide); + } + } + } + } +} diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj index b148216..9b94968 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj @@ -3,10 +3,10 @@ net6.0 TCDev.APIGenerator - 0.0.8-alpha + 0.1.1-alpha Tim Cadenbach TCDev - Creates fully working CRUD Apis from just class files + Creates fully working CRUD Apis from just models Debug;Release;DebugWithSampleApp;SampleAppNuget From e109513b5b1000077c9033edbea4da1f31bd24bb Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Fri, 1 Apr 2022 11:09:31 +0200 Subject: [PATCH 05/17] fixes and changes --- TCDev.APIGenerator.sln | 11 ++ sample/ApiGeneratorSampleApp/Model/Car.cs | 20 +++- src/TCDev.APIGenerator.DbFirst/Scaffolder.cs | 12 ++ .../TCDev.APIGenerator.DbFirst.csproj | 16 +++ .../Interfaces/IObjectBase.cs | 3 +- .../SwaggerIgnoreAttribute.cs | 0 .../Data/DBReader/DBScaffolder.cs | 111 ++++++++++++++++++ .../TCDev.APIGenerator.csproj | 4 +- 8 files changed, 172 insertions(+), 5 deletions(-) create mode 100644 src/TCDev.APIGenerator.DbFirst/Scaffolder.cs create mode 100644 src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.DbFirst.csproj rename src/{TCDev.APIGenerator/Attributes => TCDev.APIGenerator.Schema}/SwaggerIgnoreAttribute.cs (100%) create mode 100644 src/TCDev.APIGenerator/Data/DBReader/DBScaffolder.cs diff --git a/TCDev.APIGenerator.sln b/TCDev.APIGenerator.sln index 6a6cdac..fbc16f2 100644 --- a/TCDev.APIGenerator.sln +++ b/TCDev.APIGenerator.sln @@ -24,6 +24,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample App", "Sample App", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleAppNuget", "sample\SampleAppNuget\SampleAppNuget.csproj", "{BA9E04E6-4B66-4369-9B2F-C6CEC9499851}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TCDev.APIGenerator.DbFirst", "src\TCDev.APIGenerator.DbFirst\TCDev.APIGenerator.DbFirst.csproj", "{7F3574D1-7421-4824-A0BB-522F3BC9BAC4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -66,6 +68,14 @@ Global {BA9E04E6-4B66-4369-9B2F-C6CEC9499851}.Release|Any CPU.ActiveCfg = Release|Any CPU {BA9E04E6-4B66-4369-9B2F-C6CEC9499851}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU {BA9E04E6-4B66-4369-9B2F-C6CEC9499851}.SampleAppNuget|Any CPU.Build.0 = SampleAppNuget|Any CPU + {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.DebugWithSampleApp|Any CPU.ActiveCfg = DebugWithSampleApp|Any CPU + {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.DebugWithSampleApp|Any CPU.Build.0 = DebugWithSampleApp|Any CPU + {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU + {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.SampleAppNuget|Any CPU.Build.0 = SampleAppNuget|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -77,6 +87,7 @@ Global {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D} = {4189D7E0-F171-4267-AC64-C9A83BB1B559} {94E59385-D259-40A1-A373-1FBD0A42CD63} = {4189D7E0-F171-4267-AC64-C9A83BB1B559} {BA9E04E6-4B66-4369-9B2F-C6CEC9499851} = {8CC9B68F-E1C2-45B3-8814-B9FF4E1B2AB8} + {7F3574D1-7421-4824-A0BB-522F3BC9BAC4} = {4189D7E0-F171-4267-AC64-C9A83BB1B559} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {315BF454-8B91-42C5-A113-B59C72AE69C8} diff --git a/sample/ApiGeneratorSampleApp/Model/Car.cs b/sample/ApiGeneratorSampleApp/Model/Car.cs index a437724..b907c68 100644 --- a/sample/ApiGeneratorSampleApp/Model/Car.cs +++ b/sample/ApiGeneratorSampleApp/Model/Car.cs @@ -14,12 +14,28 @@ public class Car : IObjectBase { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - [SwaggerSchema(ReadOnly = true)] [SwaggerIgnore] public Guid Id { get; set; } = Guid.NewGuid(); + public string Name { get; set; } + + public string Description { get; set; } + + public string Color { get; set; } + + public Make? Make { get; set; } + } + - [SwaggerSchema(ReadOnly = true)] + [Api("/carMakes")] + public class Make : IObjectBase + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [SwaggerIgnore] + public Guid Id { get; set; } = Guid.NewGuid(); public string Name { get; set; } + + public string Description { get; set; } } } diff --git a/src/TCDev.APIGenerator.DbFirst/Scaffolder.cs b/src/TCDev.APIGenerator.DbFirst/Scaffolder.cs new file mode 100644 index 0000000..c52aaff --- /dev/null +++ b/src/TCDev.APIGenerator.DbFirst/Scaffolder.cs @@ -0,0 +1,12 @@ +// TCDev.de 2022/03/31 +// TCDev.APIGenerator.DbFirst.Scaffolder.cs +// https://www.github.com/deejaytc/dotnet-utils + +namespace TCDev.ApiGenerator.Caching; + +public class DBReader +{ + public bool ScaffoldDb() + { + } +} diff --git a/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.DbFirst.csproj b/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.DbFirst.csproj new file mode 100644 index 0000000..8e09e31 --- /dev/null +++ b/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.DbFirst.csproj @@ -0,0 +1,16 @@ + + + + net6.0 + Debug;Release;DebugWithSampleApp;SampleAppNuget + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + diff --git a/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs b/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs index f672412..794dbaf 100644 --- a/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs +++ b/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs @@ -5,7 +5,7 @@ using Swashbuckle.AspNetCore.Annotations; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; - +using TCDev.ApiGenerator.Attributes; namespace TCDev.ApiGenerator.Interfaces { @@ -14,6 +14,7 @@ public interface IObjectBase [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] [SwaggerSchema(ReadOnly = true)] + [SwaggerIgnore] TId Id { get; set; } } } \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Attributes/SwaggerIgnoreAttribute.cs b/src/TCDev.APIGenerator.Schema/SwaggerIgnoreAttribute.cs similarity index 100% rename from src/TCDev.APIGenerator/Attributes/SwaggerIgnoreAttribute.cs rename to src/TCDev.APIGenerator.Schema/SwaggerIgnoreAttribute.cs diff --git a/src/TCDev.APIGenerator/Data/DBReader/DBScaffolder.cs b/src/TCDev.APIGenerator/Data/DBReader/DBScaffolder.cs new file mode 100644 index 0000000..f408a10 --- /dev/null +++ b/src/TCDev.APIGenerator/Data/DBReader/DBScaffolder.cs @@ -0,0 +1,111 @@ +// TCDev.de 2022/03/31 +// TCDev.APIGenerator.DBScaffolder.cs +// https://www.github.com/deejaytc/dotnet-utils + +//using System.Collections.Generic; +//using System.Data.Common; +//using System.Diagnostics.CodeAnalysis; +//using System.Linq; +//using System.Linq.Expressions; +//using System.Reflection; +//using Microsoft.CodeAnalysis; +//using Microsoft.CodeAnalysis.CSharp; +//using Microsoft.EntityFrameworkCore.Design; +//using Microsoft.EntityFrameworkCore.Diagnostics; +//using Microsoft.EntityFrameworkCore.Scaffolding; +//using Microsoft.EntityFrameworkCore.Scaffolding.Internal; +//using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; +//using Microsoft.EntityFrameworkCore.SqlServer.Scaffolding.Internal; +//using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; +//using Microsoft.EntityFrameworkCore.Storage; +//using Microsoft.Extensions.DependencyInjection; + +//namespace TCDev.APIGenerator.Data.DBReader; + +//public class DbScaffolder +//{ +// [SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "We need it")] +// public static IReverseEngineerScaffolder CreateMssqlScaffolder() +// { +// return new ServiceCollection() +// .AddEntityFrameworkSqlServer() +// .AddLogging() +// .AddEntityFrameworkDesignTimeServices() +// .AddSingleton() +// .AddSingleton() +// .AddSingleton() +// .AddSingleton() +// .AddSingleton() +// .AddSingleton() +// .AddSingleton() +// .AddSingleton() +// .BuildServiceProvider() +// .GetRequiredService(); +// } + + +// public static List CompilationReferences(bool enableLazyLoading) +// { +// var refs = new List(); +// var referencedAssemblies = Assembly.GetExecutingAssembly() +// .GetReferencedAssemblies(); +// refs.AddRange(referencedAssemblies.Select(a => MetadataReference.CreateFromFile(Assembly.Load(a) +// .Location))); + +// refs.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); +// refs.Add(MetadataReference.CreateFromFile(Assembly.Load("netstandard, Version=2.0.0.0") +// .Location)); +// refs.Add(MetadataReference.CreateFromFile(typeof(DbConnection).Assembly.Location)); +// refs.Add(MetadataReference.CreateFromFile(typeof(Expression).Assembly.Location)); + +// //if (enableLazyLoading) +// //{ +// // refs.Add(MetadataReference.CreateFromFile(typeof(ProxiesExtensions).Assembly.Location)); +// //} + +// return refs; +// } + +// private static CSharpCompilation GenerateCode(List sourceFiles, bool enableLazyLoading) +// { +// var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp8); + +// var parsedSyntaxTrees = sourceFiles.Select(f => SyntaxFactory.ParseSyntaxTree(f, options)); + +// return CSharpCompilation.Create("DataContext.dll", +// parsedSyntaxTrees, +// CompilationReferences(enableLazyLoading), +// new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, +// optimizationLevel: OptimizationLevel.Release, +// assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); +// } +//} + + +// public static void GenerateClasses() +// { +// var connectionString = @""; + +// var scaffolder = CreateMssqlScaffolder(); + +// var dbOpts = new DatabaseModelFactoryOptions(); +// var modelOpts = new ModelReverseEngineerOptions(); +// var codeGenOpts = new ModelCodeGenerationOptions +// { +// RootNamespace = "TypedDataContext", +// ContextName = "DataContext", +// ContextNamespace = "TypedDataContext.Context", +// ModelNamespace = "TypedDataContext.Models", +// SuppressConnectionStringWarning = true +// }; + +// var scaffoldedModelSources = scaffolder.ScaffoldModel(connectionString, dbOpts, modelOpts, codeGenOpts); +// var sourceFiles = new List +// { +// scaffoldedModelSources.ContextFile.Code +// }; +// sourceFiles.AddRange(scaffoldedModelSources.AdditionalFiles.Select(f => f.Code)); +// } +//} + + diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj index 9b94968..0f3f383 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj @@ -33,12 +33,12 @@ + - - + all runtime; build; native; contentfiles; analyzers; buildtransitive From 3745e53e27e98cdbc57ca29cd08b7db2881896e0 Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Tue, 5 Apr 2022 20:30:53 +0200 Subject: [PATCH 06/17] Build API from Json --- TCDev.APIGenerator.sln | 2 +- .../ApiGeneratorSampleApI.csproj | 1 + .../ApiGeneratorSampleApI.xml | 2 +- .../Model/MinimalSample.cs | 42 +-- sample/ApiGeneratorSampleApp/Program.cs | 16 +- src/TCDev.APIGenerator.DbFirst/Generator.cs | 63 ++++ src/TCDev.APIGenerator.DbFirst/Sample.json | 22 ++ src/TCDev.APIGenerator.DbFirst/Scaffolder.cs | 12 - .../TCDev.APIGenerator.DbFirst.csproj | 16 -- .../TCDev.APIGenerator.Json.csproj | 17 ++ .../Attributes/GraphQLAttribute.cs | 51 ++-- .../Extension/ApiGeneratorExtension.cs | 37 ++- .../Schema/GenericGraphQLSchema.cs | 23 +- .../SwaggerIgnoreAttribute.cs | 0 .../Interfaces/{ => Hooks}/IAfterCreate.cs | 0 .../Interfaces/{ => Hooks}/IAfterDelete.cs | 0 .../Interfaces/{ => Hooks}/IAfterUpdate.cs | 0 .../Interfaces/{ => Hooks}/IBeforeCreate.cs | 0 .../Interfaces/{ => Hooks}/IBeforeDelete.cs | 0 .../Interfaces/{ => Hooks}/IBeforeUpdate.cs | 0 .../Interfaces/IObjectBase.cs | 3 - .../JsonClassDefinition.cs | 44 +++ .../TCDev.APIGenerator.Schema.csproj | 1 + .../Controller/GenericController.cs | 254 ++++++++-------- .../Data/GenericDbContext.cs | 250 ++++++++-------- .../Data/GenericRepository.cs | 217 +++++++------- .../Extension/ApiGeneratorConfig.cs | 155 +++++----- .../Extension/ApiGeneratorExtension.cs | 270 ++++++++++-------- .../GenericTypeControllerFeatureProvider.cs | 44 ++- src/TCDev.APIGenerator/Generator.cs | 105 +++++++ src/TCDev.APIGenerator/Model/BaseConfig.cs | 29 +- src/TCDev.APIGenerator/TCDev.APIGenerator.xml | 12 +- 32 files changed, 989 insertions(+), 699 deletions(-) create mode 100644 src/TCDev.APIGenerator.DbFirst/Generator.cs create mode 100644 src/TCDev.APIGenerator.DbFirst/Sample.json delete mode 100644 src/TCDev.APIGenerator.DbFirst/Scaffolder.cs delete mode 100644 src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.DbFirst.csproj create mode 100644 src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.Json.csproj rename src/TCDev.APIGenerator.Schema/{ => Attributes}/SwaggerIgnoreAttribute.cs (100%) rename src/TCDev.APIGenerator.Schema/Interfaces/{ => Hooks}/IAfterCreate.cs (100%) rename src/TCDev.APIGenerator.Schema/Interfaces/{ => Hooks}/IAfterDelete.cs (100%) rename src/TCDev.APIGenerator.Schema/Interfaces/{ => Hooks}/IAfterUpdate.cs (100%) rename src/TCDev.APIGenerator.Schema/Interfaces/{ => Hooks}/IBeforeCreate.cs (100%) rename src/TCDev.APIGenerator.Schema/Interfaces/{ => Hooks}/IBeforeDelete.cs (100%) rename src/TCDev.APIGenerator.Schema/Interfaces/{ => Hooks}/IBeforeUpdate.cs (100%) create mode 100644 src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs create mode 100644 src/TCDev.APIGenerator/Generator.cs diff --git a/TCDev.APIGenerator.sln b/TCDev.APIGenerator.sln index fbc16f2..6e2cf86 100644 --- a/TCDev.APIGenerator.sln +++ b/TCDev.APIGenerator.sln @@ -24,7 +24,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Sample App", "Sample App", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleAppNuget", "sample\SampleAppNuget\SampleAppNuget.csproj", "{BA9E04E6-4B66-4369-9B2F-C6CEC9499851}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TCDev.APIGenerator.DbFirst", "src\TCDev.APIGenerator.DbFirst\TCDev.APIGenerator.DbFirst.csproj", "{7F3574D1-7421-4824-A0BB-522F3BC9BAC4}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TCDev.APIGenerator.Json", "src\TCDev.APIGenerator.DbFirst\TCDev.APIGenerator.Json.csproj", "{7F3574D1-7421-4824-A0BB-522F3BC9BAC4}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj index 8cfd33d..f4fd3fc 100644 --- a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj +++ b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj @@ -33,6 +33,7 @@ + diff --git a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.xml b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.xml index 2d38add..c9fb4fb 100644 --- a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.xml +++ b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.xml @@ -6,7 +6,7 @@ - This is the minimal sample, yes this is a working api ;) + This is the minimal sample, yes this is a working api ;) diff --git a/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs b/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs index f1f2661..5a7a49e 100644 --- a/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs +++ b/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs @@ -1,28 +1,28 @@ -using System; +// TCDev.de 2022/03/24 +// ApiGeneratorSampleApI.MinimalSample.cs +// https://www.github.com/deejaytc/dotnet-utils + using System.Text.Json.Serialization; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Interfaces; -namespace ApiGeneratorSampleApI.Model -{ - - /// - /// This is the minimal sample, yes this is a working api ;) - /// - [Api("/minimal")] - public class MinimalSample : IObjectBase - { - public int Id { get; set; } - public string Name { get; set; } - public int Value { get; set; } - } +namespace ApiGeneratorSampleApI.Model; - [JsonConverter(typeof(JsonStringEnumConverter))] - public enum OperationEnum - { - INSERT, - UPDATE, - DELETE - } +/// +/// This is the minimal sample, yes this is a working api ;) +/// +[Api("/minimal")] +public class MinimalSample : IObjectBase +{ + public string Name { get; set; } + public int Value { get; set; } + public int Id { get; set; } +} +[JsonConverter(typeof(JsonStringEnumConverter))] +public enum OperationEnum +{ + Insert, + Update, + Delete } diff --git a/sample/ApiGeneratorSampleApp/Program.cs b/sample/ApiGeneratorSampleApp/Program.cs index e32e537..3d0fbaa 100644 --- a/sample/ApiGeneratorSampleApp/Program.cs +++ b/sample/ApiGeneratorSampleApp/Program.cs @@ -1,15 +1,20 @@ +// TCDev.de 2022/03/16 +// ApiGeneratorSampleApI.Program.cs +// https://www.github.com/deejaytc/dotnet-utils + +using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; -using System.Configuration; -using System.Reflection; -using TCDev.ApiGenerator.Data; using TCDev.ApiGenerator.Extension; +using TCDev.ApiGenerator.Json; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); + +//builder.Services.AddApiGeneratorServices(builder.Configuration, JsonClassBuilder.CreateClass()); builder.Services.AddApiGeneratorServices(builder.Configuration, Assembly.GetExecutingAssembly()); var app = builder.Build(); @@ -17,7 +22,7 @@ // Configure the HTTP request pipeline. app.UseApiGenerator(); -app.UseAutomaticAPIMigrations(true); +app.UseAutomaticApiMigrations(true); app.UseHttpsRedirection(); @@ -26,7 +31,8 @@ app.UseAuthentication(); app.UseAuthorization(); -app.UseEndpoints(endpoints => { +app.UseEndpoints(endpoints => +{ endpoints.UseApiGeneratorEndpoints(); endpoints.MapControllers(); }); diff --git a/src/TCDev.APIGenerator.DbFirst/Generator.cs b/src/TCDev.APIGenerator.DbFirst/Generator.cs new file mode 100644 index 0000000..67284c1 --- /dev/null +++ b/src/TCDev.APIGenerator.DbFirst/Generator.cs @@ -0,0 +1,63 @@ +// TCDev.de 2022/04/05 +// TCDev.APIGenerator.Generator.cs +// https://www.github.com/deejaytc/dotnet-utils + +using System.Diagnostics; +using System.IO; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; + +namespace TCDev.ApiGenerator.Json; + +public class JsonClassBuilder +{ + public const string TestClass = $@" // Auto-generated code + using System; + using Swashbuckle.AspNetCore.Annotations; + using System; + using System.ComponentModel.DataAnnotations; + using System.ComponentModel.DataAnnotations.Schema; + using System.Text.Json.Serialization; + using TCDev.ApiGenerator.Attributes; + using TCDev.ApiGenerator.Interfaces; + + namespace TCDev.ApiGenerator + {{ + [Api('/carsgen')] + public class CarsGenerated : IObjectBase + {{ + public int Id {{ get; set;}} + public string Name {{ get; set;}} + }} + }} + "; + + public void LoadJsonClass() + { + + var compilation = CSharpCompilation.Create("DynamicAssembly", new[] { CSharpSyntaxTree.ParseText(TestClass) }, + new[] + { + MetadataReference.CreateFromFile(typeof(object).Assembly.Location) + }, + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using (var ms = new MemoryStream()) + { + var emitResult = compilation.Emit(ms); + + if (!emitResult.Success) + { + // handle, log errors etc + Debug.WriteLine("Compilation failed!"); + return; + } + + ms.Seek(0, SeekOrigin.Begin); + var assembly = Assembly.Load(ms.ToArray()); + var candidates = assembly.GetExportedTypes(); + + } + } +} diff --git a/src/TCDev.APIGenerator.DbFirst/Sample.json b/src/TCDev.APIGenerator.DbFirst/Sample.json new file mode 100644 index 0000000..b2d504e --- /dev/null +++ b/src/TCDev.APIGenerator.DbFirst/Sample.json @@ -0,0 +1,22 @@ +[ + { + "name": "Car", + "route": "/cars", + "caching": true, + "cacheLiveTime": 1000, + "events": "POST,PUT,DELETE", + "idType": "int", + "Fields": [ + { + "name": "Name", + "type": "String", + "maxLength": "200" + }, + { + "name": "Description", + "type": "String", + "nullable": true + } + ] + } +] \ No newline at end of file diff --git a/src/TCDev.APIGenerator.DbFirst/Scaffolder.cs b/src/TCDev.APIGenerator.DbFirst/Scaffolder.cs deleted file mode 100644 index c52aaff..0000000 --- a/src/TCDev.APIGenerator.DbFirst/Scaffolder.cs +++ /dev/null @@ -1,12 +0,0 @@ -// TCDev.de 2022/03/31 -// TCDev.APIGenerator.DbFirst.Scaffolder.cs -// https://www.github.com/deejaytc/dotnet-utils - -namespace TCDev.ApiGenerator.Caching; - -public class DBReader -{ - public bool ScaffoldDb() - { - } -} diff --git a/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.DbFirst.csproj b/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.DbFirst.csproj deleted file mode 100644 index 8e09e31..0000000 --- a/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.DbFirst.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - net6.0 - Debug;Release;DebugWithSampleApp;SampleAppNuget - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - diff --git a/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.Json.csproj b/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.Json.csproj new file mode 100644 index 0000000..6aea5ed --- /dev/null +++ b/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.Json.csproj @@ -0,0 +1,17 @@ + + + + net6.0 + Debug;Release;DebugWithSampleApp;SampleAppNuget + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/src/TCDev.APIGenerator.GraphQL/Attributes/GraphQLAttribute.cs b/src/TCDev.APIGenerator.GraphQL/Attributes/GraphQLAttribute.cs index 493abdb..8208542 100644 --- a/src/TCDev.APIGenerator.GraphQL/Attributes/GraphQLAttribute.cs +++ b/src/TCDev.APIGenerator.GraphQL/Attributes/GraphQLAttribute.cs @@ -1,34 +1,33 @@ -// TCDev 2022/03/16 -// Apache 2.0 License +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.GraphQL.GraphQLAttribute.cs // https://www.github.com/deejaytc/dotnet-utils using System; -namespace TCDev.ApiGenerator.Attributes +namespace TCDev.ApiGenerator.Attributes; + +[AttributeUsage(AttributeTargets.Class)] +public class GraphQlAttribute : Attribute { - [AttributeUsage(AttributeTargets.Class)] - public class GraphQLAttribute : Attribute + public ApiAttributeAttributeOptions Options { get; set; } + + /// + /// Attribute defining auto generated controller for the class + /// + /// The full base route for the class ie /myclass/ + /// + /// + /// + /// + public GraphQlAttribute( + bool fireEvents = false, + bool authorize = true, + bool cache = false, + int cacheDuration = 50000) { - /// - /// Attribute defining auto generated controller for the class - /// - /// The full base route for the class ie /myclass/ - /// - /// - /// - /// - public GraphQLAttribute( - bool fireEvents = false, - bool authorize = true, - bool cache = false, - int cacheDuration = 50000) + this.Options = new ApiAttributeAttributeOptions { - Options = new ApiAttributeAttributeOptions - { - Authorize = authorize, Cache = cache, CacheDuration = cacheDuration, FireEvents = fireEvents - }; - } - - public ApiAttributeAttributeOptions Options { get; set; } + Authorize = authorize, Cache = cache, CacheDuration = cacheDuration, FireEvents = fireEvents + }; } -} \ No newline at end of file +} diff --git a/src/TCDev.APIGenerator.GraphQL/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator.GraphQL/Extension/ApiGeneratorExtension.cs index 9c031c6..2c40f92 100644 --- a/src/TCDev.APIGenerator.GraphQL/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator.GraphQL/Extension/ApiGeneratorExtension.cs @@ -1,5 +1,5 @@ -// TCDev 2022/03/16 -// Apache 2.0 License +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.GraphQL.ApiGeneratorExtension.cs // https://www.github.com/deejaytc/dotnet-utils using System.Reflection; @@ -9,28 +9,27 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -namespace TCDev.ApiGenerator.Extension +namespace TCDev.ApiGenerator.Extension; + +public static class ApiGeneratorExtension { - public static class ApiGeneratorExtension + public static IServiceCollection AddApiGeneratorGraphQl(this IServiceCollection services, IConfiguration config, Assembly assembly) { - public static IServiceCollection AddApiGeneratorGraphQL(this IServiceCollection services, IConfiguration config, Assembly assembly) - { - services.Configure(options => { options.AllowSynchronousIO = true; }); + services.Configure(options => { options.AllowSynchronousIO = true; }); - return services; - } + return services; + } - public static IApplicationBuilder UseAPIGeneratorGraphQL(this IApplicationBuilder app) - { - return app; - } + public static IApplicationBuilder UseApiGeneratorGraphQl(this IApplicationBuilder app) + { + return app; + } - public static IEndpointRouteBuilder UseGraphQLEndpoint(this IEndpointRouteBuilder endpoints) - { - endpoints.MapGraphQL(); - return endpoints; - } + public static IEndpointRouteBuilder UseGraphQlEndpoint(this IEndpointRouteBuilder endpoints) + { + endpoints.MapGraphQL(); + return endpoints; } -} \ No newline at end of file +} diff --git a/src/TCDev.APIGenerator.GraphQL/Schema/GenericGraphQLSchema.cs b/src/TCDev.APIGenerator.GraphQL/Schema/GenericGraphQLSchema.cs index a3d8079..326a501 100644 --- a/src/TCDev.APIGenerator.GraphQL/Schema/GenericGraphQLSchema.cs +++ b/src/TCDev.APIGenerator.GraphQL/Schema/GenericGraphQLSchema.cs @@ -1,20 +1,23 @@ -// TCDev 2022/03/16 -// Apache 2.0 License +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.GraphQL.GenericGraphQLSchema.cs // https://www.github.com/deejaytc/dotnet-utils using System; using System.Reflection; -namespace TCDev.ApiGenerator.GraphQL +namespace TCDev.ApiGenerator.GraphQL; + +public class GenericGraphQlSchema { - public class GenericGraphQLSchema + public GenericGraphQlSchema(IServiceProvider sp) { - public GenericGraphQLSchema(IServiceProvider sp) + string[] assemblies = + { + Assembly.GetEntryAssembly() + .FullName + }; + foreach (var assembly in assemblies) { - string[] assemblies = {Assembly.GetEntryAssembly().FullName}; - foreach (var assembly in assemblies) - { - } } } -} \ No newline at end of file +} diff --git a/src/TCDev.APIGenerator.Schema/SwaggerIgnoreAttribute.cs b/src/TCDev.APIGenerator.Schema/Attributes/SwaggerIgnoreAttribute.cs similarity index 100% rename from src/TCDev.APIGenerator.Schema/SwaggerIgnoreAttribute.cs rename to src/TCDev.APIGenerator.Schema/Attributes/SwaggerIgnoreAttribute.cs diff --git a/src/TCDev.APIGenerator.Schema/Interfaces/IAfterCreate.cs b/src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IAfterCreate.cs similarity index 100% rename from src/TCDev.APIGenerator.Schema/Interfaces/IAfterCreate.cs rename to src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IAfterCreate.cs diff --git a/src/TCDev.APIGenerator.Schema/Interfaces/IAfterDelete.cs b/src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IAfterDelete.cs similarity index 100% rename from src/TCDev.APIGenerator.Schema/Interfaces/IAfterDelete.cs rename to src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IAfterDelete.cs diff --git a/src/TCDev.APIGenerator.Schema/Interfaces/IAfterUpdate.cs b/src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IAfterUpdate.cs similarity index 100% rename from src/TCDev.APIGenerator.Schema/Interfaces/IAfterUpdate.cs rename to src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IAfterUpdate.cs diff --git a/src/TCDev.APIGenerator.Schema/Interfaces/IBeforeCreate.cs b/src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IBeforeCreate.cs similarity index 100% rename from src/TCDev.APIGenerator.Schema/Interfaces/IBeforeCreate.cs rename to src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IBeforeCreate.cs diff --git a/src/TCDev.APIGenerator.Schema/Interfaces/IBeforeDelete.cs b/src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IBeforeDelete.cs similarity index 100% rename from src/TCDev.APIGenerator.Schema/Interfaces/IBeforeDelete.cs rename to src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IBeforeDelete.cs diff --git a/src/TCDev.APIGenerator.Schema/Interfaces/IBeforeUpdate.cs b/src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IBeforeUpdate.cs similarity index 100% rename from src/TCDev.APIGenerator.Schema/Interfaces/IBeforeUpdate.cs rename to src/TCDev.APIGenerator.Schema/Interfaces/Hooks/IBeforeUpdate.cs diff --git a/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs b/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs index 794dbaf..5f2f71d 100644 --- a/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs +++ b/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs @@ -12,9 +12,6 @@ namespace TCDev.ApiGenerator.Interfaces public interface IObjectBase { [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - [SwaggerSchema(ReadOnly = true)] - [SwaggerIgnore] TId Id { get; set; } } } \ No newline at end of file diff --git a/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs b/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs new file mode 100644 index 0000000..672b926 --- /dev/null +++ b/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs @@ -0,0 +1,44 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json.Serialization; +using System.Threading.Tasks; + +namespace TCDev.APIGenerator.Schema +{ + [Flags] + public enum Events + { + POST, + PUT, + DELETE, + ALL = POST | PUT | DELETE + } + + + public class JsonClassDefinition + { + public string Name { get; set; } + + [JsonProperty("route")] + public string RouteTemplate { get; set; } = "/"; + + [JsonProperty("caching")] + public bool EnableCaching { get; set; } = false; + + [JsonProperty("idType")] + public string IdType { get; set; } = "int"; + + public List Fields { get; set; } + } + + + public class Field + { + public string Name { get; set; } + public string Type { get; set; } + } +} diff --git a/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj b/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj index 1ad43bb..c1e5bff 100644 --- a/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj +++ b/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj @@ -7,6 +7,7 @@ + diff --git a/src/TCDev.APIGenerator/Controller/GenericController.cs b/src/TCDev.APIGenerator/Controller/GenericController.cs index 31a142f..37994c2 100644 --- a/src/TCDev.APIGenerator/Controller/GenericController.cs +++ b/src/TCDev.APIGenerator/Controller/GenericController.cs @@ -1,6 +1,6 @@ -// TCDev 2022/03/16 -// Apache 2.0 License -// https://www.github.com/deejaytc/dotnet-utils +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.GenericController.cs +// https://github.com/DeeJayTC/net-dynamic-api using System; using System.Collections.Generic; @@ -14,169 +14,167 @@ using TCDev.ApiGenerator.Data; using TCDev.ApiGenerator.Interfaces; -namespace TCDev.ApiGenerator +namespace TCDev.ApiGenerator; + +[Route("api/[controller]")] +[Produces("application/json")] +public class GenericController : ODataController + where T : class, + IObjectBase { - [Route("api/[controller]")] - [Produces("application/json")] + private bool UseCache { get; set; } + private bool FireEvent { get; set; } + + private readonly IAuthorizationService authorizationService; + private readonly IGenericRespository repository; + + public ApiMethodsToGenerate MethodsToGenerate; - public class GenericController : ODataController - where T : class, - IObjectBase + public GenericController(IAuthorizationService authorizationService, IGenericRespository repository) { - public GenericController(IAuthorizationService authorizationService, IGenericRespository repository) + this.repository = repository; + this.authorizationService = authorizationService; + + ConfigureController(); + } + + private void ConfigureController() + { + // Get attribute config from underlying type T + var attrs = Attribute.GetCustomAttributes(typeof(T)); + if (attrs.FirstOrDefault(p => p.GetType() == typeof(ApiAttribute)) is ApiAttribute optionAttrib) { - _repository = repository; - _authorizationService = authorizationService; + this.UseCache = optionAttrib.Options.Cache; + this.FireEvent = optionAttrib.Options.FireEvents; + this.MethodsToGenerate = optionAttrib.Options.Methods; - ConfigureController(); + // Check if we need to remove methods.. + } + else + { + throw new Exception($"Could not find ApiAttribute on Class: {typeof(T)}"); } + } + + /// + /// Returns a list of entries + /// + /// + [Produces("application/json")] + [ProducesErrorResponseType(typeof(BadRequestResult))] + [HttpGet] + [EnableQuery( + AllowedQueryOptions = AllowedQueryOptions.All, + AllowedFunctions = AllowedFunctions.All, + MaxTop = 200, + MaxSkip = 199, + PageSize = 20)] + public IActionResult Query() + { + // Check if post is enabled + if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) + return BadRequest($"GET is disabled for {typeof(T).Name}"); - private readonly IAuthorizationService _authorizationService; - private readonly IGenericRespository _repository; - private bool useCache { get; set; } - private bool fireEvent { get; set; } + if (!this.ModelState.IsValid) + return BadRequest(); - public ApiMethodsToGenerate methodsToGenerate; + return Ok(this.repository.Get()); + } - private void ConfigureController() - { - // Get attribute config from underlying type T - var attrs = Attribute.GetCustomAttributes(typeof(T)); - if (attrs.FirstOrDefault(p => p.GetType() == typeof(ApiAttribute)) is ApiAttribute optionAttrib) - { - useCache = optionAttrib.Options.Cache; - fireEvent = optionAttrib.Options.FireEvents; - methodsToGenerate = optionAttrib.Options.Methods; + [HttpGet("{id}")] + public async Task Find(TEntityId id) + { + // Check if post is enabled + if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) + return BadRequest($"GET is disabled for {typeof(T).Name}"); - // Check if we need to remove methods.. + if (!this.ModelState.IsValid) + return BadRequest(); + var record = await this.repository.GetAsync(id); - } - else - { - throw new Exception($"Could not find ApiAttribute on Class: {typeof(T)}"); - } - } + return Ok(record); + } + - /// - /// Returns a list of entries - /// - /// - [Produces("application/json")] - [ProducesErrorResponseType(typeof(BadRequestResult))] - [HttpGet] - [EnableQuery( - AllowedQueryOptions = AllowedQueryOptions.All, - AllowedFunctions = AllowedFunctions.All, - MaxTop = 200, - MaxSkip = 199, - PageSize = 20)] - public IActionResult Query() + [HttpPost] + public async Task Create([FromBody] T record) + { + try { // Check if post is enabled - if (!methodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) - return BadRequest($"GET is disabled for {typeof(T).Name}"); + if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Insert)) + return BadRequest($"POST is disabled for {record.GetType().Name}"); - if (!ModelState.IsValid) + // Check if payload is valid + if (!this.ModelState.IsValid) return BadRequest(); - return Ok(_repository.Get()); + // Create the new entry + this.repository.Create(record); + await this.repository.SaveAsync(); + + // respond with the newly created record + return CreatedAtAction("Find", new + { + id = record.Id + }, record); } + catch (Exception ex) + { + return BadRequest(ex); + } + } - [HttpGet("{id}")] - public async Task Find(TEntityId id) + [HttpPut("{id}")] + public async Task Update(TEntityId id, [FromBody] T record) + { + try { - // Check if post is enabled - if (!methodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) - return BadRequest($"GET is disabled for {typeof(T).Name}"); + if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Update)) + return BadRequest($"PUT is disabled for {record.GetType().Name}"); - if (!ModelState.IsValid) + if (!this.ModelState.IsValid) return BadRequest(); - var record = await _repository.GetAsync(id); + var existingRecord = await this.repository.GetAsync(id); + if (existingRecord == null) return NotFound(); + this.repository.Update(record, existingRecord); + await this.repository.SaveAsync(); return Ok(record); } - - - [HttpPost] - public async Task Create([FromBody] T record) + catch (Exception ex) { - try - { - - // Check if post is enabled - if (!methodsToGenerate.HasFlag(ApiMethodsToGenerate.Insert)) - return BadRequest($"POST is disabled for {record.GetType().Name}"); - - // Check if payload is valid - if (!ModelState.IsValid) - return BadRequest(); - - // Create the new entry - _repository.Create(record); - await _repository.SaveAsync(); - - // respond with the newly created record - return CreatedAtAction("Find", new { id = record.Id }, record); - } - catch (Exception ex) - { - return BadRequest(ex); - } + return BadRequest(ex.Message); } + } - [HttpPut("{id}")] - public async Task Update(TEntityId id, [FromBody] T record) + [HttpDelete("{id}")] + public async Task Delete(TEntityId id) + { + try { - try - { - if (!methodsToGenerate.HasFlag(ApiMethodsToGenerate.Update)) - return BadRequest($"PUT is disabled for {record.GetType().Name}"); + if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Delete)) + return BadRequest("DELETE is disabled"); - if (!ModelState.IsValid) - return BadRequest(); + if (!this.ModelState.IsValid) + return BadRequest(); - var existingRecord = await _repository.GetAsync(id); - if (existingRecord == null) return NotFound(); + var existingRecord = await this.repository.GetAsync(id); + if (existingRecord == null) return NotFound(); - _repository.Update(record, existingRecord); - await _repository.SaveAsync(); + this.repository.Delete(id); + if (await this.repository.SaveAsync() == 0) + return BadRequest(); - return Ok(record); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } + return NoContent(); } - - [HttpDelete("{id}")] - public async Task Delete(TEntityId id) + catch (Exception ex) { - try - { - if (!methodsToGenerate.HasFlag(ApiMethodsToGenerate.Delete)) - return BadRequest($"DELETE is disabled"); - - if (!ModelState.IsValid) - return BadRequest(); - - var existingRecord = await _repository.GetAsync(id); - if (existingRecord == null) return NotFound(); - - _repository.Delete(id); - if (await _repository.SaveAsync() == 0) - return BadRequest(); - - return NoContent(); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } + return BadRequest(ex.Message); } } -} \ No newline at end of file +} diff --git a/src/TCDev.APIGenerator/Data/GenericDbContext.cs b/src/TCDev.APIGenerator/Data/GenericDbContext.cs index de7c265..b56f597 100644 --- a/src/TCDev.APIGenerator/Data/GenericDbContext.cs +++ b/src/TCDev.APIGenerator/Data/GenericDbContext.cs @@ -1,6 +1,6 @@ -// TCDev 2022/03/16 -// Apache 2.0 License -// https://www.github.com/deejaytc/dotnet-utils +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.GenericDbContext.cs +// https://github.com/DeeJayTC/net-dynamic-api using System; using System.IO; @@ -16,158 +16,154 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; using TCDev.ApiGenerator.Attributes; -using TCDev.ApiGenerator.Extension; -namespace TCDev.ApiGenerator.Data -{ - - - public class GenericDbContext : DbContext - { +namespace TCDev.ApiGenerator.Data; +public class GenericDbContext : DbContext +{ + protected IHttpContextAccessor HttpContextAccessor { get; } - - public GenericDbContext() - { - } + public static IModel StaticModel { get; } = BuildStaticModel(); + public static IEdmModel EdmModel { get; } = GetEdmModel(); - public GenericDbContext( - DbContextOptions options, - IConfiguration config, - IHttpContextAccessor httpContextAccessor) : base(options) - { - HttpContextAccessor = httpContextAccessor; - } - protected IHttpContextAccessor HttpContextAccessor { get; } + public GenericDbContext() + { + } - public static IModel StaticModel { get; } = BuildStaticModel(); - public static IEdmModel EdmModel { get; } = GetEdmModel(); + public GenericDbContext( + DbContextOptions options, + IConfiguration config, + IHttpContextAccessor httpContextAccessor) : base(options) + { + this.HttpContextAccessor = httpContextAccessor; + } - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) { - if (!optionsBuilder.IsConfigured) + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .AddJsonFile("secrets.json", true) + .Build(); + var config = new ApiGeneratorConfig(configuration); + // Add Database Context + + switch (config.DatabaseOptions.DatabaseType) { - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json") - .AddJsonFile("secrets.json", true) - .Build(); - var config = new ApiGeneratorConfig(configuration); - // Add Database Context - - switch (config.DatabaseOptions.DatabaseType) - { - case DBType.InMemory: - optionsBuilder.UseInMemoryDatabase("ApiGeneratorDB"); - break; - case DBType.SQL: - var connectionStringSQL = configuration.GetConnectionString("ApiGeneratorDatabase"); - optionsBuilder.UseSqlServer(connectionStringSQL); - break; - case DBType.SQLite: - var connectionStringSQLite = configuration.GetConnectionString("ApiGeneratorDatabase"); - optionsBuilder.UseSqlite(connectionStringSQLite); - break; - default: - throw new Exception("Database Type Unkown"); - } - - + case DbType.InMemory: + optionsBuilder.UseInMemoryDatabase("ApiGeneratorDB"); + break; + case DbType.Sql: + var connectionStringSql = configuration.GetConnectionString("ApiGeneratorDatabase"); + optionsBuilder.UseSqlServer(connectionStringSql); + break; + case DbType.SqLite: + var connectionStringSqLite = configuration.GetConnectionString("ApiGeneratorDatabase"); + optionsBuilder.UseSqlite(connectionStringSqLite); + break; + default: + throw new Exception("Database Type Unkown"); } } + } - // -> Tenant Isolation - //public void SetGlobalQuery(ModelBuilder builder) where T : EntityBase - //{ - // var user = HttpContextAccessor.HttpContext.GetUser(); - // builder.Entity().HasKey(e => e.Id); - // builder.Entity().HasQueryFilter(e => e.TenantId == user.TenantId); - //} - - protected override void OnModelCreating(ModelBuilder builder) - { - // Add all types T using IEntityTypeConfiguration - builder.ApplyConfigurationsFromAssembly(Assembly.GetEntryAssembly()); - - // Add all other types (auto mode) - var customTypes = Assembly.GetEntryAssembly().GetExportedTypes() - .Where(x => x.GetCustomAttributes().Any()); - foreach (var customType in customTypes.Where(x => x.GetInterface("IEntityTypeConfiguration`1") == null)) - builder.Entity(customType); + // -> Tenant Isolation + //public void SetGlobalQuery(ModelBuilder builder) where T : EntityBase + //{ + // var user = HttpContextAccessor.HttpContext.GetUser(); + // builder.Entity().HasKey(e => e.Id); + // builder.Entity().HasQueryFilter(e => e.TenantId == user.TenantId); + //} - base.OnModelCreating(builder); - } + protected override void OnModelCreating(ModelBuilder builder) + { + // Add all types T using IEntityTypeConfiguration + builder.ApplyConfigurationsFromAssembly(Assembly.GetEntryAssembly()); + + // Add all other types (auto mode) + var customTypes = Assembly.GetEntryAssembly() + .GetExportedTypes() + .Where(x => x.GetCustomAttributes() + .Any()); + foreach (var customType in customTypes.Where(x => x.GetInterface("IEntityTypeConfiguration`1") == null)) + builder.Entity(customType); + + base.OnModelCreating(builder); + } - /// - /// Generate EDM Model for OData functionalities - /// - /// - public static IEdmModel GetEdmModel() + /// + /// Generate EDM Model for OData functionalities + /// + /// + public static IEdmModel GetEdmModel() + { + var customTypes = Assembly.GetEntryAssembly() + .GetExportedTypes() + .Where(x => x.GetCustomAttributes() + .Any()); + var builder = new ODataConventionModelBuilder(); + foreach (var customType in customTypes) { - var customTypes = Assembly.GetEntryAssembly().GetExportedTypes() - .Where(x => x.GetCustomAttributes().Any()); - var builder = new ODataConventionModelBuilder(); - foreach (var customType in customTypes) - { - var newType = builder.AddEntityType(customType); - } - - return builder.GetEdmModel(); + var newType = builder.AddEntityType(customType); } + return builder.GetEdmModel(); + } - //public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) - //{ - // //var entries = ChangeTracker - // // .Entries() - // // .Where(e => - // // e.Entity is IObjectBase - // // && (e.State == EntityState.Added - // // || e.State == EntityState.Modified - // // || e.State == EntityState.Deleted - // // ) - // // ); - // //UpdateEntries(user, entries); - // //UpdateEntries(user, entries); - // //UpdateEntries(user, entries); + //public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) + //{ + // //var entries = ChangeTracker + // // .Entries() + // // .Where(e => + // // e.Entity is IObjectBase + // // && (e.State == EntityState.Added + // // || e.State == EntityState.Modified + // // || e.State == EntityState.Deleted + // // ) + // // ); - // return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); - //} + // //UpdateEntries(user, entries); + // //UpdateEntries(user, entries); + // //UpdateEntries(user, entries); + // return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); + //} - private static IModel BuildStaticModel() - { - using var dbContext = new GenericDbContext(); - return dbContext.Model; - } + private static IModel BuildStaticModel() + { + using var dbContext = new GenericDbContext(); + return dbContext.Model; + } - #region If you're targeting EF Core - public override int SaveChanges() - { - return this.SaveChangesWithTriggers(base.SaveChanges); - } + #region If you're targeting EF Core - public override int SaveChanges(bool acceptAllChangesOnSuccess) - { - return this.SaveChangesWithTriggers(base.SaveChanges, acceptAllChangesOnSuccess); - } + public override int SaveChanges() + { + return this.SaveChangesWithTriggers(base.SaveChanges); + } - public override Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, true, cancellationToken); - } + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + return this.SaveChangesWithTriggers(base.SaveChanges, acceptAllChangesOnSuccess); + } - public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, - CancellationToken cancellationToken = default) - { - return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, acceptAllChangesOnSuccess, cancellationToken); - } + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, true, cancellationToken); + } - #endregion + public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, + CancellationToken cancellationToken = default) + { + return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, acceptAllChangesOnSuccess, cancellationToken); } -} \ No newline at end of file + + #endregion +} diff --git a/src/TCDev.APIGenerator/Data/GenericRepository.cs b/src/TCDev.APIGenerator/Data/GenericRepository.cs index 8323caa..6696b99 100644 --- a/src/TCDev.APIGenerator/Data/GenericRepository.cs +++ b/src/TCDev.APIGenerator/Data/GenericRepository.cs @@ -1,5 +1,5 @@ -// TCDev 2022/03/16 -// Apache 2.0 License +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.GenericRepository.cs // https://www.github.com/deejaytc/dotnet-utils using System; @@ -9,133 +9,148 @@ using TCDev.ApiGenerator.Interfaces; using TCDev.APIGenerator.Schema.Interfaces; -namespace TCDev.ApiGenerator.Data +namespace TCDev.ApiGenerator.Data; + +public class GenericRespository : IGenericRespository + where TEntity : class, IObjectBase { - public class GenericRespository : IGenericRespository - where TEntity : class, IObjectBase + private DbContext context; + + public GenericRespository(GenericDbContext context) { - public GenericRespository(GenericDbContext context) - { - _context = context; - } + this.context = context; + } - private DbContext _context; + public IQueryable Get() + { + return this.context.Set(); + } - public IQueryable Get() - { - return _context.Set(); - } + public TEntity Get(TEntityId id) + { + return Get() + .SingleOrDefault(e => e.Id.ToString() == id.ToString()); + } - public TEntity Get(TEntityId id) - { - return Get().SingleOrDefault(e => e.Id.ToString() == id.ToString()); - } + public async Task GetAsync(TEntityId id) + { + return await Get() + .SingleOrDefaultAsync(e => e.Id.ToString() == id.ToString()); + } - public async Task GetAsync(TEntityId id) - { - return await Get().SingleOrDefaultAsync(e => e.Id.ToString() == id.ToString()); - } + public void Create(TEntity record) + { + var now = DateTime.UtcNow; - public void Create(TEntity record) - { - var now = DateTime.UtcNow; + this.context.Add(record); - _context.Add(record); + if (typeof(TEntity).IsAssignableFrom(typeof(IHasTrackingFields))) + this.context.Entry(record) + .Property("Created") + .CurrentValue = now; + } - if (typeof(TEntity).IsAssignableFrom(typeof(IHasTrackingFields))) _context.Entry(record).Property("Created").CurrentValue = now; + public async void Update(TEntity newRecord, TEntity oldRecord) + { + // We have a before update handler + if (typeof(TEntity).IsAssignableTo(typeof(IBeforeUpdate))) + { + var baseEntity = newRecord as IBeforeUpdate; + newRecord = await baseEntity.BeforeUpdate(newRecord, oldRecord); } - public async void Update(TEntity newRecord, TEntity oldRecord) + this.context.Set() + .Attach(oldRecord); + oldRecord = newRecord; + + if (typeof(TEntity).IsAssignableFrom(typeof(IHasTrackingFields))) { - // We have a before update handler - if (typeof(TEntity).IsAssignableTo(typeof(IBeforeUpdate))) - { - var baseEntity = newRecord as IBeforeUpdate; - newRecord = await baseEntity.BeforeUpdate(newRecord, oldRecord); - } + this.context.Entry(newRecord) + .Property("LastModified") + .CurrentValue = DateTime.UtcNow; + this.context.Entry(newRecord) + .State = EntityState.Modified; + } - _context.Set().Attach(oldRecord); - oldRecord = newRecord; + await this.context.SaveChangesAsync(); - if (typeof(TEntity).IsAssignableFrom(typeof(IHasTrackingFields))) - { - _context.Entry(newRecord).Property("LastModified").CurrentValue = DateTime.UtcNow; - _context.Entry(newRecord).State = EntityState.Modified; - } + // We have a after update handler + if (typeof(TEntity).IsAssignableTo(typeof(IAfterUpdate))) + { + var baseEntity = newRecord as IAfterUpdate; + await baseEntity.AfterUpdate(newRecord, oldRecord); + } + } - await _context.SaveChangesAsync(); + public void Delete(TEntityId id) + { + var record = Get(id); - // We have a after update handler - if (typeof(TEntity).IsAssignableTo(typeof(IAfterUpdate))) + if (record != null) + { + // If the entity is using softdelete -> only mark as deleted + if (typeof(TEntity).IsAssignableFrom(typeof(ISoftDelete))) { - var baseEntity = newRecord as IAfterUpdate; - await baseEntity.AfterUpdate(newRecord, oldRecord); + this.context.Entry(record) + .Property("Deleted") + .CurrentValue = DateTime.UtcNow; + this.context.Entry(record) + .Property("IsDeleted") + .CurrentValue = true; + this.context.Entry(record) + .State = EntityState.Modified; } - } - - public void Delete(TEntityId id) - { - var record = Get(id); - - if (record != null) + else { - // If the entity is using softdelete -> only mark as deleted - if (typeof(TEntity).IsAssignableFrom(typeof(ISoftDelete))) - { - _context.Entry(record).Property("Deleted").CurrentValue = DateTime.UtcNow; - _context.Entry(record).Property("IsDeleted").CurrentValue = true; - _context.Entry(record).State = EntityState.Modified; - } - else - { - _context.Remove(record); - } + this.context.Remove(record); } } + } - public Task SaveAsync() - { - return _context.SaveChangesAsync(); - } - + public Task SaveAsync() + { + return this.context.SaveChangesAsync(); + } - public int Save() - { - return _context.SaveChanges(); - } - public IQueryable GetQuery(Type EntityType) - { - var pq = from p in GetType().GetProperties() - where p.PropertyType.IsGenericType - && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>) - && p.PropertyType.GenericTypeArguments[0] == EntityType - select p; - var prop = pq.Single(); - - return (IQueryable) prop.GetValue(this); - } + public int Save() + { + return this.context.SaveChanges(); + } - #region Dispose + public IQueryable GetQuery(Type entityType) + { + var pq = + from p in GetType() + .GetProperties() + where p.PropertyType.IsGenericType + && p.PropertyType.GetGenericTypeDefinition() == typeof(DbSet<>) + && p.PropertyType.GenericTypeArguments[0] == entityType + select p; + var prop = pq.Single(); + + return (IQueryable)prop.GetValue(this); + } - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } + #region Dispose - protected virtual void Dispose(bool disposing) - { - if (disposing) - if (_context != null) - { - _context.Dispose(); - _context = null; - } - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } - #endregion + protected virtual void Dispose(bool disposing) + { + if (disposing) + if (this.context != null) + { + this.context.Dispose(); + this.context = null; + } } -} \ No newline at end of file + + #endregion +} diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs index 09bf20e..b73fccb 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs @@ -1,96 +1,91 @@ -// TCDev 2022/03/16 -// Apache 2.0 License -// https://www.github.com/deejaytc/dotnet-utils +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.ApiGeneratorConfig.cs +// https://github.com/DeeJayTC/net-dynamic-api -using System; using System.IO; using Microsoft.Extensions.Configuration; -namespace TCDev.ApiGenerator.Extension +namespace TCDev.ApiGenerator; + +public class ApiGeneratorConfig { - public class ApiGeneratorConfig - { - IConfiguration configuration; - public ApiGeneratorConfig(IConfiguration config) - { - configuration = config; - if(configuration == null) { - configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json") - .AddJsonFile("secrets.json", true) - .AddEnvironmentVariables() - .Build(); - } - - //Load Options - configuration.Bind("Api:Basic", APIOptions); - configuration.Bind("Api:Cache", CacheOptions); - configuration.Bind("Api:Swagger", SwaggerOptions); - configuration.Bind("Api:Database", DatabaseOptions); - configuration.Bind("Api:Odata", ODataOptions); - } - - private readonly IConfigurationRoot Configuration; - public CacheOptions CacheOptions { get; set; } = new CacheOptions(); - - public APIOptions APIOptions { get; set; } = new APIOptions(); - - public SwaggerOptions SwaggerOptions { get; set; } = new SwaggerOptions(); - public DatabaseOptions DatabaseOptions { get; set; } = new DatabaseOptions(); - public ODataFunctions ODataOptions { get; set; } = new ODataFunctions(); - - public string MetadataRoute { get; set; } = "odata"; - } + public CacheOptions CacheOptions { get; set; } = new(); + public ApiOptions ApiOptions { get; set; } = new(); - public class APIOptions - { - public bool UseXMLComments { get; set; } = false; - public string XMLCommentsFile { get; set; } = string.Empty; - } + public SwaggerOptions SwaggerOptions { get; set; } = new(); + public DatabaseOptions DatabaseOptions { get; set; } = new(); + public ODataFunctions ODataOptions { get; set; } = new(); + + public string MetadataRoute { get; set; } = "odata"; + private readonly IConfiguration configuration; - public class CacheOptions + public ApiGeneratorConfig(IConfiguration config) { - public bool Enabled { get; set; } = true; - public string Connection { get; set; } = "redis"; + this.configuration = config + ?? new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .AddJsonFile("secrets.json", true) + .AddEnvironmentVariables() + .Build(); + + //Load Options + this.configuration.Bind("Api:Basic", this.ApiOptions); + this.configuration.Bind("Api:Cache", this.CacheOptions); + this.configuration.Bind("Api:Swagger", this.SwaggerOptions); + this.configuration.Bind("Api:Database", this.DatabaseOptions); + this.configuration.Bind("Api:Odata", this.ODataOptions); } +} +public class ApiOptions +{ + public bool UseXmlComments { get; set; } = false; + public string XmlCommentsFile { get; set; } = string.Empty; +} - public enum DBType - { - InMemory, - SQL, - //Postgres, - SQLite - } +public class CacheOptions +{ + public bool Enabled { get; set; } = true; + public string Connection { get; set; } = "redis"; +} - public class DatabaseOptions - { - public DBType DatabaseType { get; set; } = DBType.InMemory; - public string? Connection { get; set; } = String.Empty; - public bool EnableAutomaticMigration { get; set; } = true; - } +public enum DbType +{ + InMemory, + Sql, - public class ODataFunctions - { - public bool Enabled { get; set; } = false; - public bool EnableSelect { get; set; } = true; - public bool EnableFilter { get; set; } = true; - public bool EnableSort { get; set; } = true; - } + //Postgres, + SqLite +} - public class SwaggerOptions - { - /// - /// Enable Swagger in Production - /// - public bool EnableProduction { get; set; } = true; - public string Description { get; set; } = "Sample for TCDev API Generator"; - public string Version { get; set; } = "v1"; - public string Title { get; set; } = "TCDev Api Generator Demo"; - public string ContactMail { get; set; } = "test@test.de"; - public string ContactUri { get; set; } = "https://www.test.de"; - public string Route { get; set; } = "/swagger/v1/swagger.json"; - } +public class DatabaseOptions +{ + public DbType DatabaseType { get; set; } = DbType.InMemory; + public string? Connection { get; set; } = string.Empty; + public bool EnableAutomaticMigration { get; set; } = true; +} + +public class ODataFunctions +{ + public bool Enabled { get; set; } = false; + public bool EnableSelect { get; set; } = true; + public bool EnableFilter { get; set; } = true; + public bool EnableSort { get; set; } = true; +} + +public class SwaggerOptions +{ + /// + /// Enable Swagger in Production + /// + public bool EnableProduction { get; set; } = true; + + public string Description { get; set; } = "Sample for TCDev API Generator"; + public string Version { get; set; } = "v1"; + public string Title { get; set; } = "TCDev Api Generator Demo"; + public string ContactMail { get; set; } = "test@test.de"; + public string ContactUri { get; set; } = "https://www.test.de"; + public string Route { get; set; } = "/swagger/v1/swagger.json"; } diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs index 82c6aee..c57f366 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -1,167 +1,189 @@ -// TCDev 2022/03/16 -// Apache 2.0 License +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.ApiGeneratorExtension.cs // https://www.github.com/deejaytc/dotnet-utils +using System; +using System.Collections.Generic; +using System.Linq; using System.Reflection; +using System.Text.Json.Serialization; +using EFCore.AutomaticMigrations; using EntityFramework.Triggers; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.OData; +using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.OpenApi.Models; +using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Data; -using TCDev.Controllers; -using Microsoft.AspNetCore.Routing; using TCDev.APIGenerator.Extension; -using Microsoft.OpenApi.Models; -using EFCore.AutomaticMigrations; -using System; +using TCDev.ApiGenerator.Json; +using TCDev.APIGenerator.Schema; +using TCDev.Controllers; -namespace TCDev.ApiGenerator.Extension +namespace TCDev.ApiGenerator.Extension; + +public static class ApiGeneratorExtension { - public static class ApiGeneratorExtension + public static ApiGeneratorConfig ApiGeneratorConfig { get; set; } = new(null); + + public static IServiceCollection AddApiGeneratorServices( + this IServiceCollection services, + IConfiguration config, + Assembly assembly) { + ApiGeneratorConfig = new ApiGeneratorConfig(config); - public static ApiGeneratorConfig ApiGeneratorConfig { get; set; } = new ApiGeneratorConfig(null); + // Add Database Context - public static IServiceCollection AddApiGeneratorServices( - this IServiceCollection services, - IConfiguration config, - Assembly assembly) + switch (ApiGeneratorConfig.DatabaseOptions.DatabaseType) { + case DbType.InMemory: + services.AddDbContext(options => + options.UseInMemoryDatabase("ApiGeneratorDB")); + break; + case DbType.Sql: + services.AddDbContext(options => + options.UseSqlServer(config.GetConnectionString("ApiGeneratorDatabase"), + b => b.MigrationsAssembly(assembly.FullName))); + break; + case DbType.SqLite: + services.AddDbContext(options => + options.UseSqlite(config.GetConnectionString("ApiGeneratorDatabase"), + b => b.MigrationsAssembly(assembly.FullName))); + break; + default: + throw new Exception("Database Type Unkown"); + } - ApiGeneratorConfig = new ApiGeneratorConfig(config); + services + .AddSingleton(typeof(ITriggers<,>), typeof(Triggers<,>)) + .AddSingleton(typeof(ITriggers<>), typeof(Triggers<>)) + .AddSingleton(typeof(ITriggers), typeof(Triggers)); - // Add Database Context + // Add Services + services.AddScoped(typeof(IGenericRespository<,>), typeof(GenericRespository<,>)); + + + //Add Framework Services & Options, we use the current assembly to get classes. + + + var JsonDef = new JsonClassDefinition() + { + Name = "TestGenerated", + RouteTemplate = "/testGenerated", + Fields = new List(){ + new Field + { + Name = "Id", + Type = "int" + }, + new Field + { + Name = "Name", + Type = "string" + }, - switch(ApiGeneratorConfig.DatabaseOptions.DatabaseType) - { - case DBType.InMemory: - services.AddDbContext(options => - options.UseInMemoryDatabase("ApiGeneratorDB")); - break; - case DBType.SQL: - services.AddDbContext(options => - options.UseSqlServer(config.GetConnectionString("ApiGeneratorDatabase"), - b => b.MigrationsAssembly(assembly.FullName))); - break; - case DBType.SQLite: - services.AddDbContext(options => - options.UseSqlite(config.GetConnectionString("ApiGeneratorDatabase"), - b => b.MigrationsAssembly(assembly.FullName))); - break; - default: - throw new Exception("Database Type Unkown"); } + }; - services - .AddSingleton(typeof(ITriggers<,>), typeof(Triggers<,>)) - .AddSingleton(typeof(ITriggers<>), typeof(Triggers<>)) - .AddSingleton(typeof(ITriggers), typeof(Triggers)); + // Get all types defined in JSON + var genericAssembly = JsonClassBuilder.CreateClass(JsonDef); - // Add Services - services.AddScoped(typeof(IGenericRespository<,>), typeof(GenericRespository<,>)); + // Get all types in entry assembly + var types = assembly.GetExportedTypes().Where(x => x.GetCustomAttributes().Any()); + // generate type list + var typesToLoad = new List + { + genericAssembly + }; + typesToLoad.AddRange(types); - //Add Framework Services & Options, we use the current assembly to get classes. - // Todo: Add option to add any custom assembly - services.AddMvc(o => - o.Conventions.Add(new GenericControllerRouteConvention())) - .ConfigureApplicationPartManager(m => - m.FeatureProviders.Add(new GenericTypeControllerFeatureProvider(new[] { assembly.FullName })) - ); + // Put everything together + services.AddMvc(options => + options.Conventions.Add(new GenericControllerRouteConvention())) + .ConfigureApplicationPartManager(manager => + // Add our controller feature provider + manager.FeatureProviders.Add(new GenericTypeControllerFeatureProvider(typesToLoad)) + ); - services.AddSwaggerGen(c => - { - c.SwaggerDoc(ApiGeneratorConfig.SwaggerOptions.Version, - new OpenApiInfo - { - Title = ApiGeneratorConfig.SwaggerOptions.Title, - Version = ApiGeneratorConfig.SwaggerOptions.Version, - Description = ApiGeneratorConfig.SwaggerOptions.Description, - Contact = new OpenApiContact() { - Email = ApiGeneratorConfig.SwaggerOptions.ContactMail, - Url = new System.Uri(ApiGeneratorConfig.SwaggerOptions.ContactUri) - } - }); - - c.DocumentFilter(); - c.SchemaFilter(); - - if (ApiGeneratorConfig.APIOptions.UseXMLComments) + + services.AddSwaggerGen(c => + { + c.SwaggerDoc(ApiGeneratorConfig.SwaggerOptions.Version, + new OpenApiInfo { - if (!string.IsNullOrEmpty(ApiGeneratorConfig.APIOptions.XMLCommentsFile)) + Title = ApiGeneratorConfig.SwaggerOptions.Title, + Version = ApiGeneratorConfig.SwaggerOptions.Version, + Description = ApiGeneratorConfig.SwaggerOptions.Description, + Contact = new OpenApiContact { - throw new Exception("You need to set XMLCommentsFile option when using XMl Comments"); - } else { - c.IncludeXmlComments(ApiGeneratorConfig.APIOptions.XMLCommentsFile, true); + Email = ApiGeneratorConfig.SwaggerOptions.ContactMail, Url = new Uri(ApiGeneratorConfig.SwaggerOptions.ContactUri) } - } - - }); + }); + c.DocumentFilter(); + c.SchemaFilter(); - if(ApiGeneratorConfig.ODataOptions.Enabled) + if (ApiGeneratorConfig.ApiOptions.UseXmlComments) { - services - .AddControllers() - .AddOData(opt => - { - opt.AddRouteComponents("odata", GenericDbContext.EdmModel); - opt.EnableNoDollarQueryOptions = true; - opt.EnableQueryFeatures(20000); - opt.Select().Expand().Filter(); - }) - .AddJsonOptions(o => - { - o.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); - } - ); - - } else { - services - .AddControllers() - .AddJsonOptions(o => - { - o.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter()); - }); + if (!string.IsNullOrEmpty(ApiGeneratorConfig.ApiOptions.XmlCommentsFile)) + throw new Exception("You need to set XMLCommentsFile option when using XMl Comments"); + c.IncludeXmlComments(ApiGeneratorConfig.ApiOptions.XmlCommentsFile, true); } + }); - return services; - } + if (ApiGeneratorConfig.ODataOptions.Enabled) + services + .AddControllers() + .AddOData(opt => + { + opt.AddRouteComponents("odata", GenericDbContext.EdmModel); + opt.EnableNoDollarQueryOptions = true; + opt.EnableQueryFeatures(20000); + opt.Select() + .Expand() + .Filter(); + }) + .AddJsonOptions(o => { o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } + ); + else + services + .AddControllers() + .AddJsonOptions(o => { o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); - public static IApplicationBuilder UseAutomaticAPIMigrations(this IApplicationBuilder app, bool AllowDataLoss = false) - { - using(var serviceScope = app.ApplicationServices.CreateScope()) - { - var dbContext = serviceScope.ServiceProvider.GetService(); - if(ApiGeneratorConfig.DatabaseOptions.DatabaseType != DBType.InMemory) { - dbContext.MigrateToLatestVersion(new DbMigrationsOptions - { - AutomaticMigrationsEnabled = true, - AutomaticMigrationDataLossAllowed = AllowDataLoss - }); - } - } - - return app; - } + return services; + } - public static IApplicationBuilder UseApiGenerator(this IApplicationBuilder app) - { - app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", $"{ApiGeneratorConfig.SwaggerOptions.Title} {ApiGeneratorConfig.SwaggerOptions.Version}")); - return app; - } + public static IApplicationBuilder UseAutomaticApiMigrations(this IApplicationBuilder app, bool allowDataLoss = false) + { + using var serviceScope = app.ApplicationServices.CreateScope(); + var dbContext = serviceScope.ServiceProvider.GetService(); + if (ApiGeneratorConfig.DatabaseOptions.DatabaseType != DbType.InMemory) + dbContext.MigrateToLatestVersion(new DbMigrationsOptions + { + AutomaticMigrationsEnabled = true, AutomaticMigrationDataLossAllowed = allowDataLoss + }); - public static IEndpointRouteBuilder UseApiGeneratorEndpoints(this IEndpointRouteBuilder builder) - { - return builder; - } + return app; + } + public static IApplicationBuilder UseApiGenerator(this IApplicationBuilder app) + { + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", $"{ApiGeneratorConfig.SwaggerOptions.Title} {ApiGeneratorConfig.SwaggerOptions.Version}")); + return app; + } + public static IEndpointRouteBuilder UseApiGeneratorEndpoints(this IEndpointRouteBuilder builder) + { + return builder; } -} \ No newline at end of file +} diff --git a/src/TCDev.APIGenerator/FeatureProvider/GenericTypeControllerFeatureProvider.cs b/src/TCDev.APIGenerator/FeatureProvider/GenericTypeControllerFeatureProvider.cs index 0a3a994..156c962 100644 --- a/src/TCDev.APIGenerator/FeatureProvider/GenericTypeControllerFeatureProvider.cs +++ b/src/TCDev.APIGenerator/FeatureProvider/GenericTypeControllerFeatureProvider.cs @@ -2,6 +2,7 @@ // Apache 2.0 License // https://www.github.com/deejaytc/dotnet-utils +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -12,25 +13,24 @@ namespace TCDev.Controllers { - public class GenericTypeControllerFeatureProvider : IApplicationFeatureProvider + public class GenericAssemblyControllerFeatureProvider : IApplicationFeatureProvider { /// /// Initiate the Controller generator /// /// Names of assemblies to search for classes - public GenericTypeControllerFeatureProvider(string[] assemblies) + public GenericAssemblyControllerFeatureProvider(Assembly[] assemblies) { - Assemblies = assemblies; + this.Assemblies = assemblies; } - private string[] Assemblies { get; } + private Assembly[] Assemblies { get; } public void PopulateFeature(IEnumerable parts, ControllerFeature feature) { foreach (var assembly in Assemblies) { - var loadedAssembly = Assembly.Load(assembly); - var customClasses = loadedAssembly.GetExportedTypes().Where(x => x.GetCustomAttributes().Any()); + var customClasses = assembly.GetExportedTypes().Where(x => x.GetCustomAttributes().Any()); foreach (var candidate in customClasses) { @@ -48,4 +48,36 @@ public void PopulateFeature(IEnumerable parts, ControllerFeatur } } } + + + public class GenericTypeControllerFeatureProvider : IApplicationFeatureProvider + { + /// + /// Initiate the Controller generator + /// + /// Names of assemblies to search for classes + public GenericTypeControllerFeatureProvider(List types) + { + this.Types = types; + } + + private List Types { get; } + + public void PopulateFeature(IEnumerable parts, ControllerFeature feature) + { + foreach (var type in this.Types) + { + // Ignore BaseController itself + if (type.FullName != null && type.FullName.Contains("BaseController")) continue; + + // Generate type info for our runtime controller, assign class as T + var propertyType = type.GetProperty("Id")?.PropertyType; + if (propertyType == null) continue; + var typeInfo = typeof(GenericController<,>).MakeGenericType(type, propertyType).GetTypeInfo(); + + // Finally add the new controller via FeatureProvider -> + feature.Controllers.Add(typeInfo); + } + } + } } \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Generator.cs b/src/TCDev.APIGenerator/Generator.cs new file mode 100644 index 0000000..85335d3 --- /dev/null +++ b/src/TCDev.APIGenerator/Generator.cs @@ -0,0 +1,105 @@ +// TCDev.de 2022/04/05 +// TCDev.APIGenerator.Generator.cs +// https://www.github.com/deejaytc/dotnet-utils + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Newtonsoft.Json; +using TCDev.APIGenerator.Schema; + + +namespace TCDev.ApiGenerator.Json; + +public class JsonClassBuilder +{ + + public static Type CreateClass(JsonClassDefinition definition) + { + try + { + var classCode = $@" // Auto-generated code + using System; + using Swashbuckle.AspNetCore.Annotations; + using System.ComponentModel.DataAnnotations; + using System.ComponentModel.DataAnnotations.Schema; + using System.Text.Json.Serialization; + using TCDev.ApiGenerator.Attributes; + using TCDev.ApiGenerator.Interfaces; + + namespace TCDev.ApiGenerator + {{ + [Api(""{ definition.RouteTemplate }"")] + public class { definition.Name } : IObjectBase<{definition.IdType}> + + // Add Properties + {{ + public {definition.IdType} Id {{ get; set;}} + "; + + // Add all fields + var result1 = definition.Fields.Aggregate(string.Empty, (current, field) => current + $@" public {field.Type} {field.Name} {{ get; set;}}"); + + // Complete class + classCode += result1; + classCode += $@"}} }}"; + + MetadataReference[] assemblies = AppDomain + .CurrentDomain + .GetAssemblies() + .Where(a => !string.IsNullOrEmpty(a.Location)) + .Select(a => MetadataReference.CreateFromFile(a.Location)) + .ToArray(); + classCode = FormatUsingRoslyn(classCode); + + var syntaxTree = CSharpSyntaxTree.ParseText(classCode); + + var compilation = CSharpCompilation + .Create("TCDev.ApiGenerator") + .AddSyntaxTrees(syntaxTree) + .AddReferences(assemblies) + .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using var ms = new MemoryStream(); + var result = compilation.Emit(ms); + + if (result.Success) + { + ms.Seek(0, SeekOrigin.Begin); + var assembly = Assembly.Load(ms.ToArray()); + + var newTypeFullName = $"TCDev.ApiGenerator.{definition.Name}"; + + var type = assembly.GetType(newTypeFullName); + return type; + } + + var failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); + + foreach (var diagnostic in failures) Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage()); + + return null; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public static string FormatUsingRoslyn(string csCode) + { + var tree = CSharpSyntaxTree.ParseText(csCode); + var root = tree.GetRoot() + .NormalizeWhitespace(); + var result = root.ToFullString(); + return result; + } + +} diff --git a/src/TCDev.APIGenerator/Model/BaseConfig.cs b/src/TCDev.APIGenerator/Model/BaseConfig.cs index 2850f89..f19d363 100644 --- a/src/TCDev.APIGenerator/Model/BaseConfig.cs +++ b/src/TCDev.APIGenerator/Model/BaseConfig.cs @@ -1,22 +1,19 @@ -// TCDev 2022/03/16 -// Apache 2.0 License +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.BaseConfig.cs // https://www.github.com/deejaytc/dotnet-utils using Microsoft.EntityFrameworkCore; -namespace TCDev.ApiGenerator.Model +namespace TCDev.ApiGenerator.Model; + +public enum DbTypes { - public enum DbTypes - { - Sql = 0 - , Postgres = 1 - , MySQL = 2 - } + Sql = 0, Postgres = 1, MySql = 2 +} - [Keyless] - public class BaseConfig - { - public string DbConnection { get; set; } - public DbTypes DbType { get; set; } = DbTypes.Sql; - } -} \ No newline at end of file +[Keyless] +public class BaseConfig +{ + public string DbConnection { get; set; } + public DbTypes DbType { get; set; } = DbTypes.Sql; +} diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml index 9e6423e..d5f7ae2 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml @@ -66,9 +66,9 @@ - + - Enable Swagger in Production + Enable Swagger in Production @@ -77,7 +77,13 @@ One of the main pieces to make the magic work. - + + + Initiate the Controller generator + + Names of assemblies to search for classes + + Initiate the Controller generator From 138460af1c82232327d2a4a41fd73e3e25ee08dd Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Thu, 7 Apr 2022 00:05:59 +0200 Subject: [PATCH 07/17] changes --- TCDev.APIGenerator.sln | 23 ++++ .../ApiGeneratorSampleApp/ApiDefinition.json | 18 +++ .../ApiGeneratorSampleApI.csproj | 6 +- sample/ApiGeneratorSampleApp/Program.cs | 2 +- sample/SampleAppJson/ApiDefinition.json | 18 +++ sample/SampleAppJson/Program.cs | 24 ++++ .../Properties/launchSettings.json | 31 +++++ sample/SampleAppJson/SampleAppJson.csproj | 24 ++++ .../appsettings.Development.json | 8 ++ sample/SampleAppJson/appsettings.json | 34 ++++++ sample/SampleAppNuget/SampleAppNuget.csproj | 2 +- sample/SampleClasses/Car.cs | 58 --------- sample/SampleClasses/CarDealer.cs | 33 ------ sample/SampleClasses/Person.cs | 48 -------- sample/SampleClasses/SampleClasses.csproj | 24 ---- sample/SampleClasses/Test.cs | 34 ------ .../TCDev.APIGenerator.Caching.csproj | 2 +- src/TCDev.APIGenerator.DbFirst/Generator.cs | 101 +++++++++++----- .../TCDev.APIGenerator.Json.csproj | 6 +- .../TCDev.APIGenerator.GraphQL.csproj | 2 +- .../JsonClassDefinition.cs | 1 + .../TCDev.APIGenerator.Schema.csproj | 2 +- .../Data/DBReader/DBScaffolder.cs | 111 ------------------ .../Data/GenericDbContext.cs | 33 +++--- .../Extension/ApiGeneratorExtension.cs | 49 ++------ .../GenericControllerRouteConvention.cs | 25 ++++ .../Services/AssemblyService.cs | 15 +++ .../{ => Services}/Generator.cs | 8 +- .../TCDev.APIGenerator.csproj | 6 +- src/TCDev.APIGenerator/TCDev.APIGenerator.xml | 4 +- 30 files changed, 347 insertions(+), 405 deletions(-) create mode 100644 sample/ApiGeneratorSampleApp/ApiDefinition.json create mode 100644 sample/SampleAppJson/ApiDefinition.json create mode 100644 sample/SampleAppJson/Program.cs create mode 100644 sample/SampleAppJson/Properties/launchSettings.json create mode 100644 sample/SampleAppJson/SampleAppJson.csproj create mode 100644 sample/SampleAppJson/appsettings.Development.json create mode 100644 sample/SampleAppJson/appsettings.json delete mode 100644 sample/SampleClasses/Car.cs delete mode 100644 sample/SampleClasses/CarDealer.cs delete mode 100644 sample/SampleClasses/Person.cs delete mode 100644 sample/SampleClasses/SampleClasses.csproj delete mode 100644 sample/SampleClasses/Test.cs delete mode 100644 src/TCDev.APIGenerator/Data/DBReader/DBScaffolder.cs create mode 100644 src/TCDev.APIGenerator/Services/AssemblyService.cs rename src/TCDev.APIGenerator/{ => Services}/Generator.cs (92%) diff --git a/TCDev.APIGenerator.sln b/TCDev.APIGenerator.sln index 6e2cf86..f975b39 100644 --- a/TCDev.APIGenerator.sln +++ b/TCDev.APIGenerator.sln @@ -26,11 +26,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleAppNuget", "sample\Sa EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TCDev.APIGenerator.Json", "src\TCDev.APIGenerator.DbFirst\TCDev.APIGenerator.Json.csproj", "{7F3574D1-7421-4824-A0BB-522F3BC9BAC4}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleAppJson", "sample\SampleAppJson\SampleAppJson.csproj", "{25AE6B2A-822D-411B-AB65-068E9E0E41D5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU DebugWithSampleApp|Any CPU = DebugWithSampleApp|Any CPU Release|Any CPU = Release|Any CPU + SampleAppJson|Any CPU = SampleAppJson|Any CPU SampleAppNuget|Any CPU = SampleAppNuget|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution @@ -40,6 +43,8 @@ Global {FE869C02-6C9A-4D9B-BBE2-56F1B21B2A55}.DebugWithSampleApp|Any CPU.Build.0 = DebugWithSampleApp|Any CPU {FE869C02-6C9A-4D9B-BBE2-56F1B21B2A55}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE869C02-6C9A-4D9B-BBE2-56F1B21B2A55}.Release|Any CPU.Build.0 = Release|Any CPU + {FE869C02-6C9A-4D9B-BBE2-56F1B21B2A55}.SampleAppJson|Any CPU.ActiveCfg = SampleAppJson|Any CPU + {FE869C02-6C9A-4D9B-BBE2-56F1B21B2A55}.SampleAppJson|Any CPU.Build.0 = SampleAppJson|Any CPU {FE869C02-6C9A-4D9B-BBE2-56F1B21B2A55}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU {303BF897-594C-4911-91CF-3887A8B8E839}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {303BF897-594C-4911-91CF-3887A8B8E839}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -47,14 +52,17 @@ Global {303BF897-594C-4911-91CF-3887A8B8E839}.DebugWithSampleApp|Any CPU.Build.0 = DebugWithSampleApp|Any CPU {303BF897-594C-4911-91CF-3887A8B8E839}.Release|Any CPU.ActiveCfg = Release|Any CPU {303BF897-594C-4911-91CF-3887A8B8E839}.Release|Any CPU.Build.0 = Release|Any CPU + {303BF897-594C-4911-91CF-3887A8B8E839}.SampleAppJson|Any CPU.ActiveCfg = SampleAppJson|Any CPU {303BF897-594C-4911-91CF-3887A8B8E839}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU {0C8E23AD-AC5D-41D4-9F67-0ECF3D1C4BE1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {0C8E23AD-AC5D-41D4-9F67-0ECF3D1C4BE1}.DebugWithSampleApp|Any CPU.ActiveCfg = DebugWithSampleApp|Any CPU {0C8E23AD-AC5D-41D4-9F67-0ECF3D1C4BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0C8E23AD-AC5D-41D4-9F67-0ECF3D1C4BE1}.SampleAppJson|Any CPU.ActiveCfg = SampleAppJson|Any CPU {0C8E23AD-AC5D-41D4-9F67-0ECF3D1C4BE1}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}.DebugWithSampleApp|Any CPU.ActiveCfg = DebugWithSampleApp|Any CPU {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}.SampleAppJson|Any CPU.ActiveCfg = SampleAppJson|Any CPU {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU {94E59385-D259-40A1-A373-1FBD0A42CD63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {94E59385-D259-40A1-A373-1FBD0A42CD63}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -62,10 +70,13 @@ Global {94E59385-D259-40A1-A373-1FBD0A42CD63}.DebugWithSampleApp|Any CPU.Build.0 = DebugWithSampleApp|Any CPU {94E59385-D259-40A1-A373-1FBD0A42CD63}.Release|Any CPU.ActiveCfg = Release|Any CPU {94E59385-D259-40A1-A373-1FBD0A42CD63}.Release|Any CPU.Build.0 = Release|Any CPU + {94E59385-D259-40A1-A373-1FBD0A42CD63}.SampleAppJson|Any CPU.ActiveCfg = SampleAppJson|Any CPU + {94E59385-D259-40A1-A373-1FBD0A42CD63}.SampleAppJson|Any CPU.Build.0 = SampleAppJson|Any CPU {94E59385-D259-40A1-A373-1FBD0A42CD63}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU {BA9E04E6-4B66-4369-9B2F-C6CEC9499851}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {BA9E04E6-4B66-4369-9B2F-C6CEC9499851}.DebugWithSampleApp|Any CPU.ActiveCfg = DebugWithSampleApp|Any CPU {BA9E04E6-4B66-4369-9B2F-C6CEC9499851}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BA9E04E6-4B66-4369-9B2F-C6CEC9499851}.SampleAppJson|Any CPU.ActiveCfg = SampleAppJson|Any CPU {BA9E04E6-4B66-4369-9B2F-C6CEC9499851}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU {BA9E04E6-4B66-4369-9B2F-C6CEC9499851}.SampleAppNuget|Any CPU.Build.0 = SampleAppNuget|Any CPU {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU @@ -74,8 +85,19 @@ Global {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.DebugWithSampleApp|Any CPU.Build.0 = DebugWithSampleApp|Any CPU {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.Release|Any CPU.ActiveCfg = Release|Any CPU {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.Release|Any CPU.Build.0 = Release|Any CPU + {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.SampleAppJson|Any CPU.ActiveCfg = SampleAppJson|Any CPU {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU {7F3574D1-7421-4824-A0BB-522F3BC9BAC4}.SampleAppNuget|Any CPU.Build.0 = SampleAppNuget|Any CPU + {25AE6B2A-822D-411B-AB65-068E9E0E41D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {25AE6B2A-822D-411B-AB65-068E9E0E41D5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {25AE6B2A-822D-411B-AB65-068E9E0E41D5}.DebugWithSampleApp|Any CPU.ActiveCfg = DebugWithSampleApp|Any CPU + {25AE6B2A-822D-411B-AB65-068E9E0E41D5}.DebugWithSampleApp|Any CPU.Build.0 = DebugWithSampleApp|Any CPU + {25AE6B2A-822D-411B-AB65-068E9E0E41D5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {25AE6B2A-822D-411B-AB65-068E9E0E41D5}.Release|Any CPU.Build.0 = Release|Any CPU + {25AE6B2A-822D-411B-AB65-068E9E0E41D5}.SampleAppJson|Any CPU.ActiveCfg = SampleAppJson|Any CPU + {25AE6B2A-822D-411B-AB65-068E9E0E41D5}.SampleAppJson|Any CPU.Build.0 = SampleAppJson|Any CPU + {25AE6B2A-822D-411B-AB65-068E9E0E41D5}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU + {25AE6B2A-822D-411B-AB65-068E9E0E41D5}.SampleAppNuget|Any CPU.Build.0 = SampleAppNuget|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -88,6 +110,7 @@ Global {94E59385-D259-40A1-A373-1FBD0A42CD63} = {4189D7E0-F171-4267-AC64-C9A83BB1B559} {BA9E04E6-4B66-4369-9B2F-C6CEC9499851} = {8CC9B68F-E1C2-45B3-8814-B9FF4E1B2AB8} {7F3574D1-7421-4824-A0BB-522F3BC9BAC4} = {4189D7E0-F171-4267-AC64-C9A83BB1B559} + {25AE6B2A-822D-411B-AB65-068E9E0E41D5} = {8CC9B68F-E1C2-45B3-8814-B9FF4E1B2AB8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {315BF454-8B91-42C5-A113-B59C72AE69C8} diff --git a/sample/ApiGeneratorSampleApp/ApiDefinition.json b/sample/ApiGeneratorSampleApp/ApiDefinition.json new file mode 100644 index 0000000..5d99336 --- /dev/null +++ b/sample/ApiGeneratorSampleApp/ApiDefinition.json @@ -0,0 +1,18 @@ +[ + { + "name": "Car", + "route": "/cars", + "caching": true, + "cacheLiveTime": 1000, + "events": "POST,PUT,DELETE", + "idType": "int", + "Fields": [ + { + "name": "Name", + "type": "String", + "maxLength": "200", + "nullable": false + } + ] + } +] \ No newline at end of file diff --git a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj index f4fd3fc..f9aceb9 100644 --- a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj +++ b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj @@ -3,7 +3,7 @@ net6.0 aspnet-ApiGeneratorSampleApp-56AA10DB-26A2-414F-AFD8-1D3546BD678D - Debug;Release;DebugWithSampleApp;SampleAppNuget + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson @@ -14,6 +14,10 @@ ApiGeneratorSampleApI.xml + + ApiGeneratorSampleApI.xml + + diff --git a/sample/ApiGeneratorSampleApp/Program.cs b/sample/ApiGeneratorSampleApp/Program.cs index 3d0fbaa..09b6bf4 100644 --- a/sample/ApiGeneratorSampleApp/Program.cs +++ b/sample/ApiGeneratorSampleApp/Program.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using TCDev.ApiGenerator.Extension; -using TCDev.ApiGenerator.Json; + var builder = WebApplication.CreateBuilder(args); diff --git a/sample/SampleAppJson/ApiDefinition.json b/sample/SampleAppJson/ApiDefinition.json new file mode 100644 index 0000000..dc4f842 --- /dev/null +++ b/sample/SampleAppJson/ApiDefinition.json @@ -0,0 +1,18 @@ +[ + { + "name": "Car", + "route": "/cars", + "caching": true, + "cacheLiveTime": 1000, + "events": "POST,PUT,DELETE", + "idType": "int", + "Fields": [ + { + "name": "Name", + "type": "String", + "maxLength": "200", + "nullable": false + } + ] + } + ] diff --git a/sample/SampleAppJson/Program.cs b/sample/SampleAppJson/Program.cs new file mode 100644 index 0000000..e731b50 --- /dev/null +++ b/sample/SampleAppJson/Program.cs @@ -0,0 +1,24 @@ +using System.Configuration; +using System.Reflection; +using Newtonsoft.Json; +using TCDev.ApiGenerator.Extension; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); + +builder.Services.AddApiGeneratorServices(builder.Configuration, Assembly.GetExecutingAssembly()); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.UseHttpsRedirection(); + +app.UseAuthorization(); +app.UseApiGenerator(); +app.MapControllers(); + +app.Run(); diff --git a/sample/SampleAppJson/Properties/launchSettings.json b/sample/SampleAppJson/Properties/launchSettings.json new file mode 100644 index 0000000..29f040a --- /dev/null +++ b/sample/SampleAppJson/Properties/launchSettings.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:25638", + "sslPort": 44337 + } + }, + "profiles": { + "SampleAppNuget": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "weatherforecast", + "applicationUrl": "https://localhost:7114;http://localhost:5114", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/sample/SampleAppJson/SampleAppJson.csproj b/sample/SampleAppJson/SampleAppJson.csproj new file mode 100644 index 0000000..155556e --- /dev/null +++ b/sample/SampleAppJson/SampleAppJson.csproj @@ -0,0 +1,24 @@ + + + + net6.0 + enable + enable + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson + + + + + + + + + + + + + + + + + diff --git a/sample/SampleAppJson/appsettings.Development.json b/sample/SampleAppJson/appsettings.Development.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/sample/SampleAppJson/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/sample/SampleAppJson/appsettings.json b/sample/SampleAppJson/appsettings.json new file mode 100644 index 0000000..ab2ebdd --- /dev/null +++ b/sample/SampleAppJson/appsettings.json @@ -0,0 +1,34 @@ +{ + "ConnectionStrings": { + "ApiGeneratorDatabase": "Server=localhost;database=tcdev_dev_222;user=sa;password=Password!23;" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*", + + "Api": { + "Swagger": { + "EnableProduction": "false", // Enable/Disable for production builds + "Description": "Sample Swagger Config", + "Version": "v1", + "Title": "Sample Swagger Config Title", + "ContactMail": "Me@me.de", + "ContactUri": "https://www.myuri.com" + }, + "Database": { + "DatabaseType": "InMemory", + "Connection": "" + }, + "Odata": { + "Enabled": true, + "EnableSelect": true, + "EnableFilter": true, + "EnableSort": true + } + } +} diff --git a/sample/SampleAppNuget/SampleAppNuget.csproj b/sample/SampleAppNuget/SampleAppNuget.csproj index 9c4ba98..e034d84 100644 --- a/sample/SampleAppNuget/SampleAppNuget.csproj +++ b/sample/SampleAppNuget/SampleAppNuget.csproj @@ -4,7 +4,7 @@ net6.0 enable enable - Debug;Release;DebugWithSampleApp;SampleAppNuget + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson diff --git a/sample/SampleClasses/Car.cs b/sample/SampleClasses/Car.cs deleted file mode 100644 index 7492d2f..0000000 --- a/sample/SampleClasses/Car.cs +++ /dev/null @@ -1,58 +0,0 @@ -using EntityFramework.Triggers; -using Maximago.ApiGenerator.Attributes; -using Maximago.ApiGenerator.Interfaces; -using Maximago.ApiGenerator.Schemes; -using Maximago.ApiGenerator.Schemes.Interfaces; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using Newtonsoft.Json; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace ApiGeneratorSampleApp -{ - - [GeneratedController( - route: "/cars/", - requiredReadClaims: new string[] { "car.read" }, - requiredWriteClaims: new string[] { "car.write" }, - fireEvents: true, - cacheDuration: 10000, - cache: true - )] - public class Car : - Trackable, - IObjectBase, - IEntityTypeConfiguration, - IHasQueryFields - { - - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - [Key] - public int Id { get; set; } - - [Required] - public string Name { get; set; } - - [Required] - public string Brand { get; set; } - - [Required] - public string Make { get; set; } - - public CarDealer? Dealer { get; set; } - - /// - /// All Fields of the class that are querried by q param on controller - /// - public string[] QueryFields { get { return new string[] { "Name", "Brand", "Make" }; } } - - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey("Id"); - builder.ToTable("car"); - builder.HasOne("Dealer").WithMany("Cars"); - - } - } -} diff --git a/sample/SampleClasses/CarDealer.cs b/sample/SampleClasses/CarDealer.cs deleted file mode 100644 index 76b0773..0000000 --- a/sample/SampleClasses/CarDealer.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Maximago.ApiGenerator.Attributes; -using Maximago.ApiGenerator.Interfaces; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using System; -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace ApiGeneratorSampleApp -{ - [GeneratedController( - route: "/cardealer", - authorize: false - )] - public class CarDealer : IObjectBase, IEntityTypeConfiguration - { - [Key] - public string Id { get; set; } = System.Guid.NewGuid().ToString(); - - public virtual List? Cars { get; set; } = new List(); - - - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey(p => p.Id); - - builder.HasMany(p => p.Cars).WithOne(p => p.Dealer); - } - - } - - -} diff --git a/sample/SampleClasses/Person.cs b/sample/SampleClasses/Person.cs deleted file mode 100644 index ba38631..0000000 --- a/sample/SampleClasses/Person.cs +++ /dev/null @@ -1,48 +0,0 @@ -using GraphQL.Types; -using Maximago.ApiGenerator.Attributes; -using Maximago.ApiGenerator.Interfaces; -using Maximago.ApiGenerator.Schemes.Interfaces; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using System; -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace ApiGeneratorSampleApI.Model -{ - - [Api( route: "/people2" )] - public class PersonAutoMode : IObjectBase - { - - public Guid Id { get; set; } - - public string Name { get; set; } - - public string Description { get; set; } - - } - - - [Api( - route: "/people", - authorize: false - )] - public class Person : - IObjectBase, - IEntityTypeConfiguration - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public Guid Id { get; set; } - - public string Name { get; set; } - - public string Description { get; set; } - - public void Configure(EntityTypeBuilder builder) - { - //default stuff if nothing special - } - } -} diff --git a/sample/SampleClasses/SampleClasses.csproj b/sample/SampleClasses/SampleClasses.csproj deleted file mode 100644 index 2494ef0..0000000 --- a/sample/SampleClasses/SampleClasses.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - net6.0 - enable - enable - False - - - - - - - - - - - - - - - - - diff --git a/sample/SampleClasses/Test.cs b/sample/SampleClasses/Test.cs deleted file mode 100644 index 5b4ae86..0000000 --- a/sample/SampleClasses/Test.cs +++ /dev/null @@ -1,34 +0,0 @@ -using Maximago.ApiGenerator.Attributes; -using Maximago.ApiGenerator.Interfaces; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace ApiGeneratorSampleApI.Model -{ - - [GeneratedController( - route: "/test/", - requiredReadClaims: new string[] { "test.read" }, - requiredWriteClaims: new string[] { "test.write" }, - fireEvents: true, - cacheDuration: 10000, - cache: true - )] - public class Test : - IObjectBase, - IEntityTypeConfiguration, - IHasQueryFields - { - public string Id { get; set; } - - public string[] QueryFields => throw new System.NotImplementedException(); - - public void Configure(EntityTypeBuilder builder) - { - builder.HasKey("Id"); - } - } -} diff --git a/src/TCDev.APIGenerator.Caching/TCDev.APIGenerator.Caching.csproj b/src/TCDev.APIGenerator.Caching/TCDev.APIGenerator.Caching.csproj index e2b847e..bc4994e 100644 --- a/src/TCDev.APIGenerator.Caching/TCDev.APIGenerator.Caching.csproj +++ b/src/TCDev.APIGenerator.Caching/TCDev.APIGenerator.Caching.csproj @@ -2,7 +2,7 @@ net6.0 - Debug;Release;DebugWithSampleApp;SampleAppNuget + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson diff --git a/src/TCDev.APIGenerator.DbFirst/Generator.cs b/src/TCDev.APIGenerator.DbFirst/Generator.cs index 67284c1..34c527b 100644 --- a/src/TCDev.APIGenerator.DbFirst/Generator.cs +++ b/src/TCDev.APIGenerator.DbFirst/Generator.cs @@ -2,20 +2,30 @@ // TCDev.APIGenerator.Generator.cs // https://www.github.com/deejaytc/dotnet-utils +using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Newtonsoft.Json; +using TCDev.APIGenerator.Schema; + namespace TCDev.ApiGenerator.Json; public class JsonClassBuilder { - public const string TestClass = $@" // Auto-generated code + + public static Type CreateClass(JsonClassDefinition definition) + { + try + { + var classCode = $@" // Auto-generated code using System; using Swashbuckle.AspNetCore.Annotations; - using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Text.Json.Serialization; @@ -24,40 +34,73 @@ public class JsonClassBuilder namespace TCDev.ApiGenerator {{ - [Api('/carsgen')] - public class CarsGenerated : IObjectBase - {{ - public int Id {{ get; set;}} - public string Name {{ get; set;}} - }} - }} - "; - - public void LoadJsonClass() - { + [Api(""{ definition.RouteTemplate }"")] + public class { definition.Name } : IObjectBase<{definition.IdType}> + + // Add Properties + {{ + public {definition.IdType} Id {{ get; set;}} + "; - var compilation = CSharpCompilation.Create("DynamicAssembly", new[] { CSharpSyntaxTree.ParseText(TestClass) }, - new[] - { - MetadataReference.CreateFromFile(typeof(object).Assembly.Location) - }, - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + // Add all fields + var result1 = definition.Fields.Aggregate(string.Empty, (current, field) => + current + $@" public {field.Type} {field.Name}{(field.Nullable ? "?" : "")} {{ get; set;}}"); + + // Complete class + classCode += result1; + classCode += $@"}} }}"; - using (var ms = new MemoryStream()) - { - var emitResult = compilation.Emit(ms); + MetadataReference[] assemblies = AppDomain + .CurrentDomain + .GetAssemblies() + .Where(a => !string.IsNullOrEmpty(a.Location)) + .Select(a => MetadataReference.CreateFromFile(a.Location)) + .ToArray(); + classCode = FormatUsingRoslyn(classCode); + + var syntaxTree = CSharpSyntaxTree.ParseText(classCode); + + var compilation = CSharpCompilation + .Create("TCDev.ApiGenerator") + .AddSyntaxTrees(syntaxTree) + .AddReferences(assemblies) + .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - if (!emitResult.Success) + using var ms = new MemoryStream(); + var result = compilation.Emit(ms); + + if (result.Success) { - // handle, log errors etc - Debug.WriteLine("Compilation failed!"); - return; + ms.Seek(0, SeekOrigin.Begin); + var assembly = Assembly.Load(ms.ToArray()); + + var newTypeFullName = $"TCDev.ApiGenerator.{definition.Name}"; + + var type = assembly.GetType(newTypeFullName); + return type; } - ms.Seek(0, SeekOrigin.Begin); - var assembly = Assembly.Load(ms.ToArray()); - var candidates = assembly.GetExportedTypes(); + var failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); + foreach (var diagnostic in failures) Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage()); + + return null; + } + catch (Exception e) + { + Console.WriteLine(e); + throw; } } + + public static string FormatUsingRoslyn(string csCode) + { + var tree = CSharpSyntaxTree.ParseText(csCode); + var root = tree.GetRoot() + .NormalizeWhitespace(); + var result = root.ToFullString(); + return result; + } + } diff --git a/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.Json.csproj b/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.Json.csproj index 6aea5ed..2922adf 100644 --- a/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.Json.csproj +++ b/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.Json.csproj @@ -2,7 +2,7 @@ net6.0 - Debug;Release;DebugWithSampleApp;SampleAppNuget + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson @@ -14,4 +14,8 @@ + + + + diff --git a/src/TCDev.APIGenerator.GraphQL/TCDev.APIGenerator.GraphQL.csproj b/src/TCDev.APIGenerator.GraphQL/TCDev.APIGenerator.GraphQL.csproj index 76d870e..6ea8c95 100644 --- a/src/TCDev.APIGenerator.GraphQL/TCDev.APIGenerator.GraphQL.csproj +++ b/src/TCDev.APIGenerator.GraphQL/TCDev.APIGenerator.GraphQL.csproj @@ -2,7 +2,7 @@ net6.0 - Debug;Release;DebugWithSampleApp;SampleAppNuget + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson diff --git a/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs b/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs index 672b926..230b3c9 100644 --- a/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs +++ b/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs @@ -40,5 +40,6 @@ public class Field { public string Name { get; set; } public string Type { get; set; } + public bool Nullable { get; set; } } } diff --git a/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj b/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj index c1e5bff..ad03681 100644 --- a/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj +++ b/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj @@ -2,7 +2,7 @@ net6.0 - Debug;Release;DebugWithSampleApp;SampleAppNuget + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson diff --git a/src/TCDev.APIGenerator/Data/DBReader/DBScaffolder.cs b/src/TCDev.APIGenerator/Data/DBReader/DBScaffolder.cs deleted file mode 100644 index f408a10..0000000 --- a/src/TCDev.APIGenerator/Data/DBReader/DBScaffolder.cs +++ /dev/null @@ -1,111 +0,0 @@ -// TCDev.de 2022/03/31 -// TCDev.APIGenerator.DBScaffolder.cs -// https://www.github.com/deejaytc/dotnet-utils - -//using System.Collections.Generic; -//using System.Data.Common; -//using System.Diagnostics.CodeAnalysis; -//using System.Linq; -//using System.Linq.Expressions; -//using System.Reflection; -//using Microsoft.CodeAnalysis; -//using Microsoft.CodeAnalysis.CSharp; -//using Microsoft.EntityFrameworkCore.Design; -//using Microsoft.EntityFrameworkCore.Diagnostics; -//using Microsoft.EntityFrameworkCore.Scaffolding; -//using Microsoft.EntityFrameworkCore.Scaffolding.Internal; -//using Microsoft.EntityFrameworkCore.SqlServer.Diagnostics.Internal; -//using Microsoft.EntityFrameworkCore.SqlServer.Scaffolding.Internal; -//using Microsoft.EntityFrameworkCore.SqlServer.Storage.Internal; -//using Microsoft.EntityFrameworkCore.Storage; -//using Microsoft.Extensions.DependencyInjection; - -//namespace TCDev.APIGenerator.Data.DBReader; - -//public class DbScaffolder -//{ -// [SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "We need it")] -// public static IReverseEngineerScaffolder CreateMssqlScaffolder() -// { -// return new ServiceCollection() -// .AddEntityFrameworkSqlServer() -// .AddLogging() -// .AddEntityFrameworkDesignTimeServices() -// .AddSingleton() -// .AddSingleton() -// .AddSingleton() -// .AddSingleton() -// .AddSingleton() -// .AddSingleton() -// .AddSingleton() -// .AddSingleton() -// .BuildServiceProvider() -// .GetRequiredService(); -// } - - -// public static List CompilationReferences(bool enableLazyLoading) -// { -// var refs = new List(); -// var referencedAssemblies = Assembly.GetExecutingAssembly() -// .GetReferencedAssemblies(); -// refs.AddRange(referencedAssemblies.Select(a => MetadataReference.CreateFromFile(Assembly.Load(a) -// .Location))); - -// refs.Add(MetadataReference.CreateFromFile(typeof(object).Assembly.Location)); -// refs.Add(MetadataReference.CreateFromFile(Assembly.Load("netstandard, Version=2.0.0.0") -// .Location)); -// refs.Add(MetadataReference.CreateFromFile(typeof(DbConnection).Assembly.Location)); -// refs.Add(MetadataReference.CreateFromFile(typeof(Expression).Assembly.Location)); - -// //if (enableLazyLoading) -// //{ -// // refs.Add(MetadataReference.CreateFromFile(typeof(ProxiesExtensions).Assembly.Location)); -// //} - -// return refs; -// } - -// private static CSharpCompilation GenerateCode(List sourceFiles, bool enableLazyLoading) -// { -// var options = CSharpParseOptions.Default.WithLanguageVersion(LanguageVersion.CSharp8); - -// var parsedSyntaxTrees = sourceFiles.Select(f => SyntaxFactory.ParseSyntaxTree(f, options)); - -// return CSharpCompilation.Create("DataContext.dll", -// parsedSyntaxTrees, -// CompilationReferences(enableLazyLoading), -// new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, -// optimizationLevel: OptimizationLevel.Release, -// assemblyIdentityComparer: DesktopAssemblyIdentityComparer.Default)); -// } -//} - - -// public static void GenerateClasses() -// { -// var connectionString = @""; - -// var scaffolder = CreateMssqlScaffolder(); - -// var dbOpts = new DatabaseModelFactoryOptions(); -// var modelOpts = new ModelReverseEngineerOptions(); -// var codeGenOpts = new ModelCodeGenerationOptions -// { -// RootNamespace = "TypedDataContext", -// ContextName = "DataContext", -// ContextNamespace = "TypedDataContext.Context", -// ModelNamespace = "TypedDataContext.Models", -// SuppressConnectionStringWarning = true -// }; - -// var scaffoldedModelSources = scaffolder.ScaffoldModel(connectionString, dbOpts, modelOpts, codeGenOpts); -// var sourceFiles = new List -// { -// scaffoldedModelSources.ContextFile.Code -// }; -// sourceFiles.AddRange(scaffoldedModelSources.AdditionalFiles.Select(f => f.Code)); -// } -//} - - diff --git a/src/TCDev.APIGenerator/Data/GenericDbContext.cs b/src/TCDev.APIGenerator/Data/GenericDbContext.cs index b56f597..88b8f2c 100644 --- a/src/TCDev.APIGenerator/Data/GenericDbContext.cs +++ b/src/TCDev.APIGenerator/Data/GenericDbContext.cs @@ -16,6 +16,7 @@ using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; using TCDev.ApiGenerator.Attributes; +using TCDev.APIGenerator.Services; namespace TCDev.ApiGenerator.Data; @@ -23,20 +24,20 @@ public class GenericDbContext : DbContext { protected IHttpContextAccessor HttpContextAccessor { get; } - public static IModel StaticModel { get; } = BuildStaticModel(); + //public static IModel StaticModel { get; } = BuildStaticModel(); public static IEdmModel EdmModel { get; } = GetEdmModel(); + private readonly AssemblyService assemblyService; - - public GenericDbContext() - { - } + //public GenericDbContext() + //{ + //} public GenericDbContext( DbContextOptions options, IConfiguration config, - IHttpContextAccessor httpContextAccessor) : base(options) + AssemblyService assemblyService) : base(options) { - this.HttpContextAccessor = httpContextAccessor; + this.assemblyService = assemblyService; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) @@ -83,12 +84,8 @@ protected override void OnModelCreating(ModelBuilder builder) // Add all types T using IEntityTypeConfiguration builder.ApplyConfigurationsFromAssembly(Assembly.GetEntryAssembly()); - // Add all other types (auto mode) - var customTypes = Assembly.GetEntryAssembly() - .GetExportedTypes() - .Where(x => x.GetCustomAttributes() - .Any()); - foreach (var customType in customTypes.Where(x => x.GetInterface("IEntityTypeConfiguration`1") == null)) + + foreach (var customType in assemblyService.Types.Where(x => x.GetInterface("IEntityTypeConfiguration`1") == null)) builder.Entity(customType); base.OnModelCreating(builder); @@ -135,11 +132,11 @@ public static IEdmModel GetEdmModel() //} - private static IModel BuildStaticModel() - { - using var dbContext = new GenericDbContext(); - return dbContext.Model; - } + //private static IModel BuildStaticModel() + //{ + // using var dbContext = new GenericDbContext(); + // return dbContext.Model; + //} #region If you're targeting EF Core diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs index c57f366..074f150 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -10,6 +10,7 @@ using EFCore.AutomaticMigrations; using EntityFramework.Triggers; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; @@ -17,11 +18,13 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; +using Newtonsoft.Json; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Data; using TCDev.APIGenerator.Extension; using TCDev.ApiGenerator.Json; using TCDev.APIGenerator.Schema; +using TCDev.APIGenerator.Services; using TCDev.Controllers; namespace TCDev.ApiGenerator.Extension; @@ -29,6 +32,7 @@ namespace TCDev.ApiGenerator.Extension; public static class ApiGeneratorExtension { public static ApiGeneratorConfig ApiGeneratorConfig { get; set; } = new(null); + public static List JsonClasses { get; set; } = new List(); public static IServiceCollection AddApiGeneratorServices( this IServiceCollection services, @@ -62,46 +66,19 @@ public static class ApiGeneratorExtension services .AddSingleton(typeof(ITriggers<,>), typeof(Triggers<,>)) .AddSingleton(typeof(ITriggers<>), typeof(Triggers<>)) - .AddSingleton(typeof(ITriggers), typeof(Triggers)); - - // Add Services - services.AddScoped(typeof(IGenericRespository<,>), typeof(GenericRespository<,>)); - + .AddSingleton(typeof(ITriggers), typeof(Triggers)) + .AddScoped(typeof(IGenericRespository<,>), typeof(GenericRespository<,>)); //Add Framework Services & Options, we use the current assembly to get classes. + var assemblyService = new AssemblyService(); + services.AddSingleton(assemblyService); - var JsonDef = new JsonClassDefinition() - { - Name = "TestGenerated", - RouteTemplate = "/testGenerated", - Fields = new List(){ - new Field - { - Name = "Id", - Type = "int" - }, - new Field - { - Name = "Name", - Type = "string" - }, - - } - }; - - // Get all types defined in JSON - var genericAssembly = JsonClassBuilder.CreateClass(JsonDef); + var jsonDefs = JsonConvert.DeserializeObject>(System.IO.File.ReadAllText("./ApiDefinition.json")); + var jsonTypes = jsonDefs.Select(tp => JsonClassBuilder.CreateClass(tp)).ToList(); - // Get all types in entry assembly - var types = assembly.GetExportedTypes().Where(x => x.GetCustomAttributes().Any()); - - // generate type list - var typesToLoad = new List - { - genericAssembly - }; - typesToLoad.AddRange(types); + assemblyService.Types.AddRange(jsonTypes); + assemblyService.Types.AddRange(assembly.GetExportedTypes().Where(x => x.GetCustomAttributes().Any())); // Put everything together @@ -109,7 +86,7 @@ public static class ApiGeneratorExtension options.Conventions.Add(new GenericControllerRouteConvention())) .ConfigureApplicationPartManager(manager => // Add our controller feature provider - manager.FeatureProviders.Add(new GenericTypeControllerFeatureProvider(typesToLoad)) + manager.FeatureProviders.Add(new GenericTypeControllerFeatureProvider(assemblyService.Types)) ); diff --git a/src/TCDev.APIGenerator/FeatureProvider/GenericControllerRouteConvention.cs b/src/TCDev.APIGenerator/FeatureProvider/GenericControllerRouteConvention.cs index dbdf92d..860188c 100644 --- a/src/TCDev.APIGenerator/FeatureProvider/GenericControllerRouteConvention.cs +++ b/src/TCDev.APIGenerator/FeatureProvider/GenericControllerRouteConvention.cs @@ -2,6 +2,7 @@ // Apache 2.0 License // https://www.github.com/deejaytc/dotnet-utils +using System.Linq; using System.Reflection; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ApplicationModels; @@ -16,6 +17,30 @@ namespace TCDev.Controllers /// public class GenericControllerRouteConvention : IControllerModelConvention { + + + public void bla() + { + var blubb = new int[] + { + 1, + 2, + 3, + 4, + 5, + 6 + }; + + var result = blubb.Select((n, idx) => new + { + n, idx + }) + .OrderByDescending(r => r.idx) + .Select(r => r.n) + .ToArray(); + + + } public void Apply(ControllerModel controller) { diff --git a/src/TCDev.APIGenerator/Services/AssemblyService.cs b/src/TCDev.APIGenerator/Services/AssemblyService.cs new file mode 100644 index 0000000..a853822 --- /dev/null +++ b/src/TCDev.APIGenerator/Services/AssemblyService.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +namespace TCDev.APIGenerator.Services +{ + public class AssemblyService + { + public List Assemblies { get; set; } = new List(); + public List Types { get; set; } = new List(); + } +} diff --git a/src/TCDev.APIGenerator/Generator.cs b/src/TCDev.APIGenerator/Services/Generator.cs similarity index 92% rename from src/TCDev.APIGenerator/Generator.cs rename to src/TCDev.APIGenerator/Services/Generator.cs index 85335d3..1f4cfa3 100644 --- a/src/TCDev.APIGenerator/Generator.cs +++ b/src/TCDev.APIGenerator/Services/Generator.cs @@ -16,6 +16,7 @@ namespace TCDev.ApiGenerator.Json; + public class JsonClassBuilder { @@ -43,15 +44,14 @@ public class { definition.Name } : IObjectBase<{definition.IdType}> "; // Add all fields - var result1 = definition.Fields.Aggregate(string.Empty, (current, field) => current + $@" public {field.Type} {field.Name} {{ get; set;}}"); + var result1 = definition.Fields.Aggregate(string.Empty, (current, field) => + current + $@" public {field.Type} {field.Name}{(field.Nullable ? "?" : "")} {{ get; set;}}"); // Complete class classCode += result1; classCode += $@"}} }}"; - MetadataReference[] assemblies = AppDomain - .CurrentDomain - .GetAssemblies() + MetadataReference[] assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(p=>!p.IsDynamic) .Where(a => !string.IsNullOrEmpty(a.Location)) .Select(a => MetadataReference.CreateFromFile(a.Location)) .ToArray(); diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj index 0f3f383..af23382 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj @@ -7,7 +7,7 @@ Tim Cadenbach TCDev Creates fully working CRUD Apis from just models - Debug;Release;DebugWithSampleApp;SampleAppNuget + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson @@ -18,6 +18,10 @@ TCDev.ApiGenerator.xml + + TCDev.ApiGenerator.xml + + diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml index d5f7ae2..ff21077 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml @@ -83,11 +83,11 @@ Names of assemblies to search for classes - + Initiate the Controller generator - Names of assemblies to search for classes + Names of assemblies to search for classes From 39a2228df37e7aca1c2b8a88ae31b37a4ebc0781 Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Thu, 7 Apr 2022 16:43:24 +0200 Subject: [PATCH 08/17] changes --- .editorconfig | 77 +++++ .github/workflows/dotnet.yml | 37 +++ .../ApiGeneratorSampleApp.xml | 2 +- .../Model/MinimalSample.cs | 43 +-- sample/ApiGeneratorSampleApp/Model/Person.cs | 2 +- .../Schema/Draftable.cs | 47 --- .../Schema/SoftDeletable.cs | 35 +++ .../Schema/Trackable.cs | 21 ++ .../Data/GenericDbContext.cs | 253 +++++++--------- .../Extension/ApiGeneratorExtension.cs | 278 +++++++++--------- .../GenericControllerRouteConvention.cs | 67 ++--- src/TCDev.APIGenerator/TCDev.APIGenerator.xml | 4 +- 12 files changed, 472 insertions(+), 394 deletions(-) create mode 100644 .github/workflows/dotnet.yml delete mode 100644 src/TCDev.APIGenerator.Schema/Schema/Draftable.cs create mode 100644 src/TCDev.APIGenerator.Schema/Schema/SoftDeletable.cs create mode 100644 src/TCDev.APIGenerator.Schema/Schema/Trackable.cs diff --git a/.editorconfig b/.editorconfig index 17c130c..87da84e 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,3 +2,80 @@ # CS1591: Fehledes XML-Kommentar für öffentlich sichtbaren Typ oder Element dotnet_diagnostic.CS1591.severity = none +csharp_indent_labels = one_less_than_current +csharp_using_directive_placement = outside_namespace:silent +csharp_prefer_simple_using_statement = true:suggestion +csharp_prefer_braces = true:silent +csharp_style_namespace_declarations = block_scoped:silent +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = false:silent +csharp_space_around_binary_operators = before_and_after + +[*.{cs,vb}] +#### Naming styles #### + +# Naming rules + +dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion +dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface +dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i + +dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.types_should_be_pascal_case.symbols = types +dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case + +dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members +dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case + +# Symbol specifications + +dotnet_naming_symbols.interface.applicable_kinds = interface +dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.interface.required_modifiers = + +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum +dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.types.required_modifiers = + +dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method +dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected +dotnet_naming_symbols.non_field_members.required_modifiers = + +# Naming styles + +dotnet_naming_style.begins_with_i.required_prefix = I +dotnet_naming_style.begins_with_i.required_suffix = +dotnet_naming_style.begins_with_i.word_separator = +dotnet_naming_style.begins_with_i.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case + +dotnet_naming_style.pascal_case.required_prefix = +dotnet_naming_style.pascal_case.required_suffix = +dotnet_naming_style.pascal_case.word_separator = +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +indent_size = 4 +end_of_line = crlf +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..ac544a2 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,37 @@ +name: .NET + +on: + push: + branches: + - vnext + - main + +env: + WORKING_DIRECTORY: . + CONFIGURATION: Release + DOTNET_CORE_VERSION: 6.0.x + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_CORE_VERSION }} + - name: Setup SwashBuckle .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.SWASHBUCKLE_DOTNET_CORE_VERSION }} + - name: Restore + run: dotnet restore ${{ env.WORKING_DIRECTORY }} + - name: Build + run: dotnet build ${{ env.WORKING_DIRECTORY }} --configuration ${{ env.CONFIGURATION }} --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal +© 2022 GitHub, Inc. +Terms +Privacy +Security +Status diff --git a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApp.xml b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApp.xml index 2d38add..023cd23 100644 --- a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApp.xml +++ b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApp.xml @@ -6,7 +6,7 @@ - This is the minimal sample, yes this is a working api ;) + This is the minimal sample, yes this is a working api ;) diff --git a/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs b/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs index 5a7a49e..35aef05 100644 --- a/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs +++ b/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs @@ -1,28 +1,35 @@ // TCDev.de 2022/03/24 // ApiGeneratorSampleApI.MinimalSample.cs -// https://www.github.com/deejaytc/dotnet-utils +// https://github.com/DeeJayTC/net-dynamic-api using System.Text.Json.Serialization; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Interfaces; -namespace ApiGeneratorSampleApI.Model; - -/// -/// This is the minimal sample, yes this is a working api ;) -/// -[Api("/minimal")] -public class MinimalSample : IObjectBase +namespace ApiGeneratorSampleApI.Model { - public string Name { get; set; } - public int Value { get; set; } - public int Id { get; set; } -} + /// + /// This is the minimal sample, yes this is a working api ;) + /// + [Api("/minimal")] + public class MinimalSample : IObjectBase + { + public string Name { get; set; } + public int Value { get; set; } + public int Id { get; set; } -[JsonConverter(typeof(JsonStringEnumConverter))] -public enum OperationEnum -{ - Insert, - Update, - Delete + + public string CalculateSum(int a, int b) + { + return $"{a} + {b} = {a + b}"; + } + } + + [JsonConverter(typeof(JsonStringEnumConverter))] + public enum OperationEnum + { + Insert, + Update, + Delete + } } diff --git a/sample/ApiGeneratorSampleApp/Model/Person.cs b/sample/ApiGeneratorSampleApp/Model/Person.cs index 06d456c..8e6d472 100644 --- a/sample/ApiGeneratorSampleApp/Model/Person.cs +++ b/sample/ApiGeneratorSampleApp/Model/Person.cs @@ -9,8 +9,8 @@ using System.Threading.Tasks; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Interfaces; -using TCDev.ApiGenerator.Schemes.Interfaces; using TCDev.APIGenerator.Schema.Interfaces; +using TCDev.ApiGenerator.Schemes; namespace ApiGeneratorSampleApI.Model { diff --git a/src/TCDev.APIGenerator.Schema/Schema/Draftable.cs b/src/TCDev.APIGenerator.Schema/Schema/Draftable.cs deleted file mode 100644 index 7006d48..0000000 --- a/src/TCDev.APIGenerator.Schema/Schema/Draftable.cs +++ /dev/null @@ -1,47 +0,0 @@ -// TCDev 2022/03/16 -// Apache 2.0 License -// https://www.github.com/deejaytc/dotnet-utils - -using System; -using EntityFramework.Triggers; - -namespace TCDev.ApiGenerator.Schemes.Interfaces -{ - public abstract class Trackable - { - static Trackable() - { - Triggers.Inserting += entry => entry.Entity.Inserted = DateTime.UtcNow; - Triggers.Updating += entry => entry.Entity.Updated = DateTime.UtcNow; - } - - public virtual DateTime Inserted { get; private set; } = DateTime.UtcNow; - public virtual DateTime? Updated { get; private set; } - } - - public abstract class SoftDeletable : Trackable - { - static SoftDeletable() - { - Triggers.Deleting += entry => - { - entry.Entity.SoftDelete(); - entry.Cancel = true; // Cancels the deletion, but will persist changes with the same effects as EntityState.Modified - }; - } - - public virtual DateTime? DeletedAt { get; private set; } - - public bool IsSoftDeleted => DeletedAt != null; - - public void SoftDelete() - { - DeletedAt = DateTime.UtcNow; - } - - public void SoftRestore() - { - DeletedAt = null; - } - } -} \ No newline at end of file diff --git a/src/TCDev.APIGenerator.Schema/Schema/SoftDeletable.cs b/src/TCDev.APIGenerator.Schema/Schema/SoftDeletable.cs new file mode 100644 index 0000000..58a2f0c --- /dev/null +++ b/src/TCDev.APIGenerator.Schema/Schema/SoftDeletable.cs @@ -0,0 +1,35 @@ +// TCDev.de 2022/04/07 +// TCDev.APIGenerator.Schema.SoftDeletable.cs +// https://github.com/DeeJayTC/net-dynamic-api + +using System; +using EntityFramework.Triggers; + +namespace TCDev.ApiGenerator.Schemes.Interface +{ + public abstract class SoftDeletable : Trackable + { + public virtual DateTime? DeletedAt { get; private set; } + + public bool IsSoftDeleted => this.DeletedAt != null; + + public void SoftDelete() + { + this.DeletedAt = DateTime.UtcNow; + } + + public void SoftRestore() + { + this.DeletedAt = null; + } + + static SoftDeletable() + { + Triggers.Deleting += entry => + { + entry.Entity.SoftDelete(); + entry.Cancel = true; // Cancels the deletion, but will persist changes with the same effects as EntityState.Modified + }; + } + } +} diff --git a/src/TCDev.APIGenerator.Schema/Schema/Trackable.cs b/src/TCDev.APIGenerator.Schema/Schema/Trackable.cs new file mode 100644 index 0000000..61b0140 --- /dev/null +++ b/src/TCDev.APIGenerator.Schema/Schema/Trackable.cs @@ -0,0 +1,21 @@ +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.Schema.Trackable123.cs +// https://github.com/DeeJayTC/net-dynamic-api + +using System; +using EntityFramework.Triggers; + +namespace TCDev.ApiGenerator.Schemes +{ + public abstract class Trackable + { + public virtual DateTime Inserted { get; private set; } = DateTime.UtcNow; + public virtual DateTime? Updated { get; private set; } + + static Trackable() + { + Triggers.Inserting += entry => entry.Entity.Inserted = DateTime.UtcNow; + Triggers.Updating += entry => entry.Entity.Updated = DateTime.UtcNow; + } + } +} diff --git a/src/TCDev.APIGenerator/Data/GenericDbContext.cs b/src/TCDev.APIGenerator/Data/GenericDbContext.cs index 88b8f2c..082a8c0 100644 --- a/src/TCDev.APIGenerator/Data/GenericDbContext.cs +++ b/src/TCDev.APIGenerator/Data/GenericDbContext.cs @@ -5,162 +5,121 @@ using System; using System.IO; using System.Linq; -using System.Reflection; using System.Threading; using System.Threading.Tasks; using EntityFrameworkCore.Triggers; using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.Extensions.Configuration; using Microsoft.OData.Edm; using Microsoft.OData.ModelBuilder; -using TCDev.ApiGenerator.Attributes; using TCDev.APIGenerator.Services; -namespace TCDev.ApiGenerator.Data; - -public class GenericDbContext : DbContext +namespace TCDev.ApiGenerator.Data { - protected IHttpContextAccessor HttpContextAccessor { get; } - - //public static IModel StaticModel { get; } = BuildStaticModel(); - public static IEdmModel EdmModel { get; } = GetEdmModel(); - private readonly AssemblyService assemblyService; - - //public GenericDbContext() - //{ - //} - - public GenericDbContext( - DbContextOptions options, - IConfiguration config, - AssemblyService assemblyService) : base(options) - { - this.assemblyService = assemblyService; - } - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - if (!optionsBuilder.IsConfigured) - { - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json") - .AddJsonFile("secrets.json", true) - .Build(); - var config = new ApiGeneratorConfig(configuration); - // Add Database Context - - switch (config.DatabaseOptions.DatabaseType) - { - case DbType.InMemory: - optionsBuilder.UseInMemoryDatabase("ApiGeneratorDB"); - break; - case DbType.Sql: - var connectionStringSql = configuration.GetConnectionString("ApiGeneratorDatabase"); - optionsBuilder.UseSqlServer(connectionStringSql); - break; - case DbType.SqLite: - var connectionStringSqLite = configuration.GetConnectionString("ApiGeneratorDatabase"); - optionsBuilder.UseSqlite(connectionStringSqLite); - break; - default: - throw new Exception("Database Type Unkown"); - } - } - } - - // -> Tenant Isolation - //public void SetGlobalQuery(ModelBuilder builder) where T : EntityBase - //{ - // var user = HttpContextAccessor.HttpContext.GetUser(); - // builder.Entity().HasKey(e => e.Id); - // builder.Entity().HasQueryFilter(e => e.TenantId == user.TenantId); - //} - - protected override void OnModelCreating(ModelBuilder builder) - { - // Add all types T using IEntityTypeConfiguration - builder.ApplyConfigurationsFromAssembly(Assembly.GetEntryAssembly()); - - - foreach (var customType in assemblyService.Types.Where(x => x.GetInterface("IEntityTypeConfiguration`1") == null)) - builder.Entity(customType); - - base.OnModelCreating(builder); - } - - - /// - /// Generate EDM Model for OData functionalities - /// - /// - public static IEdmModel GetEdmModel() - { - var customTypes = Assembly.GetEntryAssembly() - .GetExportedTypes() - .Where(x => x.GetCustomAttributes() - .Any()); - var builder = new ODataConventionModelBuilder(); - foreach (var customType in customTypes) - { - var newType = builder.AddEntityType(customType); - } - - return builder.GetEdmModel(); - } - - - //public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default) - //{ - // //var entries = ChangeTracker - // // .Entries() - // // .Where(e => - // // e.Entity is IObjectBase - // // && (e.State == EntityState.Added - // // || e.State == EntityState.Modified - // // || e.State == EntityState.Deleted - // // ) - // // ); - - // //UpdateEntries(user, entries); - // //UpdateEntries(user, entries); - // //UpdateEntries(user, entries); - - // return base.SaveChangesAsync(acceptAllChangesOnSuccess, cancellationToken); - //} - - - //private static IModel BuildStaticModel() - //{ - // using var dbContext = new GenericDbContext(); - // return dbContext.Model; - //} - - - #region If you're targeting EF Core - - public override int SaveChanges() - { - return this.SaveChangesWithTriggers(base.SaveChanges); - } - - public override int SaveChanges(bool acceptAllChangesOnSuccess) - { - return this.SaveChangesWithTriggers(base.SaveChanges, acceptAllChangesOnSuccess); - } - - public override Task SaveChangesAsync(CancellationToken cancellationToken = default) - { - return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, true, cancellationToken); - } - - public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, - CancellationToken cancellationToken = default) - { - return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, acceptAllChangesOnSuccess, cancellationToken); - } - - #endregion + public class GenericDbContext : DbContext + { + //public static IModel StaticModel { get; } = BuildStaticModel(); + private readonly AssemblyService assemblyService; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (optionsBuilder.IsConfigured) + { + return; + } + + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .AddJsonFile("secrets.json", true) + .Build(); + var config = new ApiGeneratorConfig(configuration); + // Add Database Context + + switch (config.DatabaseOptions.DatabaseType) + { + case DbType.InMemory: + optionsBuilder.UseInMemoryDatabase("ApiGeneratorDB"); + break; + case DbType.Sql: + var connectionStringSql = configuration.GetConnectionString("ApiGeneratorDatabase"); + optionsBuilder.UseSqlServer(connectionStringSql); + break; + case DbType.SqLite: + var connectionStringSqLite = configuration.GetConnectionString("ApiGeneratorDatabase"); + optionsBuilder.UseSqlite(connectionStringSqLite); + break; + default: + throw new Exception("Database Type Unknown"); + } + } + + protected override void OnModelCreating(ModelBuilder builder) + { + // Add all types T using IEntityTypeConfiguration + foreach (var asm in this.assemblyService.Assemblies) + { + builder.ApplyConfigurationsFromAssembly(asm); + } + + // Add all other custom types, not implementing IEntityTypeConfiguration + foreach (var customType in this.assemblyService.Types.Where(x => x.IsAssignableFrom(typeof(IEntityTypeConfiguration<>)))) + { + builder.Entity(customType); + } + + base.OnModelCreating(builder); + } + + + /// + /// Generate EDM Model for OData functionality + /// + /// + public static IEdmModel GetEdmModel(AssemblyService service) + { + var builder = new ODataConventionModelBuilder(); + foreach (var customType in service.Types) + { + builder.AddEntityType(customType); + } + + return builder.GetEdmModel(); + } + + + public GenericDbContext( + DbContextOptions options, + IConfiguration config, + AssemblyService assemblyService) : base(options) + { + this.assemblyService = assemblyService; + } + + #region If you're targeting EF Core + + public override int SaveChanges() + { + return this.SaveChangesWithTriggers(base.SaveChanges); + } + + public override int SaveChanges(bool acceptAllChangesOnSuccess) + { + return this.SaveChangesWithTriggers(base.SaveChanges, acceptAllChangesOnSuccess); + } + + public override Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, true, cancellationToken); + } + + public override Task SaveChangesAsync(bool acceptAllChangesOnSuccess, + CancellationToken cancellationToken = default) + { + return this.SaveChangesWithTriggersAsync(base.SaveChangesAsync, acceptAllChangesOnSuccess, cancellationToken); + } + + #endregion + } } diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs index 074f150..62d2a1e 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -1,20 +1,19 @@ // TCDev.de 2022/03/16 // TCDev.APIGenerator.ApiGeneratorExtension.cs -// https://www.github.com/deejaytc/dotnet-utils +// https://github.com/DeeJayTC/net-dynamic-api using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Reflection; using System.Text.Json.Serialization; using EFCore.AutomaticMigrations; using EntityFramework.Triggers; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.OData; using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Azure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; @@ -27,140 +26,157 @@ using TCDev.APIGenerator.Services; using TCDev.Controllers; -namespace TCDev.ApiGenerator.Extension; - -public static class ApiGeneratorExtension +namespace TCDev.ApiGenerator.Extension { - public static ApiGeneratorConfig ApiGeneratorConfig { get; set; } = new(null); - public static List JsonClasses { get; set; } = new List(); - - public static IServiceCollection AddApiGeneratorServices( - this IServiceCollection services, - IConfiguration config, - Assembly assembly) - { - ApiGeneratorConfig = new ApiGeneratorConfig(config); - - // Add Database Context - - switch (ApiGeneratorConfig.DatabaseOptions.DatabaseType) - { - case DbType.InMemory: - services.AddDbContext(options => - options.UseInMemoryDatabase("ApiGeneratorDB")); - break; - case DbType.Sql: - services.AddDbContext(options => - options.UseSqlServer(config.GetConnectionString("ApiGeneratorDatabase"), - b => b.MigrationsAssembly(assembly.FullName))); - break; - case DbType.SqLite: - services.AddDbContext(options => - options.UseSqlite(config.GetConnectionString("ApiGeneratorDatabase"), - b => b.MigrationsAssembly(assembly.FullName))); - break; - default: - throw new Exception("Database Type Unkown"); - } - - services - .AddSingleton(typeof(ITriggers<,>), typeof(Triggers<,>)) - .AddSingleton(typeof(ITriggers<>), typeof(Triggers<>)) - .AddSingleton(typeof(ITriggers), typeof(Triggers)) - .AddScoped(typeof(IGenericRespository<,>), typeof(GenericRespository<,>)); - - //Add Framework Services & Options, we use the current assembly to get classes. - var assemblyService = new AssemblyService(); - services.AddSingleton(assemblyService); - - - var jsonDefs = JsonConvert.DeserializeObject>(System.IO.File.ReadAllText("./ApiDefinition.json")); - var jsonTypes = jsonDefs.Select(tp => JsonClassBuilder.CreateClass(tp)).ToList(); - - assemblyService.Types.AddRange(jsonTypes); - assemblyService.Types.AddRange(assembly.GetExportedTypes().Where(x => x.GetCustomAttributes().Any())); - - - // Put everything together - services.AddMvc(options => - options.Conventions.Add(new GenericControllerRouteConvention())) - .ConfigureApplicationPartManager(manager => - // Add our controller feature provider - manager.FeatureProviders.Add(new GenericTypeControllerFeatureProvider(assemblyService.Types)) - ); - - - services.AddSwaggerGen(c => - { - c.SwaggerDoc(ApiGeneratorConfig.SwaggerOptions.Version, - new OpenApiInfo + public static class ApiGeneratorExtension + { + public static ApiGeneratorConfig ApiGeneratorConfig { get; set; } = new(null); + + public static IServiceCollection AddApiGeneratorServices( + this IServiceCollection services, + IConfiguration config, + Assembly assembly) + { + ApiGeneratorConfig = new ApiGeneratorConfig(config); + + // Add Database Context + + switch (ApiGeneratorConfig.DatabaseOptions.DatabaseType) { - Title = ApiGeneratorConfig.SwaggerOptions.Title, - Version = ApiGeneratorConfig.SwaggerOptions.Version, - Description = ApiGeneratorConfig.SwaggerOptions.Description, - Contact = new OpenApiContact - { - Email = ApiGeneratorConfig.SwaggerOptions.ContactMail, Url = new Uri(ApiGeneratorConfig.SwaggerOptions.ContactUri) - } + case DbType.InMemory: + services.AddDbContext(options => + options.UseInMemoryDatabase("ApiGeneratorDB")); + break; + case DbType.Sql: + services.AddDbContext(options => + options.UseSqlServer(config.GetConnectionString("ApiGeneratorDatabase"), + b => b.MigrationsAssembly(assembly.FullName))); + break; + + + case DbType.SqLite: + services.AddDbContext(options => + options.UseSqlite(config.GetConnectionString("ApiGeneratorDatabase"), + b => b.MigrationsAssembly(assembly.FullName))); + break; + default: + throw new Exception("Database Type Unkown"); + } + + services + .AddSingleton(typeof(ITriggers<,>), typeof(Triggers<,>)) + .AddSingleton(typeof(ITriggers<>), typeof(Triggers<>)) + .AddSingleton(typeof(ITriggers), typeof(Triggers)) + .AddScoped(typeof(IGenericRespository<,>), typeof(GenericRespository<,>)); + + //Add Framework Services & Options, we use the current assembly to get classes. + var assemblyService = new AssemblyService(); + services.AddSingleton(assemblyService); + + var jsonDefs = JsonConvert.DeserializeObject>(File.ReadAllText("./ApiDefinition.json")); + var jsonTypes = jsonDefs.Select(tp => JsonClassBuilder.CreateClass(tp)) + .ToList(); + + assemblyService.Types.AddRange(jsonTypes); + assemblyService.Types.AddRange(assembly.GetExportedTypes() + .Where(x => x.GetCustomAttributes() + .Any())); + + + // Put everything together + services.AddMvc(options => + options.Conventions.Add(new GenericControllerRouteConvention())) + .ConfigureApplicationPartManager(manager => + // Add our controller feature provider + manager.FeatureProviders.Add(new GenericTypeControllerFeatureProvider(assemblyService.Types)) + ); + + + services.AddSwaggerGen(c => + { + c.SwaggerDoc(ApiGeneratorConfig.SwaggerOptions.Version, + new OpenApiInfo + { + Title = ApiGeneratorConfig.SwaggerOptions.Title, + Version = ApiGeneratorConfig.SwaggerOptions.Version, + Description = ApiGeneratorConfig.SwaggerOptions.Description, + Contact = new OpenApiContact + { + Email = ApiGeneratorConfig.SwaggerOptions.ContactMail, Url = new Uri(ApiGeneratorConfig.SwaggerOptions.ContactUri) + } + }); + + c.DocumentFilter(); + c.SchemaFilter(); + + if (ApiGeneratorConfig.ApiOptions.UseXmlComments) + { + if (!string.IsNullOrEmpty(ApiGeneratorConfig.ApiOptions.XmlCommentsFile)) + { + throw new Exception("You need to set XMLCommentsFile option when using XMl Comments"); + } + + c.IncludeXmlComments(ApiGeneratorConfig.ApiOptions.XmlCommentsFile, true); + } }); - c.DocumentFilter(); - c.SchemaFilter(); - if (ApiGeneratorConfig.ApiOptions.UseXmlComments) - { - if (!string.IsNullOrEmpty(ApiGeneratorConfig.ApiOptions.XmlCommentsFile)) - throw new Exception("You need to set XMLCommentsFile option when using XMl Comments"); - c.IncludeXmlComments(ApiGeneratorConfig.ApiOptions.XmlCommentsFile, true); - } - }); + if (ApiGeneratorConfig.ODataOptions.Enabled) + { + services + .AddControllers() + .AddOData(opt => + { + opt.AddRouteComponents("odata", GenericDbContext.GetEdmModel(assemblyService)); + opt.EnableNoDollarQueryOptions = true; + opt.EnableQueryFeatures(20000); + opt.Select() + .Expand() + .Filter(); + }) + .AddJsonOptions(o => { o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } + ); + } + else + { + services + .AddControllers() + .AddJsonOptions(o => { o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); + } + + return services; + } - if (ApiGeneratorConfig.ODataOptions.Enabled) - services - .AddControllers() - .AddOData(opt => + public static IApplicationBuilder UseAutomaticApiMigrations(this IApplicationBuilder app, bool allowDataLoss = false) + { + using var serviceScope = app.ApplicationServices.CreateScope(); + var dbContext = serviceScope.ServiceProvider.GetService(); + if (ApiGeneratorConfig.DatabaseOptions.DatabaseType != DbType.InMemory) { - opt.AddRouteComponents("odata", GenericDbContext.EdmModel); - opt.EnableNoDollarQueryOptions = true; - opt.EnableQueryFeatures(20000); - opt.Select() - .Expand() - .Filter(); - }) - .AddJsonOptions(o => { o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } - ); - else - services - .AddControllers() - .AddJsonOptions(o => { o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); - - return services; - } - - - public static IApplicationBuilder UseAutomaticApiMigrations(this IApplicationBuilder app, bool allowDataLoss = false) - { - using var serviceScope = app.ApplicationServices.CreateScope(); - var dbContext = serviceScope.ServiceProvider.GetService(); - if (ApiGeneratorConfig.DatabaseOptions.DatabaseType != DbType.InMemory) - dbContext.MigrateToLatestVersion(new DbMigrationsOptions - { - AutomaticMigrationsEnabled = true, AutomaticMigrationDataLossAllowed = allowDataLoss - }); - - return app; - } - - public static IApplicationBuilder UseApiGenerator(this IApplicationBuilder app) - { - app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", $"{ApiGeneratorConfig.SwaggerOptions.Title} {ApiGeneratorConfig.SwaggerOptions.Version}")); - return app; - } - - public static IEndpointRouteBuilder UseApiGeneratorEndpoints(this IEndpointRouteBuilder builder) - { - return builder; - } + dbContext.MigrateToLatestVersion(new DbMigrationsOptions + { + AutomaticMigrationsEnabled = true, + AutomaticMigrationDataLossAllowed = allowDataLoss + }); + } + + return app; + } + + public static IApplicationBuilder UseApiGenerator(this IApplicationBuilder app) + { + app.UseSwagger(); + app.UseSwaggerUI(c => c.SwaggerEndpoint( + "/swagger/v1/swagger.json", + $"{ApiGeneratorConfig.SwaggerOptions.Title} {ApiGeneratorConfig.SwaggerOptions.Version}" + )); + return app; + } + + public static IEndpointRouteBuilder UseApiGeneratorEndpoints(this IEndpointRouteBuilder builder) + { + return builder; + } + } } diff --git a/src/TCDev.APIGenerator/FeatureProvider/GenericControllerRouteConvention.cs b/src/TCDev.APIGenerator/FeatureProvider/GenericControllerRouteConvention.cs index 860188c..212755b 100644 --- a/src/TCDev.APIGenerator/FeatureProvider/GenericControllerRouteConvention.cs +++ b/src/TCDev.APIGenerator/FeatureProvider/GenericControllerRouteConvention.cs @@ -17,55 +17,28 @@ namespace TCDev.Controllers /// public class GenericControllerRouteConvention : IControllerModelConvention { - - - public void bla() - { - var blubb = new int[] - { - 1, - 2, - 3, - 4, - 5, - 6 - }; - - var result = blubb.Select((n, idx) => new - { - n, idx - }) - .OrderByDescending(r => r.idx) - .Select(r => r.n) - .ToArray(); - - - } - public void Apply(ControllerModel controller) { - if (controller.ControllerType.IsGenericType) - { - var genericType = controller.ControllerType.GenericTypeArguments[0]; - var customNameAttribute = genericType.GetCustomAttribute(); - controller.ControllerName = genericType.Name; - - if (customNameAttribute?.Route != null) - { - if (controller.Selectors.Count > 0) - { - var currentSelector = controller.Selectors[0]; - currentSelector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(customNameAttribute.Route)); - } - else - { - controller.Selectors.Add(new SelectorModel - { - AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(customNameAttribute.Route)) - }); - } - } - } + if (!controller.ControllerType.IsGenericType) return; + + var genericType = controller.ControllerType.GenericTypeArguments[0]; + var customNameAttribute = genericType.GetCustomAttribute(); + controller.ControllerName = genericType.Name; + + if (customNameAttribute?.Route == null) return; + + if (controller.Selectors.Count > 0) + { + var currentSelector = controller.Selectors[0]; + currentSelector.AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(customNameAttribute.Route)); + } + else + { + controller.Selectors.Add(new SelectorModel + { + AttributeRouteModel = new AttributeRouteModel(new RouteAttribute(customNameAttribute.Route)) + }); + } } } } \ No newline at end of file diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml index ff21077..f1f8beb 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml @@ -60,9 +60,9 @@ - + - Generate EDM Model for OData functionalities + Generate EDM Model for OData functionality From 89a2e4466345ee62675046e5bfa528109c4628ca Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Thu, 7 Apr 2022 16:43:57 +0200 Subject: [PATCH 09/17] workflow vnext --- .github/workflows/dotnet.yml | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 .github/workflows/dotnet.yml diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..ac544a2 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,37 @@ +name: .NET + +on: + push: + branches: + - vnext + - main + +env: + WORKING_DIRECTORY: . + CONFIGURATION: Release + DOTNET_CORE_VERSION: 6.0.x + +jobs: + build: + runs-on: windows-latest + steps: + - uses: actions/checkout@v2 + - name: Setup .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.DOTNET_CORE_VERSION }} + - name: Setup SwashBuckle .NET Core + uses: actions/setup-dotnet@v1 + with: + dotnet-version: ${{ env.SWASHBUCKLE_DOTNET_CORE_VERSION }} + - name: Restore + run: dotnet restore ${{ env.WORKING_DIRECTORY }} + - name: Build + run: dotnet build ${{ env.WORKING_DIRECTORY }} --configuration ${{ env.CONFIGURATION }} --no-restore + - name: Test + run: dotnet test --no-build --verbosity normal +© 2022 GitHub, Inc. +Terms +Privacy +Security +Status From c3d4e12b36c0c9633678c59daf4669da3184228b Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Thu, 7 Apr 2022 16:44:48 +0200 Subject: [PATCH 10/17] remove crap --- .github/workflows/dotnet.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index ac544a2..4fdd37e 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -30,8 +30,3 @@ jobs: run: dotnet build ${{ env.WORKING_DIRECTORY }} --configuration ${{ env.CONFIGURATION }} --no-restore - name: Test run: dotnet test --no-build --verbosity normal -© 2022 GitHub, Inc. -Terms -Privacy -Security -Status From 442f3bb372493e1e7b4814f6fd3e588dcae8a353 Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Sun, 10 Apr 2022 11:53:50 +0200 Subject: [PATCH 11/17] json api's poc --- .github/workflows/dotnet.yml | 5 - .../ApiGeneratorSampleApp/ApiDefinition.json | 40 +- .../ApiGeneratorSampleApI.csproj | 6 + .../ApiGeneratorSampleApI.xml | 17 +- sample/ApiGeneratorSampleApp/Model/Car.cs | 6 +- .../Model/MinimalSample.cs | 15 - sample/ApiGeneratorSampleApp/Model/Person.cs | 49 +- sample/ApiGeneratorSampleApp/Program.cs | 7 +- .../wwwroot/SwaggerDarkTheme.css | 1329 +++++++++++++++++ .../Controller/GenericController.cs | 2 +- .../Data/GenericDbContext.cs | 5 +- .../Extension/ApiGeneratorExtension.cs | 18 +- src/TCDev.APIGenerator/Services/Generator.cs | 128 +- .../TCDev.APIGenerator.csproj | 2 +- 14 files changed, 1458 insertions(+), 171 deletions(-) create mode 100644 sample/ApiGeneratorSampleApp/wwwroot/SwaggerDarkTheme.css diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index ac544a2..4fdd37e 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -30,8 +30,3 @@ jobs: run: dotnet build ${{ env.WORKING_DIRECTORY }} --configuration ${{ env.CONFIGURATION }} --no-restore - name: Test run: dotnet test --no-build --verbosity normal -© 2022 GitHub, Inc. -Terms -Privacy -Security -Status diff --git a/sample/ApiGeneratorSampleApp/ApiDefinition.json b/sample/ApiGeneratorSampleApp/ApiDefinition.json index 5d99336..85cb0fc 100644 --- a/sample/ApiGeneratorSampleApp/ApiDefinition.json +++ b/sample/ApiGeneratorSampleApp/ApiDefinition.json @@ -1,17 +1,41 @@ [ + { - "name": "Car", - "route": "/cars", - "caching": true, - "cacheLiveTime": 1000, - "events": "POST,PUT,DELETE", + "name": "MakeJSON", + "route": "/MakeJSON", "idType": "int", "Fields": [ { "name": "Name", - "type": "String", - "maxLength": "200", - "nullable": false + "type": "string" + }, + { + "name": "Description", + "Type": "string" + + } + ] + }, + { + "name": "CarJSON", + "route": "/CarJSON", + "idType": "int", + "Fields": [ + { + "name": "Name", + "type": "string" + }, + { + "name": "Description", + "Type": "string" + }, + { + "name": "Make", + "type": "virtual MakeJSON" + }, + { + "name": "MakeId", + "type": "int" } ] } diff --git a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj index f9aceb9..ad64568 100644 --- a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj +++ b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj @@ -40,4 +40,10 @@ + + + Always + + + diff --git a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.xml b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.xml index c9fb4fb..b8dc9c7 100644 --- a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.xml +++ b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.xml @@ -6,23 +6,8 @@ - This is the minimal sample, yes this is a working api ;) + This is the minimal sample, yes this is a working api ;) - - - Before Delete Hook - - - - - - - Before Update Hook - - - - - diff --git a/sample/ApiGeneratorSampleApp/Model/Car.cs b/sample/ApiGeneratorSampleApp/Model/Car.cs index b907c68..c13b209 100644 --- a/sample/ApiGeneratorSampleApp/Model/Car.cs +++ b/sample/ApiGeneratorSampleApp/Model/Car.cs @@ -1,8 +1,6 @@ -using Swashbuckle.AspNetCore.Annotations; -using System; +using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; -using System.Text.Json.Serialization; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Interfaces; @@ -17,6 +15,8 @@ public class Car : IObjectBase [SwaggerIgnore] public Guid Id { get; set; } = Guid.NewGuid(); + + [EmailAddress] public string Name { get; set; } public string Description { get; set; } diff --git a/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs b/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs index 35aef05..1e96359 100644 --- a/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs +++ b/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs @@ -2,7 +2,6 @@ // ApiGeneratorSampleApI.MinimalSample.cs // https://github.com/DeeJayTC/net-dynamic-api -using System.Text.Json.Serialization; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Interfaces; @@ -17,19 +16,5 @@ public class MinimalSample : IObjectBase public string Name { get; set; } public int Value { get; set; } public int Id { get; set; } - - - public string CalculateSum(int a, int b) - { - return $"{a} + {b} = {a + b}"; - } - } - - [JsonConverter(typeof(JsonStringEnumConverter))] - public enum OperationEnum - { - Insert, - Update, - Delete } } diff --git a/sample/ApiGeneratorSampleApp/Model/Person.cs b/sample/ApiGeneratorSampleApp/Model/Person.cs index 8e6d472..df0ecc6 100644 --- a/sample/ApiGeneratorSampleApp/Model/Person.cs +++ b/sample/ApiGeneratorSampleApp/Model/Person.cs @@ -2,60 +2,19 @@ // Apache 2.0 License // https://www.github.com/deejaytc/dotnet-utils -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; using System; -using System.Collections.Generic; -using System.Threading.Tasks; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Interfaces; -using TCDev.APIGenerator.Schema.Interfaces; -using TCDev.ApiGenerator.Schemes; namespace ApiGeneratorSampleApI.Model { - [Api("/people", ApiMethodsToGenerate.All )] - public class Person : Trackable, - IObjectBase, - IBeforeUpdate, // Before Update Hook - IBeforeDelete, // BeforeDelete Hook - IEntityTypeConfiguration // Configure Table Options yourself - { + [Api("/people")] + public class Person : IObjectBase + { public string Name { get; set; } public DateTime Date { get; set; } public string Description { get; set; } public int Age { get; set; } public Guid Id { get; set; } - - /// - /// Before Delete Hook - /// - /// - /// - public Task BeforeDelete(Person item) - { - // NOOOO Don't delete me! - return Task.FromResult(true); - } - - /// - /// Before Update Hook - /// - /// - /// - /// - public Task BeforeUpdate(Person newPerson, Person oldPerson) - { - newPerson.Age = 333; - - return Task.FromResult(newPerson); - } - - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("MyFancyTableName"); - //....all the other EF Core Options - } - } - + } } \ No newline at end of file diff --git a/sample/ApiGeneratorSampleApp/Program.cs b/sample/ApiGeneratorSampleApp/Program.cs index 09b6bf4..2a3ac03 100644 --- a/sample/ApiGeneratorSampleApp/Program.cs +++ b/sample/ApiGeneratorSampleApp/Program.cs @@ -1,13 +1,8 @@ -// TCDev.de 2022/03/16 -// ApiGeneratorSampleApI.Program.cs -// https://www.github.com/deejaytc/dotnet-utils - using System.Reflection; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using TCDev.ApiGenerator.Extension; - var builder = WebApplication.CreateBuilder(args); // Add services to the container. @@ -25,7 +20,7 @@ app.UseAutomaticApiMigrations(true); app.UseHttpsRedirection(); - +app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); diff --git a/sample/ApiGeneratorSampleApp/wwwroot/SwaggerDarkTheme.css b/sample/ApiGeneratorSampleApp/wwwroot/SwaggerDarkTheme.css new file mode 100644 index 0000000..fe9c05f --- /dev/null +++ b/sample/ApiGeneratorSampleApp/wwwroot/SwaggerDarkTheme.css @@ -0,0 +1,1329 @@ +a { + color: #8c8cfa; +} + +::-webkit-scrollbar-track-piece { + background-color: rgba(255, 255, 255, .2) !important; +} + +::-webkit-scrollbar-track { + background-color: rgba(255, 255, 255, .3) !important; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, .5) !important; +} + +embed[type="application/pdf"] { + filter: invert(90%); +} + +html { + background: #1f1f1f !important; + box-sizing: border-box; + filter: contrast(100%) brightness(100%) saturate(100%); + overflow-y: scroll; +} + +body { + background: #1f1f1f; + background-color: #1f1f1f; + background-image: none !important; +} + +button, input, select, textarea { + background-color: #1f1f1f; + color: #bfbfbf; +} + +font, html { + color: #bfbfbf; +} + +.swagger-ui, .swagger-ui section h3 { + color: #b5bac9; +} + + .swagger-ui a { + background-color: transparent; + } + + .swagger-ui mark { + background-color: #664b00; + color: #bfbfbf; + } + + .swagger-ui legend { + color: inherit; + } + + .swagger-ui .debug * { + outline: #e6da99 solid 1px; + } + + .swagger-ui .debug-white * { + outline: #fff solid 1px; + } + + .swagger-ui .debug-black * { + outline: #bfbfbf solid 1px; + } + + .swagger-ui .debug-grid { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAgAAAAICAYAAADED76LAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTRDOTY4N0U2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTRDOTY4N0Q2N0VFMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3NjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3NzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsBS+GMAAAAjSURBVHjaYvz//z8DLsD4gcGXiYEAGBIKGBne//fFpwAgwAB98AaF2pjlUQAAAABJRU5ErkJggg==) 0 0; + } + + .swagger-ui .debug-grid-16 { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODYyRjhERDU2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODYyRjhERDQ2N0YyMTFFNjg2MzZDQjkwNkQ4MjgwMEIiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QTY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3QjY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PvCS01IAAABMSURBVHjaYmR4/5+BFPBfAMFm/MBgx8RAGWCn1AAmSg34Q6kBDKMGMDCwICeMIemF/5QawEipAWwUhwEjMDvbAWlWkvVBwu8vQIABAEwBCph8U6c0AAAAAElFTkSuQmCC) 0 0; + } + + .swagger-ui .debug-grid-8-solid { + background: url(data:image/jpeg;base64,/9j/4QAYRXhpZgAASUkqAAgAAAAAAAAAAAAAAP/sABFEdWNreQABAAQAAAAAAAD/4QMxaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLwA8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/PiA8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJBZG9iZSBYTVAgQ29yZSA1LjYtYzExMSA3OS4xNTgzMjUsIDIwMTUvMDkvMTAtMDE6MTA6MjAgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcD0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE1IChNYWNpbnRvc2gpIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkIxMjI0OTczNjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkIxMjI0OTc0NjdCMzExRTZCMkJDRTI0MDgxMDAyMTcxIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6QjEyMjQ5NzE2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6QjEyMjQ5NzI2N0IzMTFFNkIyQkNFMjQwODEwMDIxNzEiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz7/7gAOQWRvYmUAZMAAAAAB/9sAhAAbGhopHSlBJiZBQi8vL0JHPz4+P0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHAR0pKTQmND8oKD9HPzU/R0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0dHR0f/wAARCAAIAAgDASIAAhEBAxEB/8QAWQABAQAAAAAAAAAAAAAAAAAAAAYBAQEAAAAAAAAAAAAAAAAAAAIEEAEBAAMBAAAAAAAAAAAAAAABADECA0ERAAEDBQAAAAAAAAAAAAAAAAARITFBUWESIv/aAAwDAQACEQMRAD8AoOnTV1QTD7JJshP3vSM3P//Z) 0 0 #1c1c21; + } + + .swagger-ui .debug-grid-16-solid { + background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTExIDc5LjE1ODMyNSwgMjAxNS8wOS8xMC0wMToxMDoyMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzY3MkJEN0U2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzY3MkJEN0Y2N0M1MTFFNkIyQkNFMjQwODEwMDIxNzEiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3NjcyQkQ3QzY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3NjcyQkQ3RDY3QzUxMUU2QjJCQ0UyNDA4MTAwMjE3MSIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pve6J3kAAAAzSURBVHjaYvz//z8D0UDsMwMjSRoYP5Gq4SPNbRjVMEQ1fCRDg+in/6+J1AJUxsgAEGAA31BAJMS0GYEAAAAASUVORK5CYII=) 0 0 #1c1c21; + } + + .swagger-ui .b--black { + border-color: #000; + } + + .swagger-ui .b--near-black { + border-color: #121212; + } + + .swagger-ui .b--dark-gray { + border-color: #333; + } + + .swagger-ui .b--mid-gray { + border-color: #545454; + } + + .swagger-ui .b--gray { + border-color: #787878; + } + + .swagger-ui .b--silver { + border-color: #999; + } + + .swagger-ui .b--light-silver { + border-color: #6e6e6e; + } + + .swagger-ui .b--moon-gray { + border-color: #4d4d4d; + } + + .swagger-ui .b--light-gray { + border-color: #2b2b2b; + } + + .swagger-ui .b--near-white { + border-color: #242424; + } + + .swagger-ui .b--white { + border-color: #1c1c21; + } + + .swagger-ui .b--white-90 { + border-color: rgba(28, 28, 33, .9); + } + + .swagger-ui .b--white-80 { + border-color: rgba(28, 28, 33, .8); + } + + .swagger-ui .b--white-70 { + border-color: rgba(28, 28, 33, .7); + } + + .swagger-ui .b--white-60 { + border-color: rgba(28, 28, 33, .6); + } + + .swagger-ui .b--white-50 { + border-color: rgba(28, 28, 33, .5); + } + + .swagger-ui .b--white-40 { + border-color: rgba(28, 28, 33, .4); + } + + .swagger-ui .b--white-30 { + border-color: rgba(28, 28, 33, .3); + } + + .swagger-ui .b--white-20 { + border-color: rgba(28, 28, 33, .2); + } + + .swagger-ui .b--white-10 { + border-color: rgba(28, 28, 33, .1); + } + + .swagger-ui .b--white-05 { + border-color: rgba(28, 28, 33, .05); + } + + .swagger-ui .b--white-025 { + border-color: rgba(28, 28, 33, .024); + } + + .swagger-ui .b--white-0125 { + border-color: rgba(28, 28, 33, .01); + } + + .swagger-ui .b--black-90 { + border-color: rgba(0, 0, 0, .9); + } + + .swagger-ui .b--black-80 { + border-color: rgba(0, 0, 0, .8); + } + + .swagger-ui .b--black-70 { + border-color: rgba(0, 0, 0, .7); + } + + .swagger-ui .b--black-60 { + border-color: rgba(0, 0, 0, .6); + } + + .swagger-ui .b--black-50 { + border-color: rgba(0, 0, 0, .5); + } + + .swagger-ui .b--black-40 { + border-color: rgba(0, 0, 0, .4); + } + + .swagger-ui .b--black-30 { + border-color: rgba(0, 0, 0, .3); + } + + .swagger-ui .b--black-20 { + border-color: rgba(0, 0, 0, .2); + } + + .swagger-ui .b--black-10 { + border-color: rgba(0, 0, 0, .1); + } + + .swagger-ui .b--black-05 { + border-color: rgba(0, 0, 0, .05); + } + + .swagger-ui .b--black-025 { + border-color: rgba(0, 0, 0, .024); + } + + .swagger-ui .b--black-0125 { + border-color: rgba(0, 0, 0, .01); + } + + .swagger-ui .b--dark-red { + border-color: #bc2f36; + } + + .swagger-ui .b--red { + border-color: #c83932; + } + + .swagger-ui .b--light-red { + border-color: #ab3c2b; + } + + .swagger-ui .b--orange { + border-color: #cc6e33; + } + + .swagger-ui .b--purple { + border-color: #5e2ca5; + } + + .swagger-ui .b--light-purple { + border-color: #672caf; + } + + .swagger-ui .b--dark-pink { + border-color: #ab2b81; + } + + .swagger-ui .b--hot-pink { + border-color: #c03086; + } + + .swagger-ui .b--pink { + border-color: #8f2464; + } + + .swagger-ui .b--light-pink { + border-color: #721d4d; + } + + .swagger-ui .b--dark-green { + border-color: #1c6e50; + } + + .swagger-ui .b--green { + border-color: #279b70; + } + + .swagger-ui .b--light-green { + border-color: #228762; + } + + .swagger-ui .b--navy { + border-color: #0d1d35; + } + + .swagger-ui .b--dark-blue { + border-color: #20497e; + } + + .swagger-ui .b--blue { + border-color: #4380d0; + } + + .swagger-ui .b--light-blue { + border-color: #20517e; + } + + .swagger-ui .b--lightest-blue { + border-color: #143a52; + } + + .swagger-ui .b--washed-blue { + border-color: #0c312d; + } + + .swagger-ui .b--washed-green { + border-color: #0f3d2c; + } + + .swagger-ui .b--washed-red { + border-color: #411010; + } + + .swagger-ui .b--transparent { + border-color: transparent; + } + + .swagger-ui .b--gold, .swagger-ui .b--light-yellow, .swagger-ui .b--washed-yellow, .swagger-ui .b--yellow { + border-color: #664b00; + } + + .swagger-ui .shadow-1 { + box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2 { + box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3 { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4 { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5 { + box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; + } + +@media screen and (min-width: 30em) { + .swagger-ui .shadow-1-ns { + box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2-ns { + box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3-ns { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4-ns { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5-ns { + box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; + } +} + +@media screen and (max-width: 60em) and (min-width: 30em) { + .swagger-ui .shadow-1-m { + box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2-m { + box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3-m { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4-m { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5-m { + box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; + } +} + +@media screen and (min-width: 60em) { + .swagger-ui .shadow-1-l { + box-shadow: rgba(0, 0, 0, .2) 0 0 4px 2px; + } + + .swagger-ui .shadow-2-l { + box-shadow: rgba(0, 0, 0, .2) 0 0 8px 2px; + } + + .swagger-ui .shadow-3-l { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 4px 2px; + } + + .swagger-ui .shadow-4-l { + box-shadow: rgba(0, 0, 0, .2) 2px 2px 8px 0; + } + + .swagger-ui .shadow-5-l { + box-shadow: rgba(0, 0, 0, .2) 4px 4px 8px 0; + } +} + +.swagger-ui .black-05 { + color: rgba(191, 191, 191, .05); +} + +.swagger-ui .bg-black-05 { + background-color: rgba(0, 0, 0, .05); +} + +.swagger-ui .black-90, .swagger-ui .hover-black-90:focus, .swagger-ui .hover-black-90:hover { + color: rgba(191, 191, 191, .9); +} + +.swagger-ui .black-80, .swagger-ui .hover-black-80:focus, .swagger-ui .hover-black-80:hover { + color: rgba(191, 191, 191, .8); +} + +.swagger-ui .black-70, .swagger-ui .hover-black-70:focus, .swagger-ui .hover-black-70:hover { + color: rgba(191, 191, 191, .7); +} + +.swagger-ui .black-60, .swagger-ui .hover-black-60:focus, .swagger-ui .hover-black-60:hover { + color: rgba(191, 191, 191, .6); +} + +.swagger-ui .black-50, .swagger-ui .hover-black-50:focus, .swagger-ui .hover-black-50:hover { + color: rgba(191, 191, 191, .5); +} + +.swagger-ui .black-40, .swagger-ui .hover-black-40:focus, .swagger-ui .hover-black-40:hover { + color: rgba(191, 191, 191, .4); +} + +.swagger-ui .black-30, .swagger-ui .hover-black-30:focus, .swagger-ui .hover-black-30:hover { + color: rgba(191, 191, 191, .3); +} + +.swagger-ui .black-20, .swagger-ui .hover-black-20:focus, .swagger-ui .hover-black-20:hover { + color: rgba(191, 191, 191, .2); +} + +.swagger-ui .black-10, .swagger-ui .hover-black-10:focus, .swagger-ui .hover-black-10:hover { + color: rgba(191, 191, 191, .1); +} + +.swagger-ui .hover-white-90:focus, .swagger-ui .hover-white-90:hover, .swagger-ui .white-90 { + color: rgba(255, 255, 255, .9); +} + +.swagger-ui .hover-white-80:focus, .swagger-ui .hover-white-80:hover, .swagger-ui .white-80 { + color: rgba(255, 255, 255, .8); +} + +.swagger-ui .hover-white-70:focus, .swagger-ui .hover-white-70:hover, .swagger-ui .white-70 { + color: rgba(255, 255, 255, .7); +} + +.swagger-ui .hover-white-60:focus, .swagger-ui .hover-white-60:hover, .swagger-ui .white-60 { + color: rgba(255, 255, 255, .6); +} + +.swagger-ui .hover-white-50:focus, .swagger-ui .hover-white-50:hover, .swagger-ui .white-50 { + color: rgba(255, 255, 255, .5); +} + +.swagger-ui .hover-white-40:focus, .swagger-ui .hover-white-40:hover, .swagger-ui .white-40 { + color: rgba(255, 255, 255, .4); +} + +.swagger-ui .hover-white-30:focus, .swagger-ui .hover-white-30:hover, .swagger-ui .white-30 { + color: rgba(255, 255, 255, .3); +} + +.swagger-ui .hover-white-20:focus, .swagger-ui .hover-white-20:hover, .swagger-ui .white-20 { + color: rgba(255, 255, 255, .2); +} + +.swagger-ui .hover-white-10:focus, .swagger-ui .hover-white-10:hover, .swagger-ui .white-10 { + color: rgba(255, 255, 255, .1); +} + +.swagger-ui .hover-moon-gray:focus, .swagger-ui .hover-moon-gray:hover, .swagger-ui .moon-gray { + color: #ccc; +} + +.swagger-ui .hover-light-gray:focus, .swagger-ui .hover-light-gray:hover, .swagger-ui .light-gray { + color: #ededed; +} + +.swagger-ui .hover-near-white:focus, .swagger-ui .hover-near-white:hover, .swagger-ui .near-white { + color: #f5f5f5; +} + +.swagger-ui .dark-red, .swagger-ui .hover-dark-red:focus, .swagger-ui .hover-dark-red:hover { + color: #e6999d; +} + +.swagger-ui .hover-red:focus, .swagger-ui .hover-red:hover, .swagger-ui .red { + color: #e69d99; +} + +.swagger-ui .hover-light-red:focus, .swagger-ui .hover-light-red:hover, .swagger-ui .light-red { + color: #e6a399; +} + +.swagger-ui .hover-orange:focus, .swagger-ui .hover-orange:hover, .swagger-ui .orange { + color: #e6b699; +} + +.swagger-ui .gold, .swagger-ui .hover-gold:focus, .swagger-ui .hover-gold:hover { + color: #e6d099; +} + +.swagger-ui .hover-yellow:focus, .swagger-ui .hover-yellow:hover, .swagger-ui .yellow { + color: #e6da99; +} + +.swagger-ui .hover-light-yellow:focus, .swagger-ui .hover-light-yellow:hover, .swagger-ui .light-yellow { + color: #ede6b6; +} + +.swagger-ui .hover-purple:focus, .swagger-ui .hover-purple:hover, .swagger-ui .purple { + color: #b99ae4; +} + +.swagger-ui .hover-light-purple:focus, .swagger-ui .hover-light-purple:hover, .swagger-ui .light-purple { + color: #bb99e6; +} + +.swagger-ui .dark-pink, .swagger-ui .hover-dark-pink:focus, .swagger-ui .hover-dark-pink:hover { + color: #e699cc; +} + +.swagger-ui .hot-pink, .swagger-ui .hover-hot-pink:focus, .swagger-ui .hover-hot-pink:hover, .swagger-ui .hover-pink:focus, .swagger-ui .hover-pink:hover, .swagger-ui .pink { + color: #e699c7; +} + +.swagger-ui .hover-light-pink:focus, .swagger-ui .hover-light-pink:hover, .swagger-ui .light-pink { + color: #edb6d5; +} + +.swagger-ui .dark-green, .swagger-ui .green, .swagger-ui .hover-dark-green:focus, .swagger-ui .hover-dark-green:hover, .swagger-ui .hover-green:focus, .swagger-ui .hover-green:hover { + color: #99e6c9; +} + +.swagger-ui .hover-light-green:focus, .swagger-ui .hover-light-green:hover, .swagger-ui .light-green { + color: #a1e8ce; +} + +.swagger-ui .hover-navy:focus, .swagger-ui .hover-navy:hover, .swagger-ui .navy { + color: #99b8e6; +} + +.swagger-ui .blue, .swagger-ui .dark-blue, .swagger-ui .hover-blue:focus, .swagger-ui .hover-blue:hover, .swagger-ui .hover-dark-blue:focus, .swagger-ui .hover-dark-blue:hover { + color: #99bae6; +} + +.swagger-ui .hover-light-blue:focus, .swagger-ui .hover-light-blue:hover, .swagger-ui .light-blue { + color: #a9cbea; +} + +.swagger-ui .hover-lightest-blue:focus, .swagger-ui .hover-lightest-blue:hover, .swagger-ui .lightest-blue { + color: #d6e9f5; +} + +.swagger-ui .hover-washed-blue:focus, .swagger-ui .hover-washed-blue:hover, .swagger-ui .washed-blue { + color: #f7fdfc; +} + +.swagger-ui .hover-washed-green:focus, .swagger-ui .hover-washed-green:hover, .swagger-ui .washed-green { + color: #ebfaf4; +} + +.swagger-ui .hover-washed-yellow:focus, .swagger-ui .hover-washed-yellow:hover, .swagger-ui .washed-yellow { + color: #fbf9ef; +} + +.swagger-ui .hover-washed-red:focus, .swagger-ui .hover-washed-red:hover, .swagger-ui .washed-red { + color: #f9e7e7; +} + +.swagger-ui .color-inherit, .swagger-ui .hover-inherit:focus, .swagger-ui .hover-inherit:hover { + color: inherit; +} + +.swagger-ui .bg-black-90, .swagger-ui .hover-bg-black-90:focus, .swagger-ui .hover-bg-black-90:hover { + background-color: rgba(0, 0, 0, .9); +} + +.swagger-ui .bg-black-80, .swagger-ui .hover-bg-black-80:focus, .swagger-ui .hover-bg-black-80:hover { + background-color: rgba(0, 0, 0, .8); +} + +.swagger-ui .bg-black-70, .swagger-ui .hover-bg-black-70:focus, .swagger-ui .hover-bg-black-70:hover { + background-color: rgba(0, 0, 0, .7); +} + +.swagger-ui .bg-black-60, .swagger-ui .hover-bg-black-60:focus, .swagger-ui .hover-bg-black-60:hover { + background-color: rgba(0, 0, 0, .6); +} + +.swagger-ui .bg-black-50, .swagger-ui .hover-bg-black-50:focus, .swagger-ui .hover-bg-black-50:hover { + background-color: rgba(0, 0, 0, .5); +} + +.swagger-ui .bg-black-40, .swagger-ui .hover-bg-black-40:focus, .swagger-ui .hover-bg-black-40:hover { + background-color: rgba(0, 0, 0, .4); +} + +.swagger-ui .bg-black-30, .swagger-ui .hover-bg-black-30:focus, .swagger-ui .hover-bg-black-30:hover { + background-color: rgba(0, 0, 0, .3); +} + +.swagger-ui .bg-black-20, .swagger-ui .hover-bg-black-20:focus, .swagger-ui .hover-bg-black-20:hover { + background-color: rgba(0, 0, 0, .2); +} + +.swagger-ui .bg-white-90, .swagger-ui .hover-bg-white-90:focus, .swagger-ui .hover-bg-white-90:hover { + background-color: rgba(28, 28, 33, .9); +} + +.swagger-ui .bg-white-80, .swagger-ui .hover-bg-white-80:focus, .swagger-ui .hover-bg-white-80:hover { + background-color: rgba(28, 28, 33, .8); +} + +.swagger-ui .bg-white-70, .swagger-ui .hover-bg-white-70:focus, .swagger-ui .hover-bg-white-70:hover { + background-color: rgba(28, 28, 33, .7); +} + +.swagger-ui .bg-white-60, .swagger-ui .hover-bg-white-60:focus, .swagger-ui .hover-bg-white-60:hover { + background-color: rgba(28, 28, 33, .6); +} + +.swagger-ui .bg-white-50, .swagger-ui .hover-bg-white-50:focus, .swagger-ui .hover-bg-white-50:hover { + background-color: rgba(28, 28, 33, .5); +} + +.swagger-ui .bg-white-40, .swagger-ui .hover-bg-white-40:focus, .swagger-ui .hover-bg-white-40:hover { + background-color: rgba(28, 28, 33, .4); +} + +.swagger-ui .bg-white-30, .swagger-ui .hover-bg-white-30:focus, .swagger-ui .hover-bg-white-30:hover { + background-color: rgba(28, 28, 33, .3); +} + +.swagger-ui .bg-white-20, .swagger-ui .hover-bg-white-20:focus, .swagger-ui .hover-bg-white-20:hover { + background-color: rgba(28, 28, 33, .2); +} + +.swagger-ui .bg-black, .swagger-ui .hover-bg-black:focus, .swagger-ui .hover-bg-black:hover { + background-color: #000; +} + +.swagger-ui .bg-near-black, .swagger-ui .hover-bg-near-black:focus, .swagger-ui .hover-bg-near-black:hover { + background-color: #121212; +} + +.swagger-ui .bg-dark-gray, .swagger-ui .hover-bg-dark-gray:focus, .swagger-ui .hover-bg-dark-gray:hover { + background-color: #333; +} + +.swagger-ui .bg-mid-gray, .swagger-ui .hover-bg-mid-gray:focus, .swagger-ui .hover-bg-mid-gray:hover { + background-color: #545454; +} + +.swagger-ui .bg-gray, .swagger-ui .hover-bg-gray:focus, .swagger-ui .hover-bg-gray:hover { + background-color: #787878; +} + +.swagger-ui .bg-silver, .swagger-ui .hover-bg-silver:focus, .swagger-ui .hover-bg-silver:hover { + background-color: #999; +} + +.swagger-ui .bg-white, .swagger-ui .hover-bg-white:focus, .swagger-ui .hover-bg-white:hover { + background-color: #1c1c21; +} + +.swagger-ui .bg-transparent, .swagger-ui .hover-bg-transparent:focus, .swagger-ui .hover-bg-transparent:hover { + background-color: transparent; +} + +.swagger-ui .bg-dark-red, .swagger-ui .hover-bg-dark-red:focus, .swagger-ui .hover-bg-dark-red:hover { + background-color: #bc2f36; +} + +.swagger-ui .bg-red, .swagger-ui .hover-bg-red:focus, .swagger-ui .hover-bg-red:hover { + background-color: #c83932; +} + +.swagger-ui .bg-light-red, .swagger-ui .hover-bg-light-red:focus, .swagger-ui .hover-bg-light-red:hover { + background-color: #ab3c2b; +} + +.swagger-ui .bg-orange, .swagger-ui .hover-bg-orange:focus, .swagger-ui .hover-bg-orange:hover { + background-color: #cc6e33; +} + +.swagger-ui .bg-gold, .swagger-ui .bg-light-yellow, .swagger-ui .bg-washed-yellow, .swagger-ui .bg-yellow, .swagger-ui .hover-bg-gold:focus, .swagger-ui .hover-bg-gold:hover, .swagger-ui .hover-bg-light-yellow:focus, .swagger-ui .hover-bg-light-yellow:hover, .swagger-ui .hover-bg-washed-yellow:focus, .swagger-ui .hover-bg-washed-yellow:hover, .swagger-ui .hover-bg-yellow:focus, .swagger-ui .hover-bg-yellow:hover { + background-color: #664b00; +} + +.swagger-ui .bg-purple, .swagger-ui .hover-bg-purple:focus, .swagger-ui .hover-bg-purple:hover { + background-color: #5e2ca5; +} + +.swagger-ui .bg-light-purple, .swagger-ui .hover-bg-light-purple:focus, .swagger-ui .hover-bg-light-purple:hover { + background-color: #672caf; +} + +.swagger-ui .bg-dark-pink, .swagger-ui .hover-bg-dark-pink:focus, .swagger-ui .hover-bg-dark-pink:hover { + background-color: #ab2b81; +} + +.swagger-ui .bg-hot-pink, .swagger-ui .hover-bg-hot-pink:focus, .swagger-ui .hover-bg-hot-pink:hover { + background-color: #c03086; +} + +.swagger-ui .bg-pink, .swagger-ui .hover-bg-pink:focus, .swagger-ui .hover-bg-pink:hover { + background-color: #8f2464; +} + +.swagger-ui .bg-light-pink, .swagger-ui .hover-bg-light-pink:focus, .swagger-ui .hover-bg-light-pink:hover { + background-color: #721d4d; +} + +.swagger-ui .bg-dark-green, .swagger-ui .hover-bg-dark-green:focus, .swagger-ui .hover-bg-dark-green:hover { + background-color: #1c6e50; +} + +.swagger-ui .bg-green, .swagger-ui .hover-bg-green:focus, .swagger-ui .hover-bg-green:hover { + background-color: #279b70; +} + +.swagger-ui .bg-light-green, .swagger-ui .hover-bg-light-green:focus, .swagger-ui .hover-bg-light-green:hover { + background-color: #228762; +} + +.swagger-ui .bg-navy, .swagger-ui .hover-bg-navy:focus, .swagger-ui .hover-bg-navy:hover { + background-color: #0d1d35; +} + +.swagger-ui .bg-dark-blue, .swagger-ui .hover-bg-dark-blue:focus, .swagger-ui .hover-bg-dark-blue:hover { + background-color: #20497e; +} + +.swagger-ui .bg-blue, .swagger-ui .hover-bg-blue:focus, .swagger-ui .hover-bg-blue:hover { + background-color: #4380d0; +} + +.swagger-ui .bg-light-blue, .swagger-ui .hover-bg-light-blue:focus, .swagger-ui .hover-bg-light-blue:hover { + background-color: #20517e; +} + +.swagger-ui .bg-lightest-blue, .swagger-ui .hover-bg-lightest-blue:focus, .swagger-ui .hover-bg-lightest-blue:hover { + background-color: #143a52; +} + +.swagger-ui .bg-washed-blue, .swagger-ui .hover-bg-washed-blue:focus, .swagger-ui .hover-bg-washed-blue:hover { + background-color: #0c312d; +} + +.swagger-ui .bg-washed-green, .swagger-ui .hover-bg-washed-green:focus, .swagger-ui .hover-bg-washed-green:hover { + background-color: #0f3d2c; +} + +.swagger-ui .bg-washed-red, .swagger-ui .hover-bg-washed-red:focus, .swagger-ui .hover-bg-washed-red:hover { + background-color: #411010; +} + +.swagger-ui .bg-inherit, .swagger-ui .hover-bg-inherit:focus, .swagger-ui .hover-bg-inherit:hover { + background-color: inherit; +} + +.swagger-ui .shadow-hover { + transition: all .5s cubic-bezier(.165, .84, .44, 1) 0s; +} + + .swagger-ui .shadow-hover::after { + border-radius: inherit; + box-shadow: rgba(0, 0, 0, .2) 0 0 16px 2px; + content: ""; + height: 100%; + left: 0; + opacity: 0; + position: absolute; + top: 0; + transition: opacity .5s cubic-bezier(.165, .84, .44, 1) 0s; + width: 100%; + z-index: -1; + } + +.swagger-ui .bg-animate, .swagger-ui .bg-animate:focus, .swagger-ui .bg-animate:hover { + transition: background-color .15s ease-in-out 0s; +} + +.swagger-ui .nested-links a { + color: #99bae6; + transition: color .15s ease-in 0s; +} + + .swagger-ui .nested-links a:focus, .swagger-ui .nested-links a:hover { + color: #a9cbea; + transition: color .15s ease-in 0s; + } + +.swagger-ui .opblock-tag { + border-bottom: 1px solid rgba(58, 64, 80, .3); + color: #b5bac9; + transition: all .2s ease 0s; +} + + .swagger-ui .opblock-tag svg, .swagger-ui section.models h4 svg { + transition: all .4s ease 0s; + } + +.swagger-ui .opblock { + border: 1px solid #000; + border-radius: 4px; + box-shadow: rgba(0, 0, 0, .19) 0 0 3px; + margin: 0 0 15px; +} + + .swagger-ui .opblock .tab-header .tab-item.active h4 span::after { + background: gray; + } + + .swagger-ui .opblock.is-open .opblock-summary { + border-bottom: 1px solid #000; + } + + .swagger-ui .opblock .opblock-section-header { + background: rgba(28, 28, 33, .8); + box-shadow: rgba(0, 0, 0, .1) 0 1px 2px; + } + + .swagger-ui .opblock .opblock-section-header > label > span { + padding: 0 10px 0 0; + } + + .swagger-ui .opblock .opblock-summary-method { + background: #000; + color: #fff; + text-shadow: rgba(0, 0, 0, .1) 0 1px 0; + } + + .swagger-ui .opblock.opblock-post { + background: rgba(72, 203, 144, .1); + border-color: #48cb90; + } + + .swagger-ui .opblock.opblock-post .opblock-summary-method, .swagger-ui .opblock.opblock-post .tab-header .tab-item.active h4 span::after { + background: #48cb90; + } + + .swagger-ui .opblock.opblock-post .opblock-summary { + border-color: #48cb90; + } + + .swagger-ui .opblock.opblock-put { + background: rgba(213, 157, 88, .1); + border-color: #d59d58; + } + + .swagger-ui .opblock.opblock-put .opblock-summary-method, .swagger-ui .opblock.opblock-put .tab-header .tab-item.active h4 span::after { + background: #d59d58; + } + + .swagger-ui .opblock.opblock-put .opblock-summary { + border-color: #d59d58; + } + + .swagger-ui .opblock.opblock-delete { + background: rgba(200, 50, 50, .1); + border-color: #c83232; + } + + .swagger-ui .opblock.opblock-delete .opblock-summary-method, .swagger-ui .opblock.opblock-delete .tab-header .tab-item.active h4 span::after { + background: #c83232; + } + + .swagger-ui .opblock.opblock-delete .opblock-summary { + border-color: #c83232; + } + + .swagger-ui .opblock.opblock-get { + background: rgba(42, 105, 167, .1); + border-color: #2a69a7; + } + + .swagger-ui .opblock.opblock-get .opblock-summary-method, .swagger-ui .opblock.opblock-get .tab-header .tab-item.active h4 span::after { + background: #2a69a7; + } + + .swagger-ui .opblock.opblock-get .opblock-summary { + border-color: #2a69a7; + } + + .swagger-ui .opblock.opblock-patch { + background: rgba(92, 214, 188, .1); + border-color: #5cd6bc; + } + + .swagger-ui .opblock.opblock-patch .opblock-summary-method, .swagger-ui .opblock.opblock-patch .tab-header .tab-item.active h4 span::after { + background: #5cd6bc; + } + + .swagger-ui .opblock.opblock-patch .opblock-summary { + border-color: #5cd6bc; + } + + .swagger-ui .opblock.opblock-head { + background: rgba(140, 63, 207, .1); + border-color: #8c3fcf; + } + + .swagger-ui .opblock.opblock-head .opblock-summary-method, .swagger-ui .opblock.opblock-head .tab-header .tab-item.active h4 span::after { + background: #8c3fcf; + } + + .swagger-ui .opblock.opblock-head .opblock-summary { + border-color: #8c3fcf; + } + + .swagger-ui .opblock.opblock-options { + background: rgba(36, 89, 143, .1); + border-color: #24598f; + } + + .swagger-ui .opblock.opblock-options .opblock-summary-method, .swagger-ui .opblock.opblock-options .tab-header .tab-item.active h4 span::after { + background: #24598f; + } + + .swagger-ui .opblock.opblock-options .opblock-summary { + border-color: #24598f; + } + + .swagger-ui .opblock.opblock-deprecated { + background: rgba(46, 46, 46, .1); + border-color: #2e2e2e; + opacity: .6; + } + + .swagger-ui .opblock.opblock-deprecated .opblock-summary-method, .swagger-ui .opblock.opblock-deprecated .tab-header .tab-item.active h4 span::after { + background: #2e2e2e; + } + + .swagger-ui .opblock.opblock-deprecated .opblock-summary { + border-color: #2e2e2e; + } + +.swagger-ui .filter .operation-filter-input { + border: 2px solid #2b3446; +} + +.swagger-ui .tab li:first-of-type::after { + background: rgba(0, 0, 0, .2); +} + +.swagger-ui .download-contents { + background: #7c8192; + color: #fff; +} + +.swagger-ui .scheme-container { + background: #1c1c21; + box-shadow: rgba(0, 0, 0, .15) 0 1px 2px 0; +} + +.swagger-ui .loading-container .loading::before { + animation: 1s linear 0s infinite normal none running rotation, .5s ease 0s 1 normal none running opacity; + border-color: rgba(0, 0, 0, .6) rgba(84, 84, 84, .1) rgba(84, 84, 84, .1); +} + +.swagger-ui .response-control-media-type--accept-controller select { + border-color: #196619; +} + +.swagger-ui .response-control-media-type__accept-message { + color: #99e699; +} + +.swagger-ui .version-pragma__message code { + background-color: #3b3b3b; +} + +.swagger-ui .btn { + background: 0 0; + border: 2px solid gray; + box-shadow: rgba(0, 0, 0, .1) 0 1px 2px; + color: #b5bac9; +} + + .swagger-ui .btn:hover { + box-shadow: rgba(0, 0, 0, .3) 0 0 5px; + } + + .swagger-ui .btn.authorize, .swagger-ui .btn.cancel { + background-color: transparent; + border-color: #a72a2a; + color: #e69999; + } + + .swagger-ui .btn.authorize { + border-color: #48cb90; + color: #9ce3c3; + } + + .swagger-ui .btn.authorize svg { + fill: #9ce3c3; + } + + .swagger-ui .btn.execute { + background-color: #5892d5; + border-color: #5892d5; + color: #fff; + } + +.swagger-ui .copy-to-clipboard { + background: #7c8192; +} + + .swagger-ui .copy-to-clipboard button { + background: url("data:image/svg+xml;charset=utf-8,") 50% center no-repeat; + } + +.swagger-ui select { + background: url("data:image/svg+xml;charset=utf-8,") right 10px center/20px no-repeat #212121; + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wICg0MDM1YTRmYjQ5LCAyMDIwLTA1LTAxKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZG93bmxvYWQuc3ZnIgogICBpZD0ic3ZnNCIKICAgdmVyc2lvbj0iMS4xIgogICB2aWV3Qm94PSIwIDAgMjAgMjAiPgogIDxtZXRhZGF0YQogICAgIGlkPSJtZXRhZGF0YTEwIj4KICAgIDxyZGY6UkRGPgogICAgICA8Y2M6V29yawogICAgICAgICByZGY6YWJvdXQ9IiI+CiAgICAgICAgPGRjOmZvcm1hdD5pbWFnZS9zdmcreG1sPC9kYzpmb3JtYXQ+CiAgICAgICAgPGRjOnR5cGUKICAgICAgICAgICByZGY6cmVzb3VyY2U9Imh0dHA6Ly9wdXJsLm9yZy9kYy9kY21pdHlwZS9TdGlsbEltYWdlIiAvPgogICAgICA8L2NjOldvcms+CiAgICA8L3JkZjpSREY+CiAgPC9tZXRhZGF0YT4KICA8ZGVmcwogICAgIGlkPSJkZWZzOCIgLz4KICA8c29kaXBvZGk6bmFtZWR2aWV3CiAgICAgaW5rc2NhcGU6Y3VycmVudC1sYXllcj0ic3ZnNCIKICAgICBpbmtzY2FwZTp3aW5kb3ctbWF4aW1pemVkPSIxIgogICAgIGlua3NjYXBlOndpbmRvdy15PSItOSIKICAgICBpbmtzY2FwZTp3aW5kb3cteD0iLTkiCiAgICAgaW5rc2NhcGU6Y3k9IjEwIgogICAgIGlua3NjYXBlOmN4PSIxMCIKICAgICBpbmtzY2FwZTp6b29tPSI0MS41IgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDAxIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIKICAgICBpZD0icGF0aDIiCiAgICAgZD0iTTEzLjQxOCA3Ljg1OWEuNjk1LjY5NSAwIDAxLjk3OCAwIC42OC42OCAwIDAxMCAuOTY5bC0zLjkwOCAzLjgzYS42OTcuNjk3IDAgMDEtLjk3OSAwbC0zLjkwOC0zLjgzYS42OC42OCAwIDAxMC0uOTY5LjY5NS42OTUgMCAwMS45NzggMEwxMCAxMWwzLjQxOC0zLjE0MXoiIC8+Cjwvc3ZnPgo=) right 10px center/20px no-repeat #1c1c21; + border: 2px solid #41444e; +} + + .swagger-ui select[multiple] { + background: #212121; + } + + .swagger-ui button.invalid, .swagger-ui input[type=email].invalid, .swagger-ui input[type=file].invalid, .swagger-ui input[type=password].invalid, .swagger-ui input[type=search].invalid, .swagger-ui input[type=text].invalid, .swagger-ui select.invalid, .swagger-ui textarea.invalid { + background: #390e0e; + border-color: #c83232; + } + +.swagger-ui input[type=email], .swagger-ui input[type=file], .swagger-ui input[type=password], .swagger-ui input[type=search], .swagger-ui input[type=text], .swagger-ui textarea { + background: #1c1c21; + border: 1px solid #404040; +} + +.swagger-ui textarea { + background: rgba(28, 28, 33, .8); + color: #b5bac9; +} + +.swagger-ui input[disabled], .swagger-ui select[disabled] { + background-color: #1f1f1f; + color: #bfbfbf; +} + +.swagger-ui textarea[disabled] { + background-color: #41444e; + color: #fff; +} + +.swagger-ui select[disabled] { + border-color: #878787; +} + +.swagger-ui textarea:focus { + border: 2px solid #2a69a7; +} + +.swagger-ui .checkbox input[type=checkbox] + label > .item { + background: #303030; + box-shadow: #303030 0 0 0 2px; +} + +.swagger-ui .checkbox input[type=checkbox]:checked + label > .item { + background: url("data:image/svg+xml;charset=utf-8,") 50% center no-repeat #303030; +} + +.swagger-ui .dialog-ux .backdrop-ux { + background: rgba(0, 0, 0, .8); +} + +.swagger-ui .dialog-ux .modal-ux { + background: #1c1c21; + border: 1px solid #2e2e2e; + box-shadow: rgba(0, 0, 0, .2) 0 10px 30px 0; +} + +.swagger-ui .dialog-ux .modal-ux-header .close-modal { + background: 0 0; +} + +.swagger-ui .model .deprecated span, .swagger-ui .model .deprecated td { + color: #bfbfbf !important; +} + +.swagger-ui .model-toggle::after { + background: url("data:image/svg+xml;charset=utf-8,") 50% center/100% no-repeat; +} + +.swagger-ui .model-hint { + background: rgba(0, 0, 0, .7); + color: #ebebeb; +} + +.swagger-ui section.models { + border: 1px solid rgba(58, 64, 80, .3); +} + + .swagger-ui section.models.is-open h4 { + border-bottom: 1px solid rgba(58, 64, 80, .3); + } + + .swagger-ui section.models .model-container { + background: rgba(0, 0, 0, .05); + } + + .swagger-ui section.models .model-container:hover { + background: rgba(0, 0, 0, .07); + } + +.swagger-ui .model-box { + background: rgba(0, 0, 0, .1); +} + +.swagger-ui .prop-type { + color: #aaaad4; +} + +.swagger-ui table thead tr td, .swagger-ui table thead tr th { + border-bottom: 1px solid rgba(58, 64, 80, .2); + color: #b5bac9; +} + +.swagger-ui .parameter__name.required::after { + color: rgba(230, 153, 153, .6); +} + +.swagger-ui .topbar .download-url-wrapper .select-label { + color: #f0f0f0; +} + +.swagger-ui .topbar .download-url-wrapper .download-url-button { + background: #63a040; + color: #fff; +} + +.swagger-ui .info .title small { + background: #7c8492; +} + + .swagger-ui .info .title small.version-stamp { + background-color: #7a9b27; + } + +.swagger-ui .auth-container .errors { + background-color: #350d0d; + color: #b5bac9; +} + +.swagger-ui .errors-wrapper { + background: rgba(200, 50, 50, .1); + border: 2px solid #c83232; +} + +.swagger-ui .markdown code, .swagger-ui .renderedmarkdown code { + background: rgba(0, 0, 0, .05); + color: #c299e6; +} + +.swagger-ui .model-toggle:after { + background: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjxzdmcKICAgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIgogICB4bWxuczpjYz0iaHR0cDovL2NyZWF0aXZlY29tbW9ucy5vcmcvbnMjIgogICB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiCiAgIHhtbG5zOnN2Zz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciCiAgIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIKICAgeG1sbnM6c29kaXBvZGk9Imh0dHA6Ly9zb2RpcG9kaS5zb3VyY2Vmb3JnZS5uZXQvRFREL3NvZGlwb2RpLTAuZHRkIgogICB4bWxuczppbmtzY2FwZT0iaHR0cDovL3d3dy5pbmtzY2FwZS5vcmcvbmFtZXNwYWNlcy9pbmtzY2FwZSIKICAgaW5rc2NhcGU6dmVyc2lvbj0iMS4wICg0MDM1YTRmYjQ5LCAyMDIwLTA1LTAxKSIKICAgc29kaXBvZGk6ZG9jbmFtZT0iZG93bmxvYWQyLnN2ZyIKICAgaWQ9InN2ZzQiCiAgIHZlcnNpb249IjEuMSIKICAgaGVpZ2h0PSIyNCIKICAgd2lkdGg9IjI0Ij4KICA8bWV0YWRhdGEKICAgICBpZD0ibWV0YWRhdGExMCI+CiAgICA8cmRmOlJERj4KICAgICAgPGNjOldvcmsKICAgICAgICAgcmRmOmFib3V0PSIiPgogICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2Uvc3ZnK3htbDwvZGM6Zm9ybWF0PgogICAgICAgIDxkYzp0eXBlCiAgICAgICAgICAgcmRmOnJlc291cmNlPSJodHRwOi8vcHVybC5vcmcvZGMvZGNtaXR5cGUvU3RpbGxJbWFnZSIgLz4KICAgICAgPC9jYzpXb3JrPgogICAgPC9yZGY6UkRGPgogIDwvbWV0YWRhdGE+CiAgPGRlZnMKICAgICBpZD0iZGVmczgiIC8+CiAgPHNvZGlwb2RpOm5hbWVkdmlldwogICAgIGlua3NjYXBlOmN1cnJlbnQtbGF5ZXI9InN2ZzQiCiAgICAgaW5rc2NhcGU6d2luZG93LW1heGltaXplZD0iMSIKICAgICBpbmtzY2FwZTp3aW5kb3cteT0iLTkiCiAgICAgaW5rc2NhcGU6d2luZG93LXg9Ii05IgogICAgIGlua3NjYXBlOmN5PSIxMiIKICAgICBpbmtzY2FwZTpjeD0iMTIiCiAgICAgaW5rc2NhcGU6em9vbT0iMzQuNTgzMzMzIgogICAgIHNob3dncmlkPSJmYWxzZSIKICAgICBpZD0ibmFtZWR2aWV3NiIKICAgICBpbmtzY2FwZTp3aW5kb3ctaGVpZ2h0PSIxMDAxIgogICAgIGlua3NjYXBlOndpbmRvdy13aWR0aD0iMTkyMCIKICAgICBpbmtzY2FwZTpwYWdlc2hhZG93PSIyIgogICAgIGlua3NjYXBlOnBhZ2VvcGFjaXR5PSIwIgogICAgIGd1aWRldG9sZXJhbmNlPSIxMCIKICAgICBncmlkdG9sZXJhbmNlPSIxMCIKICAgICBvYmplY3R0b2xlcmFuY2U9IjEwIgogICAgIGJvcmRlcm9wYWNpdHk9IjEiCiAgICAgYm9yZGVyY29sb3I9IiM2NjY2NjYiCiAgICAgcGFnZWNvbG9yPSIjZmZmZmZmIiAvPgogIDxwYXRoCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZiIKICAgICBpZD0icGF0aDIiCiAgICAgZD0iTTEwIDZMOC41OSA3LjQxIDEzLjE3IDEybC00LjU4IDQuNTlMMTAgMThsNi02eiIgLz4KPC9zdmc+Cg==) 50% no-repeat; +} + +.swagger-ui .expand-operation svg, .swagger-ui section.models h4 svg { + fill: #fff; +} + +::-webkit-scrollbar-track { + background-color: #646464 !important; +} + +::-webkit-scrollbar-thumb { + background-color: #242424 !important; + border: 2px solid #3e4346 !important; +} + +::-webkit-scrollbar-button:vertical:start:decrement { + background: linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%), linear-gradient(230deg, #696969 40%, transparent 41%), linear-gradient(0deg, #696969 40%, transparent 31%); + background-color: #b6b6b6; +} + +::-webkit-scrollbar-button:vertical:end:increment { + background: linear-gradient(310deg, #696969 40%, transparent 41%), linear-gradient(50deg, #696969 40%, transparent 41%), linear-gradient(180deg, #696969 40%, transparent 31%); + background-color: #b6b6b6; +} + +::-webkit-scrollbar-button:horizontal:end:increment { + background: linear-gradient(210deg, #696969 40%, transparent 41%), linear-gradient(330deg, #696969 40%, transparent 41%), linear-gradient(90deg, #696969 30%, transparent 31%); + background-color: #b6b6b6; +} + +::-webkit-scrollbar-button:horizontal:start:decrement { + background: linear-gradient(30deg, #696969 40%, transparent 41%), linear-gradient(150deg, #696969 40%, transparent 41%), linear-gradient(270deg, #696969 30%, transparent 31%); + background-color: #b6b6b6; +} + +::-webkit-scrollbar-button, ::-webkit-scrollbar-track-piece { + background-color: #3e4346 !important; +} + +.swagger-ui .black, .swagger-ui .checkbox, .swagger-ui .dark-gray, .swagger-ui .download-url-wrapper .loading, .swagger-ui .errors-wrapper .errors small, .swagger-ui .fallback, .swagger-ui .filter .loading, .swagger-ui .gray, .swagger-ui .hover-black:focus, .swagger-ui .hover-black:hover, .swagger-ui .hover-dark-gray:focus, .swagger-ui .hover-dark-gray:hover, .swagger-ui .hover-gray:focus, .swagger-ui .hover-gray:hover, .swagger-ui .hover-light-silver:focus, .swagger-ui .hover-light-silver:hover, .swagger-ui .hover-mid-gray:focus, .swagger-ui .hover-mid-gray:hover, .swagger-ui .hover-near-black:focus, .swagger-ui .hover-near-black:hover, .swagger-ui .hover-silver:focus, .swagger-ui .hover-silver:hover, .swagger-ui .light-silver, .swagger-ui .markdown pre, .swagger-ui .mid-gray, .swagger-ui .model .property, .swagger-ui .model .property.primitive, .swagger-ui .model-title, .swagger-ui .near-black, .swagger-ui .parameter__extension, .swagger-ui .parameter__in, .swagger-ui .prop-format, .swagger-ui .renderedmarkdown pre, .swagger-ui .response-col_links .response-undocumented, .swagger-ui .response-col_status .response-undocumented, .swagger-ui .silver, .swagger-ui section.models h4, .swagger-ui section.models h5, .swagger-ui span.token-not-formatted, .swagger-ui span.token-string, .swagger-ui table.headers .header-example, .swagger-ui table.model tr.description, .swagger-ui table.model tr.extension { + color: #bfbfbf; +} + +.swagger-ui .hover-white:focus, .swagger-ui .hover-white:hover, .swagger-ui .info .title small pre, .swagger-ui .topbar a, .swagger-ui .white { + color: #fff; +} + +.swagger-ui .bg-black-10, .swagger-ui .hover-bg-black-10:focus, .swagger-ui .hover-bg-black-10:hover, .swagger-ui .stripe-dark:nth-child(2n + 1) { + background-color: rgba(0, 0, 0, .1); +} + +.swagger-ui .bg-white-10, .swagger-ui .hover-bg-white-10:focus, .swagger-ui .hover-bg-white-10:hover, .swagger-ui .stripe-light:nth-child(2n + 1) { + background-color: rgba(28, 28, 33, .1); +} + +.swagger-ui .bg-light-silver, .swagger-ui .hover-bg-light-silver:focus, .swagger-ui .hover-bg-light-silver:hover, .swagger-ui .striped--light-silver:nth-child(2n + 1) { + background-color: #6e6e6e; +} + +.swagger-ui .bg-moon-gray, .swagger-ui .hover-bg-moon-gray:focus, .swagger-ui .hover-bg-moon-gray:hover, .swagger-ui .striped--moon-gray:nth-child(2n + 1) { + background-color: #4d4d4d; +} + +.swagger-ui .bg-light-gray, .swagger-ui .hover-bg-light-gray:focus, .swagger-ui .hover-bg-light-gray:hover, .swagger-ui .striped--light-gray:nth-child(2n + 1) { + background-color: #2b2b2b; +} + +.swagger-ui .bg-near-white, .swagger-ui .hover-bg-near-white:focus, .swagger-ui .hover-bg-near-white:hover, .swagger-ui .striped--near-white:nth-child(2n + 1) { + background-color: #242424; +} + +.swagger-ui .opblock-tag:hover, .swagger-ui section.models h4:hover { + background: rgba(0, 0, 0, .02); +} + +.swagger-ui .checkbox p, .swagger-ui .dialog-ux .modal-ux-content h4, .swagger-ui .dialog-ux .modal-ux-content p, .swagger-ui .dialog-ux .modal-ux-header h3, .swagger-ui .errors-wrapper .errors h4, .swagger-ui .errors-wrapper hgroup h4, .swagger-ui .info .base-url, .swagger-ui .info .title, .swagger-ui .info h1, .swagger-ui .info h2, .swagger-ui .info h3, .swagger-ui .info h4, .swagger-ui .info h5, .swagger-ui .info li, .swagger-ui .info p, .swagger-ui .info table, .swagger-ui .loading-container .loading::after, .swagger-ui .model, .swagger-ui .opblock .opblock-section-header h4, .swagger-ui .opblock .opblock-section-header > label, .swagger-ui .opblock .opblock-summary-description, .swagger-ui .opblock .opblock-summary-operation-id, .swagger-ui .opblock .opblock-summary-path, .swagger-ui .opblock .opblock-summary-path__deprecated, .swagger-ui .opblock-description-wrapper, .swagger-ui .opblock-description-wrapper h4, .swagger-ui .opblock-description-wrapper p, .swagger-ui .opblock-external-docs-wrapper, .swagger-ui .opblock-external-docs-wrapper h4, .swagger-ui .opblock-external-docs-wrapper p, .swagger-ui .opblock-tag small, .swagger-ui .opblock-title_normal, .swagger-ui .opblock-title_normal h4, .swagger-ui .opblock-title_normal p, .swagger-ui .parameter__name, .swagger-ui .parameter__type, .swagger-ui .response-col_links, .swagger-ui .response-col_status, .swagger-ui .responses-inner h4, .swagger-ui .responses-inner h5, .swagger-ui .scheme-container .schemes > label, .swagger-ui .scopes h2, .swagger-ui .servers > label, .swagger-ui .tab li, .swagger-ui label, .swagger-ui select, .swagger-ui table.headers td { + color: #b5bac9; +} + + .swagger-ui .download-url-wrapper .failed, .swagger-ui .filter .failed, .swagger-ui .model-deprecated-warning, .swagger-ui .parameter__deprecated, .swagger-ui .parameter__name.required span, .swagger-ui table.model tr.property-row .star { + color: #e69999; + } + +.swagger-ui .opblock-body pre.microlight, .swagger-ui textarea.curl { + background: #41444e; + border-radius: 4px; + color: #fff; +} + +.swagger-ui .expand-methods svg, .swagger-ui .expand-methods:hover svg { + fill: #bfbfbf; +} + +.swagger-ui .auth-container, .swagger-ui .dialog-ux .modal-ux-header { + border-bottom: 1px solid #2e2e2e; +} + +.swagger-ui .topbar .download-url-wrapper .select-label select, .swagger-ui .topbar .download-url-wrapper input[type=text] { + border: 2px solid #63a040; +} + +.swagger-ui .info a, .swagger-ui .info a:hover, .swagger-ui .scopes h2 a { + color: #99bde6; +} + +/* Dark Scrollbar */ +::-webkit-scrollbar { + width: 14px; + height: 14px; +} + +::-webkit-scrollbar-button { + background-color: #3e4346 !important; +} + +::-webkit-scrollbar-track { + background-color: #646464 !important; +} + +::-webkit-scrollbar-track-piece { + background-color: #3e4346 !important; +} + +::-webkit-scrollbar-thumb { + height: 50px; + background-color: #242424 !important; + border: 2px solid #3e4346 !important; +} + +::-webkit-scrollbar-corner { +} + +::-webkit-resizer { +} + +::-webkit-scrollbar-button:vertical:start:decrement { + background: linear-gradient(130deg, #696969 40%, rgba(255, 0, 0, 0) 41%), linear-gradient(230deg, #696969 40%, rgba(0, 0, 0, 0) 41%), linear-gradient(0deg, #696969 40%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; +} + +::-webkit-scrollbar-button:vertical:end:increment { + background: linear-gradient(310deg, #696969 40%, rgba(0, 0, 0, 0) 41%), linear-gradient(50deg, #696969 40%, rgba(0, 0, 0, 0) 41%), linear-gradient(180deg, #696969 40%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; +} + +::-webkit-scrollbar-button:horizontal:end:increment { + background: linear-gradient(210deg, #696969 40%, rgba(0, 0, 0, 0) 41%), linear-gradient(330deg, #696969 40%, rgba(0, 0, 0, 0) 41%), linear-gradient(90deg, #696969 30%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; +} + +::-webkit-scrollbar-button:horizontal:start:decrement { + background: linear-gradient(30deg, #696969 40%, rgba(0, 0, 0, 0) 41%), linear-gradient(150deg, #696969 40%, rgba(0, 0, 0, 0) 41%), linear-gradient(270deg, #696969 30%, rgba(0, 0, 0, 0) 31%); + background-color: #b6b6b6; +} diff --git a/src/TCDev.APIGenerator/Controller/GenericController.cs b/src/TCDev.APIGenerator/Controller/GenericController.cs index 37994c2..e22a9a3 100644 --- a/src/TCDev.APIGenerator/Controller/GenericController.cs +++ b/src/TCDev.APIGenerator/Controller/GenericController.cs @@ -123,7 +123,7 @@ public async Task Create([FromBody] T record) } catch (Exception ex) { - return BadRequest(ex); + return BadRequest(ex.Message); } } diff --git a/src/TCDev.APIGenerator/Data/GenericDbContext.cs b/src/TCDev.APIGenerator/Data/GenericDbContext.cs index 082a8c0..65caa68 100644 --- a/src/TCDev.APIGenerator/Data/GenericDbContext.cs +++ b/src/TCDev.APIGenerator/Data/GenericDbContext.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using EntityFrameworkCore.Triggers; -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.OData.Edm; @@ -64,9 +63,11 @@ protected override void OnModelCreating(ModelBuilder builder) } // Add all other custom types, not implementing IEntityTypeConfiguration - foreach (var customType in this.assemblyService.Types.Where(x => x.IsAssignableFrom(typeof(IEntityTypeConfiguration<>)))) + foreach (var customType in this.assemblyService.Types.Where(x => !x.IsAssignableFrom(typeof(IEntityTypeConfiguration<>)))) { builder.Entity(customType); + + //builder.Model.AddEntityType(customType); } base.OnModelCreating(builder); diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs index 62d2a1e..ea2e6fe 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -74,10 +74,7 @@ public static class ApiGeneratorExtension services.AddSingleton(assemblyService); var jsonDefs = JsonConvert.DeserializeObject>(File.ReadAllText("./ApiDefinition.json")); - var jsonTypes = jsonDefs.Select(tp => JsonClassBuilder.CreateClass(tp)) - .ToList(); - - assemblyService.Types.AddRange(jsonTypes); + assemblyService.Types.AddRange(JsonClassBuilder.CreateTypes(jsonDefs)); assemblyService.Types.AddRange(assembly.GetExportedTypes() .Where(x => x.GetCustomAttributes() .Any())); @@ -167,10 +164,15 @@ public static IApplicationBuilder UseAutomaticApiMigrations(this IApplicationBui public static IApplicationBuilder UseApiGenerator(this IApplicationBuilder app) { app.UseSwagger(); - app.UseSwaggerUI(c => c.SwaggerEndpoint( - "/swagger/v1/swagger.json", - $"{ApiGeneratorConfig.SwaggerOptions.Title} {ApiGeneratorConfig.SwaggerOptions.Version}" - )); + app.UseSwaggerUI(c => + { + c.InjectStylesheet("/SwaggerDarkTheme.css"); + c.SwaggerEndpoint( + "/swagger/v1/swagger.json", + $"{ApiGeneratorConfig.SwaggerOptions.Title} {ApiGeneratorConfig.SwaggerOptions.Version}" + ); + }); + return app; } diff --git a/src/TCDev.APIGenerator/Services/Generator.cs b/src/TCDev.APIGenerator/Services/Generator.cs index 1f4cfa3..66e7e0f 100644 --- a/src/TCDev.APIGenerator/Services/Generator.cs +++ b/src/TCDev.APIGenerator/Services/Generator.cs @@ -4,27 +4,69 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; -using Newtonsoft.Json; using TCDev.APIGenerator.Schema; - namespace TCDev.ApiGenerator.Json; public class JsonClassBuilder { - - public static Type CreateClass(JsonClassDefinition definition) - { - try - { - var classCode = $@" // Auto-generated code + public static List CreateTypes(List definitions) + { + try + { + var trees = definitions.Select(def => CreateTree(def)) + .ToList(); + + MetadataReference[] assemblies = AppDomain.CurrentDomain.GetAssemblies() + .Where(p => !p.IsDynamic) + .Where(a => !string.IsNullOrEmpty(a.Location)) + .Select(a => MetadataReference.CreateFromFile(a.Location)) + .ToArray(); + + var compilation = CSharpCompilation + .Create("TCDev.ApiGenerator") + .AddSyntaxTrees(trees) + .AddReferences(assemblies) + .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); + + using var ms = new MemoryStream(); + var result = compilation.Emit(ms); + + if (result.Success) + { + ms.Seek(0, SeekOrigin.Begin); + var assembly = Assembly.Load(ms.ToArray()); + return assembly.ExportedTypes.ToList(); + } + + var failures = result.Diagnostics.Where(diagnostic => + diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); + + + foreach (var diagnostic in failures) + { + Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage()); + } + + throw new Exception("Failed to parse JSON Definitions, could not compile assemblies", + new Exception(string.Join(",", failures.Select(p => p.GetMessage())))); + } + catch (Exception e) + { + Console.WriteLine(e); + throw; + } + } + + public static SyntaxTree CreateTree(JsonClassDefinition definition) + { + var classCode = $@" // Auto-generated code using System; using Swashbuckle.AspNetCore.Annotations; using System.ComponentModel.DataAnnotations; @@ -35,66 +77,30 @@ public static Type CreateClass(JsonClassDefinition definition) namespace TCDev.ApiGenerator {{ - [Api(""{ definition.RouteTemplate }"")] - public class { definition.Name } : IObjectBase<{definition.IdType}> + [Api(""{definition.RouteTemplate}"")] + public class {definition.Name} : IObjectBase<{definition.IdType}> // Add Properties {{ public {definition.IdType} Id {{ get; set;}} "; - // Add all fields - var result1 = definition.Fields.Aggregate(string.Empty, (current, field) => + // Add all fields + var result1 = definition.Fields.Aggregate(string.Empty, (current, field) => current + $@" public {field.Type} {field.Name}{(field.Nullable ? "?" : "")} {{ get; set;}}"); - - // Complete class - classCode += result1; - classCode += $@"}} }}"; - - MetadataReference[] assemblies = AppDomain.CurrentDomain.GetAssemblies().Where(p=>!p.IsDynamic) - .Where(a => !string.IsNullOrEmpty(a.Location)) - .Select(a => MetadataReference.CreateFromFile(a.Location)) - .ToArray(); - classCode = FormatUsingRoslyn(classCode); - - var syntaxTree = CSharpSyntaxTree.ParseText(classCode); - - var compilation = CSharpCompilation - .Create("TCDev.ApiGenerator") - .AddSyntaxTrees(syntaxTree) - .AddReferences(assemblies) - .WithOptions(new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); - - using var ms = new MemoryStream(); - var result = compilation.Emit(ms); - - if (result.Success) - { - ms.Seek(0, SeekOrigin.Begin); - var assembly = Assembly.Load(ms.ToArray()); - - var newTypeFullName = $"TCDev.ApiGenerator.{definition.Name}"; - - var type = assembly.GetType(newTypeFullName); - return type; - } - - var failures = result.Diagnostics.Where(diagnostic => - diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error); - - foreach (var diagnostic in failures) Console.Error.WriteLine("{0}: {1}", diagnostic.Id, diagnostic.GetMessage()); - - return null; - } - catch (Exception e) - { - Console.WriteLine(e); - throw; - } - } - public static string FormatUsingRoslyn(string csCode) - { + // Complete class + classCode += result1; + classCode += @"} }"; + + + classCode = FormatUsingRoslyn(classCode); + + return CSharpSyntaxTree.ParseText(classCode); + } + + public static string FormatUsingRoslyn(string csCode) + { var tree = CSharpSyntaxTree.ParseText(csCode); var root = tree.GetRoot() .NormalizeWhitespace(); diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj index af23382..17c5bea 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj @@ -3,7 +3,7 @@ net6.0 TCDev.APIGenerator - 0.1.1-alpha + 0.1.0-alpha-4a262d Tim Cadenbach TCDev Creates fully working CRUD Apis from just models From 543e69a1191357bfe2bdff175bdc4f9eeee42761 Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Sun, 10 Apr 2022 23:31:42 +0200 Subject: [PATCH 12/17] AuthorizationAttribute --- TCDev.APIGenerator.sln | 8 --- sample/ApiGeneratorSampleApp/Model/Car.cs | 2 +- sample/ApiGeneratorSampleApp/Program.cs | 6 +- .../ServiceExtension.cs | 41 ++++++++++++ .../TCDev.APIGenerator.Identity.csproj | 13 ++++ .../Attributes/ApiAttribute.cs | 58 ++++++++++++++++ ...ons.cs => ApiAttributeAttributeOptions.cs} | 2 +- .../Attributes/ApiMethodsToGenerate.cs | 20 ++++++ .../Attributes/AuthorizeAttribute.cs | 46 ++++++++----- .../GeneratedControllerAttribute.cs | 66 ------------------- .../Controller/GenericController.cs | 15 +++-- .../Extension/ApiGeneratorConfig.cs | 22 +++++-- .../Extension/ApiGeneratorExtension.cs | 8 +++ .../Extension/Auth/ScopeHandler.cs | 42 ++++++++++++ .../Extension/Auth/ServiceExtension.cs | 42 ++++++++++++ .../Extension/SwaggerSchemaFilter.cs | 2 +- 16 files changed, 287 insertions(+), 106 deletions(-) create mode 100644 src/TCDev.APIGenerator.Identity/ServiceExtension.cs create mode 100644 src/TCDev.APIGenerator.Identity/TCDev.APIGenerator.Identity.csproj create mode 100644 src/TCDev.APIGenerator/Attributes/ApiAttribute.cs rename src/TCDev.APIGenerator/Attributes/{GeneratedControllerAttributeOptions.cs => ApiAttributeAttributeOptions.cs} (95%) create mode 100644 src/TCDev.APIGenerator/Attributes/ApiMethodsToGenerate.cs delete mode 100644 src/TCDev.APIGenerator/Attributes/GeneratedControllerAttribute.cs create mode 100644 src/TCDev.APIGenerator/Extension/Auth/ScopeHandler.cs create mode 100644 src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs diff --git a/TCDev.APIGenerator.sln b/TCDev.APIGenerator.sln index f975b39..df836d5 100644 --- a/TCDev.APIGenerator.sln +++ b/TCDev.APIGenerator.sln @@ -14,8 +14,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TCDev.APIGenerator.Caching", "src\TCDev.APIGenerator.Caching\TCDev.APIGenerator.Caching.csproj", "{0C8E23AD-AC5D-41D4-9F67-0ECF3D1C4BE1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TCDev.APIGenerator.GraphQL", "src\TCDev.APIGenerator.GraphQL\TCDev.APIGenerator.GraphQL.csproj", "{EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TCDev.APIGenerator.Schema", "src\TCDev.APIGenerator.Schema\TCDev.APIGenerator.Schema.csproj", "{94E59385-D259-40A1-A373-1FBD0A42CD63}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ApiGenerator", "ApiGenerator", "{4189D7E0-F171-4267-AC64-C9A83BB1B559}" @@ -59,11 +57,6 @@ Global {0C8E23AD-AC5D-41D4-9F67-0ECF3D1C4BE1}.Release|Any CPU.ActiveCfg = Release|Any CPU {0C8E23AD-AC5D-41D4-9F67-0ECF3D1C4BE1}.SampleAppJson|Any CPU.ActiveCfg = SampleAppJson|Any CPU {0C8E23AD-AC5D-41D4-9F67-0ECF3D1C4BE1}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU - {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}.DebugWithSampleApp|Any CPU.ActiveCfg = DebugWithSampleApp|Any CPU - {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}.SampleAppJson|Any CPU.ActiveCfg = SampleAppJson|Any CPU - {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D}.SampleAppNuget|Any CPU.ActiveCfg = SampleAppNuget|Any CPU {94E59385-D259-40A1-A373-1FBD0A42CD63}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {94E59385-D259-40A1-A373-1FBD0A42CD63}.Debug|Any CPU.Build.0 = Debug|Any CPU {94E59385-D259-40A1-A373-1FBD0A42CD63}.DebugWithSampleApp|Any CPU.ActiveCfg = DebugWithSampleApp|Any CPU @@ -106,7 +99,6 @@ Global {FE869C02-6C9A-4D9B-BBE2-56F1B21B2A55} = {4189D7E0-F171-4267-AC64-C9A83BB1B559} {303BF897-594C-4911-91CF-3887A8B8E839} = {8CC9B68F-E1C2-45B3-8814-B9FF4E1B2AB8} {0C8E23AD-AC5D-41D4-9F67-0ECF3D1C4BE1} = {4189D7E0-F171-4267-AC64-C9A83BB1B559} - {EDEA4DF4-49DF-4205-9B8E-61D76F26BA8D} = {4189D7E0-F171-4267-AC64-C9A83BB1B559} {94E59385-D259-40A1-A373-1FBD0A42CD63} = {4189D7E0-F171-4267-AC64-C9A83BB1B559} {BA9E04E6-4B66-4369-9B2F-C6CEC9499851} = {8CC9B68F-E1C2-45B3-8814-B9FF4E1B2AB8} {7F3574D1-7421-4824-A0BB-522F3BC9BAC4} = {4189D7E0-F171-4267-AC64-C9A83BB1B559} diff --git a/sample/ApiGeneratorSampleApp/Model/Car.cs b/sample/ApiGeneratorSampleApp/Model/Car.cs index c13b209..df937d3 100644 --- a/sample/ApiGeneratorSampleApp/Model/Car.cs +++ b/sample/ApiGeneratorSampleApp/Model/Car.cs @@ -7,7 +7,7 @@ namespace ApiGeneratorSampleApI.Model { - [Api("/car")] + [Api("/car", authorize: true)] public class Car : IObjectBase { [Key] diff --git a/sample/ApiGeneratorSampleApp/Program.cs b/sample/ApiGeneratorSampleApp/Program.cs index 2a3ac03..fec1391 100644 --- a/sample/ApiGeneratorSampleApp/Program.cs +++ b/sample/ApiGeneratorSampleApp/Program.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; using TCDev.ApiGenerator.Extension; +using TCDev.APIGenerator.Identity; var builder = WebApplication.CreateBuilder(args); @@ -9,7 +10,7 @@ builder.Services.AddControllers(); -//builder.Services.AddApiGeneratorServices(builder.Configuration, JsonClassBuilder.CreateClass()); +builder.Services.AddApiGeneratorIdentity(builder.Configuration); builder.Services.AddApiGeneratorServices(builder.Configuration, Assembly.GetExecutingAssembly()); var app = builder.Build(); @@ -23,8 +24,7 @@ app.UseStaticFiles(); app.UseRouting(); -app.UseAuthentication(); -app.UseAuthorization(); +app.UseApiGeneratorAuthentication(); app.UseEndpoints(endpoints => { diff --git a/src/TCDev.APIGenerator.Identity/ServiceExtension.cs b/src/TCDev.APIGenerator.Identity/ServiceExtension.cs new file mode 100644 index 0000000..b898d2e --- /dev/null +++ b/src/TCDev.APIGenerator.Identity/ServiceExtension.cs @@ -0,0 +1,41 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace TCDev.APIGenerator.Identity +{ + public static class ServiceExtension + { + public static IServiceCollection ConfigureIdentity(this IServiceCollection services, IConfiguration configuration) + { + string domain = $"https://{configuration["Auth0:Domain"]}/"; + services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = domain; + options.Audience = configuration["Auth0:Audience"]; + options.TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = ClaimTypes.NameIdentifier + }; + }); + + + return services; + } + + public static IApplicationBuilder UseApiGeneratorAuthentication(this IApplicationBuilder app) + { + app.UseAuthentication(); + app.UseAuthorization(); + + return app; + } + + + } +} \ No newline at end of file diff --git a/src/TCDev.APIGenerator.Identity/TCDev.APIGenerator.Identity.csproj b/src/TCDev.APIGenerator.Identity/TCDev.APIGenerator.Identity.csproj new file mode 100644 index 0000000..c121e0a --- /dev/null +++ b/src/TCDev.APIGenerator.Identity/TCDev.APIGenerator.Identity.csproj @@ -0,0 +1,13 @@ + + + + net6.0 + enable + enable + + + + + + + diff --git a/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs b/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs new file mode 100644 index 0000000..1f821c7 --- /dev/null +++ b/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs @@ -0,0 +1,58 @@ +// TCDev.de 2022/04/10 +// TCDev.APIGenerator.ApiAttribute.cs +// https://github.com/DeeJayTC/net-dynamic-api + +using System; +using System.Linq; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; + +namespace TCDev.ApiGenerator.Attributes +{ + [AttributeUsage(AttributeTargets.Class)] + public class ApiAttribute : Attribute + { + /// + /// Attribute defining auto generated controller for the class + /// + /// The full base route for the class ie /myclass/ + /// + /// + /// + /// + /// + /// + /// + /// + /// The methods to generate for this endpoint + public ApiAttribute( + string route, + ApiMethodsToGenerate methods = ApiMethodsToGenerate.All, + string[] requiredReadClaims = null, + string[] requiredWriteClaims = null, + string[] requiredRolesRead = null, + string[] requiredRolesWrite = null, + bool fireEvents = false, + bool authorize = true, + bool cache = false, + int cacheDuration = 50000) + { + this.Route = route; + this.Options = new ApiAttributeAttributeOptions + { + RequiredReadClaims = requiredReadClaims, + RequiredWriteClaims = requiredWriteClaims, + Authorize = authorize, + Cache = cache, + CacheDuration = cacheDuration, + FireEvents = fireEvents, + Methods = methods + }; + } + + public string Route { get; set; } + public ApiAttributeAttributeOptions Options { get; set; } + + } +} diff --git a/src/TCDev.APIGenerator/Attributes/GeneratedControllerAttributeOptions.cs b/src/TCDev.APIGenerator/Attributes/ApiAttributeAttributeOptions.cs similarity index 95% rename from src/TCDev.APIGenerator/Attributes/GeneratedControllerAttributeOptions.cs rename to src/TCDev.APIGenerator/Attributes/ApiAttributeAttributeOptions.cs index 12f6932..e99b36e 100644 --- a/src/TCDev.APIGenerator/Attributes/GeneratedControllerAttributeOptions.cs +++ b/src/TCDev.APIGenerator/Attributes/ApiAttributeAttributeOptions.cs @@ -22,7 +22,7 @@ public class ApiAttributeAttributeOptions /// /// Wether authorized access is required or not /// - public bool Authorize { get; set; } = true; + public bool Authorize { get; set; } = false; /// /// Cache responses diff --git a/src/TCDev.APIGenerator/Attributes/ApiMethodsToGenerate.cs b/src/TCDev.APIGenerator/Attributes/ApiMethodsToGenerate.cs new file mode 100644 index 0000000..174690f --- /dev/null +++ b/src/TCDev.APIGenerator/Attributes/ApiMethodsToGenerate.cs @@ -0,0 +1,20 @@ +// TCDev 2022/03/16 +// Apache 2.0 License +// https://www.github.com/deejaytc/dotnet-utils + +using System; + +namespace TCDev.ApiGenerator.Attributes +{ + + [Flags] + public enum ApiMethodsToGenerate + { + Get = 1, + GetById = 2, + Insert = 4, + Update = 8, + Delete = 16, + All = Get | GetById | Delete | Update | Insert + } +} \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Attributes/AuthorizeAttribute.cs b/src/TCDev.APIGenerator/Attributes/AuthorizeAttribute.cs index f288555..43a2db0 100644 --- a/src/TCDev.APIGenerator/Attributes/AuthorizeAttribute.cs +++ b/src/TCDev.APIGenerator/Attributes/AuthorizeAttribute.cs @@ -3,28 +3,40 @@ // https://www.github.com/deejaytc/dotnet-utils using System; +using System.Linq; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; +using TCDev.APIGenerator.Services; namespace TCDev.ApiGenerator.Attributes { - [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] - public class PatientAuthorizeAttribute : TypeFilterAttribute - { - public PatientAuthorizeAttribute() : base(typeof(AuthFilter)) - { - } + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class ApiAuthAttribute : Attribute, IAuthorizationFilter + { + private readonly AssemblyService _assemblies; - private class AuthFilter : IActionFilter - { - public void OnActionExecuted(ActionExecutedContext filterContext) - { - throw new NotImplementedException(); - } + public void OnAuthorization(AuthorizationFilterContext context) + { + var _assemblies = context.HttpContext.RequestServices.GetService(typeof(AssemblyService)) as AssemblyService; + var _type = _assemblies.Types.FirstOrDefault(p => p.Name == "Car"); - public void OnActionExecuting(ActionExecutingContext context) - { - } - } - } + if (_type == null) return; + + var attrs = Attribute.GetCustomAttributes(_type, typeof(ApiAttribute)); + if (attrs.FirstOrDefault(p => p.GetType() == typeof(ApiAttribute)) is ApiAttribute optionAttrib) + { + if (!optionAttrib.Options.Authorize) return; + + if (context.HttpContext.User.Identity != null && !context.HttpContext.User.Identity.IsAuthenticated) + { + context.Result = new UnauthorizedResult(); + } + } + else + { + throw new Exception($"Could not find ApiAttribute on Class: {_type.GetType()}"); + } + + } + } } \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Attributes/GeneratedControllerAttribute.cs b/src/TCDev.APIGenerator/Attributes/GeneratedControllerAttribute.cs deleted file mode 100644 index 9a3c829..0000000 --- a/src/TCDev.APIGenerator/Attributes/GeneratedControllerAttribute.cs +++ /dev/null @@ -1,66 +0,0 @@ -// TCDev 2022/03/16 -// Apache 2.0 License -// https://www.github.com/deejaytc/dotnet-utils - -using System; - -namespace TCDev.ApiGenerator.Attributes -{ - - [Flags] - public enum ApiMethodsToGenerate - { - Get = 1, - GetById = 2, - Insert = 4, - Update = 8, - Delete = 16, - All = Get | GetById | Delete | Update | Insert - } - - - [AttributeUsage(AttributeTargets.Class)] - public class ApiAttribute : Attribute - { - /// - /// Attribute defining auto generated controller for the class - /// - /// The full base route for the class ie /myclass/ - /// - /// - /// - /// - /// - /// - /// - /// - /// The methods to generate for this endpoint - public ApiAttribute( - string route, - ApiMethodsToGenerate methods = ApiMethodsToGenerate.All, - string[] requiredReadClaims = null, - string[] requiredWriteClaims = null, - string[] requiredRolesRead = null, - string[] requiredRolesWrite = null, - bool fireEvents = false, - bool authorize = true, - bool cache = false, - int cacheDuration = 50000) - { - Route = route; - Options = new ApiAttributeAttributeOptions - { - RequiredReadClaims = requiredReadClaims, - RequiredWriteClaims = requiredWriteClaims, - Authorize = authorize, - Cache = cache, - CacheDuration = cacheDuration, - FireEvents = fireEvents, - Methods = methods - }; - } - - public string Route { get; set; } - public ApiAttributeAttributeOptions Options { get; set; } - } -} \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Controller/GenericController.cs b/src/TCDev.APIGenerator/Controller/GenericController.cs index e22a9a3..937f800 100644 --- a/src/TCDev.APIGenerator/Controller/GenericController.cs +++ b/src/TCDev.APIGenerator/Controller/GenericController.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.Identity.Web.Resource; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Data; using TCDev.ApiGenerator.Interfaces; @@ -18,6 +19,7 @@ namespace TCDev.ApiGenerator; [Route("api/[controller]")] [Produces("application/json")] +[ApiAuthAttribute] public class GenericController : ODataController where T : class, IObjectBase @@ -48,7 +50,7 @@ private void ConfigureController() this.FireEvent = optionAttrib.Options.FireEvents; this.MethodsToGenerate = optionAttrib.Options.Methods; - // Check if we need to remove methods.. + // Check if we need to remove methods. } else { @@ -66,15 +68,17 @@ private void ConfigureController() [EnableQuery( AllowedQueryOptions = AllowedQueryOptions.All, AllowedFunctions = AllowedFunctions.All, - MaxTop = 200, - MaxSkip = 199, - PageSize = 20)] + PageSize = 20) + ] public IActionResult Query() { // Check if post is enabled if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) return BadRequest($"GET is disabled for {typeof(T).Name}"); + HttpContext.VerifyUserHasAnyAcceptedScope("read_all"); + + if (!this.ModelState.IsValid) return BadRequest(); @@ -88,6 +92,9 @@ public async Task Find(TEntityId id) if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) return BadRequest($"GET is disabled for {typeof(T).Name}"); + + + if (!this.ModelState.IsValid) return BadRequest(); diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs index b73fccb..f316eb6 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs @@ -10,14 +10,11 @@ namespace TCDev.ApiGenerator; public class ApiGeneratorConfig { public CacheOptions CacheOptions { get; set; } = new(); - public ApiOptions ApiOptions { get; set; } = new(); - public SwaggerOptions SwaggerOptions { get; set; } = new(); public DatabaseOptions DatabaseOptions { get; set; } = new(); public ODataFunctions ODataOptions { get; set; } = new(); - - public string MetadataRoute { get; set; } = "odata"; + public IdentityOptions IdentityOptions { get; set; } = new(); private readonly IConfiguration configuration; public ApiGeneratorConfig(IConfiguration config) @@ -26,6 +23,7 @@ public ApiGeneratorConfig(IConfiguration config) ?? new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json") + .AddJsonFile("apiGeneratorConfig.json",true) .AddJsonFile("secrets.json", true) .AddEnvironmentVariables() .Build(); @@ -36,7 +34,8 @@ public ApiGeneratorConfig(IConfiguration config) this.configuration.Bind("Api:Swagger", this.SwaggerOptions); this.configuration.Bind("Api:Database", this.DatabaseOptions); this.configuration.Bind("Api:Odata", this.ODataOptions); - } + this.configuration.Bind("Api:Identity", this.IdentityOptions); + } } public class ApiOptions @@ -89,3 +88,16 @@ public class SwaggerOptions public string ContactUri { get; set; } = "https://www.test.de"; public string Route { get; set; } = "/swagger/v1/swagger.json"; } + +public class IdentityOptions +{ + public string EnableIdentity { get; set; } = "false"; + public string Audience { get; set; } = "TCDevApiGenerator"; + public string Authority { get; set; } = "https://localhost:44300"; + public string[] Scopes { get; set; } = { "ReadWrite.All" }; + + public bool ValidateIssuer { get; set; } = true; + public bool ValidateAudience { get; set; } = true; + public bool ValidateLifetime { get; set; } = true; + public bool ValidateIssuerSigningKey { get; set; } = true; +} \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs index ea2e6fe..b333c65 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -69,6 +69,10 @@ public static class ApiGeneratorExtension .AddSingleton(typeof(ITriggers), typeof(Triggers)) .AddScoped(typeof(IGenericRespository<,>), typeof(GenericRespository<,>)); + + + + //Add Framework Services & Options, we use the current assembly to get classes. var assemblyService = new AssemblyService(); services.AddSingleton(assemblyService); @@ -129,6 +133,10 @@ public static class ApiGeneratorExtension opt.EnableQueryFeatures(20000); opt.Select() .Expand() + .OrderBy() + .SetMaxTop(10000) + .Count() + .SkipToken() .Filter(); }) .AddJsonOptions(o => { o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } diff --git a/src/TCDev.APIGenerator/Extension/Auth/ScopeHandler.cs b/src/TCDev.APIGenerator/Extension/Auth/ScopeHandler.cs new file mode 100644 index 0000000..883be1f --- /dev/null +++ b/src/TCDev.APIGenerator/Extension/Auth/ScopeHandler.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; + +namespace TCDev.APIGenerator.Extension.Auth +{ + + public class HasScopeRequirement : IAuthorizationRequirement + { + public string Issuer { get; } + public string Scope { get; } + + public HasScopeRequirement(string scope, string issuer) + { + this.Scope = scope ?? throw new ArgumentNullException(nameof(scope)); + this.Issuer = issuer ?? throw new ArgumentNullException(nameof(issuer)); + } + } + + + public class HasScopeHandler : AuthorizationHandler + { + protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasScopeRequirement requirement) + { + // If user does not have the scope claim, get out of here + if (!context.User.HasClaim(c => c.Type == "scope" && c.Issuer == requirement.Issuer)) + return Task.CompletedTask; + + // Split the scopes string into an array + var scopes = context.User.FindFirst(c => c.Type == "scope" && c.Issuer == requirement.Issuer).Value.Split(' '); + + // Succeed if the scope array contains the required scope + if (scopes.Any(s => s == requirement.Scope)) + context.Succeed(requirement); + + return Task.CompletedTask; + } + } +} diff --git a/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs b/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs new file mode 100644 index 0000000..6815348 --- /dev/null +++ b/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs @@ -0,0 +1,42 @@ +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.IdentityModel.Tokens; + +namespace TCDev.APIGenerator.Identity +{ + public static class ServiceExtension + { + public static IServiceCollection AddApiGeneratorIdentity(this IServiceCollection services, IConfiguration configuration) + { + string domain = $"https://{configuration["Auth0:Domain"]}/"; + services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = domain; + options.Audience = configuration["Auth0:Audience"]; + options.TokenValidationParameters = new TokenValidationParameters + { + NameClaimType = ClaimTypes.NameIdentifier + }; + }); + JwtSecurityTokenHandler.DefaultMapInboundClaims = false; + + return services; + } + + public static IApplicationBuilder UseApiGeneratorAuthentication(this IApplicationBuilder app) + { + app.UseAuthentication(); + app.UseAuthorization(); + + return app; + } + + + } +} \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs b/src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs index c91f4b9..631e2ed 100644 --- a/src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs +++ b/src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs @@ -25,7 +25,7 @@ public void Apply(OpenApiSchema schema, SchemaFilterContext context) foreach (var ignoreDataMemberProperty in ignoreDataMemberProperties) { var propertyToHide = schema.Properties.Keys - .SingleOrDefault(x => x.ToLower() == ignoreDataMemberProperty.Name.ToLower()); + .SingleOrDefault(x => string.Equals(x, ignoreDataMemberProperty.Name, StringComparison.CurrentCultureIgnoreCase)); if (propertyToHide != null) { From b69f6e4d0c5e4b355cc52cb028866bbe4e7c4fdf Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Tue, 12 Apr 2022 01:17:01 +0200 Subject: [PATCH 13/17] Scope + permission checks in controller --- sample/ApiGeneratorSampleApp/Model/Car.cs | 7 +- .../Attributes/ApiAttribute.cs | 16 +- .../ApiAttributeAttributeOptions.cs | 4 +- .../Controller/GenericController.cs | 394 ++++++++++-------- .../Controller/ScopeValidator.cs | 18 + .../Extension/Auth/ServiceExtension.cs | 4 +- src/TCDev.APIGenerator/TCDev.APIGenerator.xml | 12 +- 7 files changed, 263 insertions(+), 192 deletions(-) create mode 100644 src/TCDev.APIGenerator/Controller/ScopeValidator.cs diff --git a/sample/ApiGeneratorSampleApp/Model/Car.cs b/sample/ApiGeneratorSampleApp/Model/Car.cs index df937d3..801c1bf 100644 --- a/sample/ApiGeneratorSampleApp/Model/Car.cs +++ b/sample/ApiGeneratorSampleApp/Model/Car.cs @@ -7,8 +7,11 @@ namespace ApiGeneratorSampleApI.Model { - [Api("/car", authorize: true)] - public class Car : IObjectBase + [Api("/car", + authorize: true, + requiredReadScopes: new string[] { "all.read" }, + requiredWriteScopes: new string[] { "all.write" })] + public class Car : IObjectBase { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs b/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs index 1f821c7..4aa8273 100644 --- a/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs +++ b/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs @@ -17,10 +17,8 @@ public class ApiAttribute : Attribute /// Attribute defining auto generated controller for the class /// /// The full base route for the class ie /myclass/ - /// - /// - /// - /// + /// + /// /// /// /// @@ -29,10 +27,8 @@ public class ApiAttribute : Attribute public ApiAttribute( string route, ApiMethodsToGenerate methods = ApiMethodsToGenerate.All, - string[] requiredReadClaims = null, - string[] requiredWriteClaims = null, - string[] requiredRolesRead = null, - string[] requiredRolesWrite = null, + string[] requiredReadScopes = null, + string[] requiredWriteScopes = null, bool fireEvents = false, bool authorize = true, bool cache = false, @@ -41,8 +37,8 @@ public class ApiAttribute : Attribute this.Route = route; this.Options = new ApiAttributeAttributeOptions { - RequiredReadClaims = requiredReadClaims, - RequiredWriteClaims = requiredWriteClaims, + RequiredReadScopes = requiredReadScopes, + RequiredWriteScopes = requiredWriteScopes, Authorize = authorize, Cache = cache, CacheDuration = cacheDuration, diff --git a/src/TCDev.APIGenerator/Attributes/ApiAttributeAttributeOptions.cs b/src/TCDev.APIGenerator/Attributes/ApiAttributeAttributeOptions.cs index e99b36e..89c4938 100644 --- a/src/TCDev.APIGenerator/Attributes/ApiAttributeAttributeOptions.cs +++ b/src/TCDev.APIGenerator/Attributes/ApiAttributeAttributeOptions.cs @@ -12,12 +12,12 @@ public class ApiAttributeAttributeOptions /// /// Claims required for read access /// - public string[] RequiredReadClaims { get; set; } = new string[0]; + public string[] RequiredReadScopes { get; set; } = new string[0]; /// /// Claims required for write access /// - public string[] RequiredWriteClaims { get; set; } = new string[0]; + public string[] RequiredWriteScopes { get; set; } = new string[0]; /// /// Wether authorized access is required or not diff --git a/src/TCDev.APIGenerator/Controller/GenericController.cs b/src/TCDev.APIGenerator/Controller/GenericController.cs index 937f800..89e2ed7 100644 --- a/src/TCDev.APIGenerator/Controller/GenericController.cs +++ b/src/TCDev.APIGenerator/Controller/GenericController.cs @@ -10,178 +10,234 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; -using Microsoft.Identity.Web.Resource; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Data; using TCDev.ApiGenerator.Interfaces; -namespace TCDev.ApiGenerator; - -[Route("api/[controller]")] -[Produces("application/json")] -[ApiAuthAttribute] -public class GenericController : ODataController - where T : class, - IObjectBase +namespace TCDev.ApiGenerator { - private bool UseCache { get; set; } - private bool FireEvent { get; set; } - - private readonly IAuthorizationService authorizationService; - private readonly IGenericRespository repository; - - public ApiMethodsToGenerate MethodsToGenerate; - - public GenericController(IAuthorizationService authorizationService, IGenericRespository repository) - { - this.repository = repository; - this.authorizationService = authorizationService; - - ConfigureController(); - } - - private void ConfigureController() - { - // Get attribute config from underlying type T - var attrs = Attribute.GetCustomAttributes(typeof(T)); - if (attrs.FirstOrDefault(p => p.GetType() == typeof(ApiAttribute)) is ApiAttribute optionAttrib) - { - this.UseCache = optionAttrib.Options.Cache; - this.FireEvent = optionAttrib.Options.FireEvents; - this.MethodsToGenerate = optionAttrib.Options.Methods; - - // Check if we need to remove methods. - } - else - { - throw new Exception($"Could not find ApiAttribute on Class: {typeof(T)}"); - } - } - - /// - /// Returns a list of entries - /// - /// - [Produces("application/json")] - [ProducesErrorResponseType(typeof(BadRequestResult))] - [HttpGet] - [EnableQuery( - AllowedQueryOptions = AllowedQueryOptions.All, - AllowedFunctions = AllowedFunctions.All, - PageSize = 20) - ] - public IActionResult Query() - { - // Check if post is enabled - if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) - return BadRequest($"GET is disabled for {typeof(T).Name}"); - - HttpContext.VerifyUserHasAnyAcceptedScope("read_all"); - - - if (!this.ModelState.IsValid) - return BadRequest(); - - return Ok(this.repository.Get()); - } - - [HttpGet("{id}")] - public async Task Find(TEntityId id) - { - // Check if post is enabled - if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) - return BadRequest($"GET is disabled for {typeof(T).Name}"); - - - - - if (!this.ModelState.IsValid) - return BadRequest(); - - var record = await this.repository.GetAsync(id); - - - return Ok(record); - } - - - [HttpPost] - public async Task Create([FromBody] T record) - { - try - { - // Check if post is enabled - if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Insert)) - return BadRequest($"POST is disabled for {record.GetType().Name}"); - - // Check if payload is valid - if (!this.ModelState.IsValid) - return BadRequest(); - - // Create the new entry - this.repository.Create(record); - await this.repository.SaveAsync(); - - // respond with the newly created record - return CreatedAtAction("Find", new - { - id = record.Id - }, record); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } - } - - [HttpPut("{id}")] - public async Task Update(TEntityId id, [FromBody] T record) - { - try - { - if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Update)) - return BadRequest($"PUT is disabled for {record.GetType().Name}"); - - if (!this.ModelState.IsValid) - return BadRequest(); - - var existingRecord = await this.repository.GetAsync(id); - if (existingRecord == null) return NotFound(); - - this.repository.Update(record, existingRecord); - await this.repository.SaveAsync(); - - return Ok(record); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } - } - - [HttpDelete("{id}")] - public async Task Delete(TEntityId id) - { - try - { - if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Delete)) - return BadRequest("DELETE is disabled"); - - if (!this.ModelState.IsValid) - return BadRequest(); - - var existingRecord = await this.repository.GetAsync(id); - if (existingRecord == null) return NotFound(); - - this.repository.Delete(id); - if (await this.repository.SaveAsync() == 0) - return BadRequest(); - - return NoContent(); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } - } + [Route("api/[controller]")] + [Produces("application/json")] + [ApiAuthAttribute] + public class GenericController : ODataController + where T : class, + IObjectBase + { + private bool Authorize { get; set; } + private bool UseCache { get; set; } + private bool FireEvent { get; set; } + private string[] readScopes { get; set; } + private string[] writeScopes { get; set; } + + private readonly IAuthorizationService authorizationService; + private readonly IGenericRespository repository; + + public ApiMethodsToGenerate MethodsToGenerate; + + private void ConfigureController() + { + // Get attribute config from underlying type T + var attrs = Attribute.GetCustomAttributes(typeof(T)); + if (attrs.FirstOrDefault(p => p.GetType() == typeof(ApiAttribute)) is ApiAttribute optionAttrib) + { + this.UseCache = optionAttrib.Options.Cache; + this.FireEvent = optionAttrib.Options.FireEvents; + this.MethodsToGenerate = optionAttrib.Options.Methods; + this.readScopes = optionAttrib.Options.RequiredReadScopes; + this.writeScopes = optionAttrib.Options.RequiredReadScopes; + this.Authorize = optionAttrib.Options.Authorize; + + // Check if we need to remove methods. + } + else + { + throw new Exception($"Could not find ApiAttribute on Class: {typeof(T)}"); + } + } + + /// + /// Returns a list of entries + /// + /// + [Produces("application/json")] + [ProducesErrorResponseType(typeof(BadRequestResult))] + [HttpGet] + [EnableQuery( + AllowedQueryOptions = AllowedQueryOptions.All, + AllowedFunctions = AllowedFunctions.All, + PageSize = 20) + ] + public IActionResult Query() + { + // Check if post is enabled + if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) + { + return BadRequest($"GET is disabled for {typeof(T).Name}"); + } + + if (this.Authorize && !this.HttpContext.ValidateScopes(this.readScopes, "")) + { + return Forbid(); + } + + if (!this.ModelState.IsValid) + { + return BadRequest(); + } + + return Ok(this.repository.Get()); + + } + + [HttpGet("{id}")] + public async Task Find(TEntityId id) + { + // Check if post is enabled + if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) + { + return BadRequest($"GET is disabled for {typeof(T).Name}"); + } + + if (this.Authorize && !this.HttpContext.ValidateScopes(this.readScopes, "")) + { + return Forbid(); + } + + if (!this.ModelState.IsValid) + { + return BadRequest(); + } + + var record = await this.repository.GetAsync(id); + + + return Ok(record); + + } + + + [HttpPost] + public async Task Create([FromBody] T record) + { + try + { + // Check if post is enabled + if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Insert)) + { + return BadRequest($"POST is disabled for {record.GetType().Name}"); + } + + if (this.Authorize && !this.HttpContext.ValidateScopes(this.writeScopes, "")) + { + return Forbid(); + } + + // Check if payload is valid + if (!this.ModelState.IsValid) + { + return BadRequest(); + } + + // Create the new entry + this.repository.Create(record); + await this.repository.SaveAsync(); + + // respond with the newly created record + return CreatedAtAction("Find", new + { + id = record.Id + }, record); + + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpPut("{id}")] + public async Task Update(TEntityId id, [FromBody] T record) + { + try + { + if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Update)) + { + return BadRequest($"PUT is disabled for {record.GetType().Name}"); + } + + if (this.Authorize && !this.HttpContext.ValidateScopes(this.writeScopes, "")) + { + return Forbid(); + } + + if (!this.ModelState.IsValid) + { + return BadRequest(); + } + + var existingRecord = await this.repository.GetAsync(id); + if (existingRecord == null) + { + return NotFound(); + } + + this.repository.Update(record, existingRecord); + await this.repository.SaveAsync(); + + return Ok(record); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + [HttpDelete("{id}")] + public async Task Delete(TEntityId id) + { + try + { + if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Delete)) + { + return BadRequest("DELETE is disabled"); + } + + if (this.Authorize && !this.HttpContext.ValidateScopes(this.writeScopes, "")) + { + return Forbid(); + } + + if (!this.ModelState.IsValid) + { + return BadRequest(); + } + + var existingRecord = await this.repository.GetAsync(id); + if (existingRecord == null) + { + return NotFound(); + } + + this.repository.Delete(id); + if (await this.repository.SaveAsync() == 0) + { + return BadRequest(); + } + + return NoContent(); + } + catch (Exception ex) + { + return BadRequest(ex.Message); + } + } + + public GenericController(IAuthorizationService authorizationService, IGenericRespository repository) + { + this.repository = repository; + this.authorizationService = authorizationService; + + ConfigureController(); + } + } } diff --git a/src/TCDev.APIGenerator/Controller/ScopeValidator.cs b/src/TCDev.APIGenerator/Controller/ScopeValidator.cs new file mode 100644 index 0000000..f2acc4a --- /dev/null +++ b/src/TCDev.APIGenerator/Controller/ScopeValidator.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Linq; +using Microsoft.AspNetCore.Http; + +namespace TCDev.ApiGenerator +{ + public static class ScopeValidator + { + public static bool ValidateScopes(this HttpContext context, IEnumerable compareScopes, string resource) + { + var userScopes = context.User.Claims + .FirstOrDefault(p => p.Type == "scope" || p.Type == "scp").Value + .Split(" ") + .ToArray(); + return userScopes.Any(compareScopes.Contains); + } + } +} \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs b/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs index 6815348..86b56b5 100644 --- a/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs +++ b/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs @@ -17,8 +17,8 @@ public static IServiceCollection AddApiGeneratorIdentity(this IServiceCollection .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { - options.Authority = domain; - options.Audience = configuration["Auth0:Audience"]; + options.Authority = "https://structures.eu.auth0.com/"; + options.Audience = "https://www.smoower.com"; options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = ClaimTypes.NameIdentifier diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml index f1f8beb..a033618 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml @@ -4,15 +4,13 @@ TCDev.APIGenerator - + Attribute defining auto generated controller for the class The full base route for the class ie /myclass/ - - - - + + @@ -24,12 +22,12 @@ Configuration settings for generated controller behaviour - + Claims required for read access - + Claims required for write access From 6197b22e5bf76fd4da0c87f5874a2810f3771442 Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Wed, 13 Apr 2022 00:30:28 +0200 Subject: [PATCH 14/17] Add scope lookup for odata query $expand --- sample/ApiGeneratorSampleApp/Model/Car.cs | 20 ++++ .../Attributes/ApiAttribute.cs | 2 +- .../Controller/GenericController.cs | 54 +++++------ .../Controller/ScopeValidator.cs | 8 ++ .../Extension/ApiGeneratorExtension.cs | 1 + .../Services/ODataScopeLookup.cs | 94 +++++++++++++++++++ .../TCDev.APIGenerator.csproj | 4 + src/TCDev.APIGenerator/TCDev.APIGenerator.xml | 21 ++++- 8 files changed, 173 insertions(+), 31 deletions(-) create mode 100644 src/TCDev.APIGenerator/Services/ODataScopeLookup.cs diff --git a/sample/ApiGeneratorSampleApp/Model/Car.cs b/sample/ApiGeneratorSampleApp/Model/Car.cs index 801c1bf..7a54255 100644 --- a/sample/ApiGeneratorSampleApp/Model/Car.cs +++ b/sample/ApiGeneratorSampleApp/Model/Car.cs @@ -27,6 +27,8 @@ public class Car : IObjectBase public string Color { get; set; } public Make? Make { get; set; } + + public Model? Model { get; set; } } @@ -40,5 +42,23 @@ public class Make : IObjectBase public string Name { get; set; } public string Description { get; set; } + + + public Model? Model { get; set; } + } + + + + [Api("/carModel")] + public class Model : IObjectBase + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [SwaggerIgnore] + public Guid Id { get; set; } = Guid.NewGuid(); + public string Name { get; set; } + + public string Description { get; set; } } + } diff --git a/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs b/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs index 4aa8273..d36c21f 100644 --- a/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs +++ b/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs @@ -27,7 +27,7 @@ public class ApiAttribute : Attribute public ApiAttribute( string route, ApiMethodsToGenerate methods = ApiMethodsToGenerate.All, - string[] requiredReadScopes = null, + string[] requiredReadScopes = null, string[] requiredWriteScopes = null, bool fireEvents = false, bool authorize = true, diff --git a/src/TCDev.APIGenerator/Controller/GenericController.cs b/src/TCDev.APIGenerator/Controller/GenericController.cs index 89e2ed7..55ac209 100644 --- a/src/TCDev.APIGenerator/Controller/GenericController.cs +++ b/src/TCDev.APIGenerator/Controller/GenericController.cs @@ -4,15 +4,19 @@ using System; using System.Collections.Generic; +using System.Data.Entity.Core.Metadata.Edm; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.OData.Query; using Microsoft.AspNetCore.OData.Routing.Controllers; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Data; using TCDev.ApiGenerator.Interfaces; +using TCDev.APIGenerator.Services; namespace TCDev.ApiGenerator { @@ -23,16 +27,10 @@ public class GenericController : ODataController where T : class, IObjectBase { - private bool Authorize { get; set; } - private bool UseCache { get; set; } - private bool FireEvent { get; set; } - private string[] readScopes { get; set; } - private string[] writeScopes { get; set; } - + private ApiAttributeAttributeOptions options; private readonly IAuthorizationService authorizationService; private readonly IGenericRespository repository; - - public ApiMethodsToGenerate MethodsToGenerate; + private readonly ODataScopeLookup scopeLookup; private void ConfigureController() { @@ -40,14 +38,7 @@ private void ConfigureController() var attrs = Attribute.GetCustomAttributes(typeof(T)); if (attrs.FirstOrDefault(p => p.GetType() == typeof(ApiAttribute)) is ApiAttribute optionAttrib) { - this.UseCache = optionAttrib.Options.Cache; - this.FireEvent = optionAttrib.Options.FireEvents; - this.MethodsToGenerate = optionAttrib.Options.Methods; - this.readScopes = optionAttrib.Options.RequiredReadScopes; - this.writeScopes = optionAttrib.Options.RequiredReadScopes; - this.Authorize = optionAttrib.Options.Authorize; - - // Check if we need to remove methods. + this.options = optionAttrib.Options; } else { @@ -67,15 +58,18 @@ private void ConfigureController() AllowedFunctions = AllowedFunctions.All, PageSize = 20) ] - public IActionResult Query() + public IActionResult Query(ODataQueryOptions options) { // Check if post is enabled - if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) + if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Get)) { return BadRequest($"GET is disabled for {typeof(T).Name}"); } - if (this.Authorize && !this.HttpContext.ValidateScopes(this.readScopes, "")) + + var requiredScopes = this.scopeLookup.GetRequestedScopes(options); + + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.options.RequiredReadScopes, "")) { return Forbid(); } @@ -93,12 +87,12 @@ public IActionResult Query() public async Task Find(TEntityId id) { // Check if post is enabled - if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) + if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Get)) { return BadRequest($"GET is disabled for {typeof(T).Name}"); } - if (this.Authorize && !this.HttpContext.ValidateScopes(this.readScopes, "")) + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.options.RequiredReadScopes, "")) { return Forbid(); } @@ -122,12 +116,12 @@ public async Task Create([FromBody] T record) try { // Check if post is enabled - if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Insert)) + if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Insert)) { return BadRequest($"POST is disabled for {record.GetType().Name}"); } - if (this.Authorize && !this.HttpContext.ValidateScopes(this.writeScopes, "")) + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.options.RequiredWriteScopes, "")) { return Forbid(); } @@ -160,12 +154,12 @@ public async Task Update(TEntityId id, [FromBody] T record) { try { - if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Update)) + if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Update)) { return BadRequest($"PUT is disabled for {record.GetType().Name}"); } - if (this.Authorize && !this.HttpContext.ValidateScopes(this.writeScopes, "")) + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.options.RequiredWriteScopes, "")) { return Forbid(); } @@ -197,12 +191,12 @@ public async Task Delete(TEntityId id) { try { - if (!this.MethodsToGenerate.HasFlag(ApiMethodsToGenerate.Delete)) + if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Delete)) { return BadRequest("DELETE is disabled"); } - if (this.Authorize && !this.HttpContext.ValidateScopes(this.writeScopes, "")) + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.options.RequiredWriteScopes, "")) { return Forbid(); } @@ -232,10 +226,14 @@ public async Task Delete(TEntityId id) } } - public GenericController(IAuthorizationService authorizationService, IGenericRespository repository) + public GenericController( + IAuthorizationService authorizationService, + IGenericRespository repository, + ODataScopeLookup scopeLookup) { this.repository = repository; this.authorizationService = authorizationService; + this.scopeLookup = scopeLookup; ConfigureController(); } diff --git a/src/TCDev.APIGenerator/Controller/ScopeValidator.cs b/src/TCDev.APIGenerator/Controller/ScopeValidator.cs index f2acc4a..0ae03a1 100644 --- a/src/TCDev.APIGenerator/Controller/ScopeValidator.cs +++ b/src/TCDev.APIGenerator/Controller/ScopeValidator.cs @@ -12,6 +12,14 @@ public static bool ValidateScopes(this HttpContext context, IEnumerable .FirstOrDefault(p => p.Type == "scope" || p.Type == "scp").Value .Split(" ") .ToArray(); + + var query = context.Request.Query.ToDictionary(p => p.Key, p => p.Value); + if (query.ContainsKey($"$expand")) + { + + } + + return userScopes.Any(compareScopes.Contains); } } diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs index b333c65..97af09a 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -67,6 +67,7 @@ public static class ApiGeneratorExtension .AddSingleton(typeof(ITriggers<,>), typeof(Triggers<,>)) .AddSingleton(typeof(ITriggers<>), typeof(Triggers<>)) .AddSingleton(typeof(ITriggers), typeof(Triggers)) + .AddScoped(typeof(ODataScopeLookup<,>)) .AddScoped(typeof(IGenericRespository<,>), typeof(GenericRespository<,>)); diff --git a/src/TCDev.APIGenerator/Services/ODataScopeLookup.cs b/src/TCDev.APIGenerator/Services/ODataScopeLookup.cs new file mode 100644 index 0000000..80f5de5 --- /dev/null +++ b/src/TCDev.APIGenerator/Services/ODataScopeLookup.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Data.Entity.Core.Metadata.Edm; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.OData.Query; +using Microsoft.EntityFrameworkCore.Diagnostics; +using Microsoft.OData.Edm; +using Microsoft.OData.UriParser; +using TCDev.ApiGenerator.Attributes; +using TCDev.ApiGenerator.Interfaces; + +namespace TCDev.APIGenerator.Services +{ + public class ODataScopeLookup where T : IObjectBase + { + private readonly AssemblyService assemblyData; + public ODataScopeLookup(AssemblyService assemblyData) + { + this.assemblyData = assemblyData; + } + + /// + /// Get all scopes required for the given query + /// walks along all $expand properties and checks all required scopes + /// returns a list of all scopes required + /// + /// + /// + public List GetRequestedScopes(ODataQueryOptions options) + { + var scopes = new List(); + var types = GetRequestedTypes(options); + foreach (var type in types) + { + var attrs = Attribute.GetCustomAttributes(type); + if (attrs.FirstOrDefault(p => p.GetType() == typeof(ApiAttribute)) is not ApiAttribute optionAttrib) + continue; + + if(optionAttrib.Options.RequiredReadScopes != null) scopes.AddRange(optionAttrib.Options.RequiredReadScopes); + + } + + return scopes; + } + + /// + /// Get all types requested in the OData query, + /// walks along all $expand properties and returns all types requested + /// + /// + /// + public IEnumerable GetRequestedTypes(ODataQueryOptions options) + { + if (options.SelectExpand.GetType() + .GetProperty("ProcessedSelectExpandClause", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) + ?.GetValue(options.SelectExpand) is SelectExpandClause selectedTypes) + { + var typesRequested = new List + { + this.assemblyData.Types.FirstOrDefault(p => p.Name == options.Context.NavigationSource.Name) + }; + foreach (ExpandedNavigationSelectItem selectItem in selectedTypes.SelectedItems) + { + typesRequested.AddRange(GetTypes(selectItem).FindAll((x) => !typesRequested.Contains(x))); + } + + return typesRequested; + } + + return null; + } + + + private List GetTypes(ExpandedNavigationSelectItem item) + { + var types = new List + { + this.assemblyData.Types.FirstOrDefault(p => p.Name == item.NavigationSource.Name) + }; + + foreach (ExpandedNavigationSelectItem subItem in item.SelectAndExpand.SelectedItems) + { + types.AddRange(GetTypes(subItem).FindAll((x) => !types.Contains(x))); + } + + return types; + } + + } +} diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj index 17c5bea..470d881 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj @@ -61,5 +61,9 @@ + + + + diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml index a033618..fee50e8 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml @@ -52,9 +52,9 @@ Default cache duration - + - Returns a list of entries + Returns a list of entries @@ -69,6 +69,23 @@ Enable Swagger in Production + + + Get all scopes required for the given query + walks along all $expand properties and checks all required scopes + returns a list of all scopes required + + + + + + + Get all types requested in the OData query, + walks along all $expand properties and returns all types requested + + + + Applies route conventions to allow routes for auto generated controllers From c6593f428f64a97e83c025e24990d4147d73512b Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Thu, 14 Apr 2022 00:32:17 +0200 Subject: [PATCH 15/17] more changes --- sample/ApiGeneratorSampleApp/Model/Car.cs | 18 ++++++++++++------ .../JsonClassDefinition.cs | 2 ++ .../Controller/GenericController.cs | 9 +++------ .../Controller/ScopeValidator.cs | 9 +-------- .../Extension/Auth/ServiceExtension.cs | 6 ++++-- src/TCDev.APIGenerator/Services/Generator.cs | 8 +++++++- .../TCDev.APIGenerator.csproj | 2 +- 7 files changed, 30 insertions(+), 24 deletions(-) diff --git a/sample/ApiGeneratorSampleApp/Model/Car.cs b/sample/ApiGeneratorSampleApp/Model/Car.cs index 7a54255..4cb3091 100644 --- a/sample/ApiGeneratorSampleApp/Model/Car.cs +++ b/sample/ApiGeneratorSampleApp/Model/Car.cs @@ -9,8 +9,8 @@ namespace ApiGeneratorSampleApI.Model [Api("/car", authorize: true, - requiredReadScopes: new string[] { "all.read" }, - requiredWriteScopes: new string[] { "all.write" })] + requiredReadScopes: new string[] { "car.read" }, + requiredWriteScopes: new string[] { "car.write" })] public class Car : IObjectBase { [Key] @@ -32,8 +32,11 @@ public class Car : IObjectBase } - [Api("/carMakes")] - public class Make : IObjectBase + [Api("/carMakes", + authorize: true, + requiredReadScopes: new string[] { "make.read" }, + requiredWriteScopes: new string[] { "make.write" })] + public class Make : IObjectBase { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] @@ -49,8 +52,11 @@ public class Make : IObjectBase - [Api("/carModel")] - public class Model : IObjectBase + [Api("/carModel", + authorize: true, + requiredReadScopes: new string[] { "model.read" }, + requiredWriteScopes: new string[] { "model.write" })] + public class Model : IObjectBase { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] diff --git a/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs b/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs index 230b3c9..537f7cf 100644 --- a/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs +++ b/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs @@ -32,6 +32,8 @@ public class JsonClassDefinition [JsonProperty("idType")] public string IdType { get; set; } = "int"; + public bool Authorize { get; set; } = false; + public List Fields { get; set; } } diff --git a/src/TCDev.APIGenerator/Controller/GenericController.cs b/src/TCDev.APIGenerator/Controller/GenericController.cs index 55ac209..5f50d12 100644 --- a/src/TCDev.APIGenerator/Controller/GenericController.cs +++ b/src/TCDev.APIGenerator/Controller/GenericController.cs @@ -66,10 +66,7 @@ public IActionResult Query(ODataQueryOptions options) return BadRequest($"GET is disabled for {typeof(T).Name}"); } - - var requiredScopes = this.scopeLookup.GetRequestedScopes(options); - - if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.options.RequiredReadScopes, "")) + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.scopeLookup.GetRequestedScopes(options), "")) { return Forbid(); } @@ -84,7 +81,7 @@ public IActionResult Query(ODataQueryOptions options) } [HttpGet("{id}")] - public async Task Find(TEntityId id) + public async Task Find(TEntityId id, ODataQueryOptions options) { // Check if post is enabled if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Get)) @@ -92,7 +89,7 @@ public async Task Find(TEntityId id) return BadRequest($"GET is disabled for {typeof(T).Name}"); } - if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.options.RequiredReadScopes, "")) + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.scopeLookup.GetRequestedScopes(options), "")) { return Forbid(); } diff --git a/src/TCDev.APIGenerator/Controller/ScopeValidator.cs b/src/TCDev.APIGenerator/Controller/ScopeValidator.cs index 0ae03a1..cbca258 100644 --- a/src/TCDev.APIGenerator/Controller/ScopeValidator.cs +++ b/src/TCDev.APIGenerator/Controller/ScopeValidator.cs @@ -13,14 +13,7 @@ public static bool ValidateScopes(this HttpContext context, IEnumerable .Split(" ") .ToArray(); - var query = context.Request.Query.ToDictionary(p => p.Key, p => p.Value); - if (query.ContainsKey($"$expand")) - { - - } - - - return userScopes.Any(compareScopes.Contains); + return userScopes.All(compareScopes.Contains); } } } \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs b/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs index 86b56b5..9b14588 100644 --- a/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs +++ b/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.IdentityModel.Tokens; +using TCDev.ApiGenerator; namespace TCDev.APIGenerator.Identity { @@ -12,12 +13,13 @@ public static class ServiceExtension { public static IServiceCollection AddApiGeneratorIdentity(this IServiceCollection services, IConfiguration configuration) { - string domain = $"https://{configuration["Auth0:Domain"]}/"; + var config = new ApiGeneratorConfig(configuration); + services .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { - options.Authority = "https://structures.eu.auth0.com/"; + options.Authority = config.IdentityOptions.Authority; options.Audience = "https://www.smoower.com"; options.TokenValidationParameters = new TokenValidationParameters { diff --git a/src/TCDev.APIGenerator/Services/Generator.cs b/src/TCDev.APIGenerator/Services/Generator.cs index 66e7e0f..233ea47 100644 --- a/src/TCDev.APIGenerator/Services/Generator.cs +++ b/src/TCDev.APIGenerator/Services/Generator.cs @@ -9,6 +9,7 @@ using System.Reflection; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using TCDev.ApiGenerator.Interfaces; using TCDev.APIGenerator.Schema; namespace TCDev.ApiGenerator.Json; @@ -77,7 +78,12 @@ public static SyntaxTree CreateTree(JsonClassDefinition definition) namespace TCDev.ApiGenerator {{ - [Api(""{definition.RouteTemplate}"")] + [Api(""{definition.RouteTemplate}"", + authorize: {definition.Authorize}, + requiredReadScopes: new string[] {{ \""car.read\"" }} + requiredWriteScopes: new string[] {{\""car.read\""}})] + + )] public class {definition.Name} : IObjectBase<{definition.IdType}> // Add Properties diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj index 470d881..05b8587 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj @@ -3,7 +3,7 @@ net6.0 TCDev.APIGenerator - 0.1.0-alpha-4a262d + 0.1.0 Tim Cadenbach TCDev Creates fully working CRUD Apis from just models From 7a9575273b0f29f83d4f695e2fc77dfc50a89394 Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Fri, 15 Apr 2022 00:51:28 +0200 Subject: [PATCH 16/17] Auth for json mode --- .../ApiGeneratorSampleApp/ApiDefinition.json | 3 + .../JsonClassDefinition.cs | 88 ++++++++++++------- .../Extension/ApiGeneratorExtension.cs | 9 +- src/TCDev.APIGenerator/Services/Generator.cs | 19 ++-- 4 files changed, 79 insertions(+), 40 deletions(-) diff --git a/sample/ApiGeneratorSampleApp/ApiDefinition.json b/sample/ApiGeneratorSampleApp/ApiDefinition.json index 85cb0fc..4ffc03e 100644 --- a/sample/ApiGeneratorSampleApp/ApiDefinition.json +++ b/sample/ApiGeneratorSampleApp/ApiDefinition.json @@ -4,6 +4,9 @@ "name": "MakeJSON", "route": "/MakeJSON", "idType": "int", + "authorize": true, + "scopesRead": [ "all.read" ], + "scopesWrite": [ "all.write" ], "Fields": [ { "name": "Name", diff --git a/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs b/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs index 537f7cf..65a855c 100644 --- a/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs +++ b/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs @@ -1,47 +1,71 @@ -using Newtonsoft.Json; +// TCDev.de 2022/04/07 +// TCDev.APIGenerator.Schema.JsonClassDefinition.cs +// https://github.com/DeeJayTC/net-dynamic-api + using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json.Serialization; -using System.Threading.Tasks; +using Newtonsoft.Json; namespace TCDev.APIGenerator.Schema { - [Flags] - public enum Events - { - POST, - PUT, - DELETE, - ALL = POST | PUT | DELETE - } + [Flags] + public enum Events + { + POST, + PUT, + DELETE, + ALL = POST | PUT | DELETE + } + + + public class JsonClassDefinition + { + public string Name { get; set; } + + [JsonProperty("route")] + public string RouteTemplate { get; set; } = "/"; + + [JsonProperty("caching")] + public bool EnableCaching { get; set; } + + [JsonProperty("idType")] + public string IdType { get; set; } = "int"; + public bool Authorize { get; set; } = false; - public class JsonClassDefinition - { - public string Name { get; set; } + [JsonProperty("ScopesRead")] + public List ScopesReadList { get; set; } = new List(); - [JsonProperty("route")] - public string RouteTemplate { get; set; } = "/"; + [JsonProperty("ScopesWrite")] + public List ScopesWriteList { get; set; } = new List(); - [JsonProperty("caching")] - public bool EnableCaching { get; set; } = false; - [JsonProperty("idType")] - public string IdType { get; set; } = "int"; + [JsonIgnore] + public string ScopesRead { + get + { + return ScopesReadList.Any() ? string.Join(",", ScopesReadList.Select(p => $"\"{p}\"").ToList()) : string.Empty; + } + } + [JsonIgnore] + public string ScopesWrite + { + get + { + return ScopesWriteList.Any() ? string.Join(",", ScopesWriteList.Select(p => $"\"{p}\"").ToList()) : string.Empty; + } + } - public bool Authorize { get; set; } = false; - - public List Fields { get; set; } - } + public List Fields { get; set; } + } - public class Field - { - public string Name { get; set; } - public string Type { get; set; } - public bool Nullable { get; set; } - } + public class Field + { + public string Name { get; set; } + public string Type { get; set; } + public bool Nullable { get; set; } + public string MaxLength { get; set; } + } } diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs index 97af09a..8d48b15 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -18,6 +18,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Models; using Newtonsoft.Json; +using Swashbuckle.AspNetCore.SwaggerUI; using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Data; using TCDev.APIGenerator.Extension; @@ -107,7 +108,6 @@ public static class ApiGeneratorExtension Email = ApiGeneratorConfig.SwaggerOptions.ContactMail, Url = new Uri(ApiGeneratorConfig.SwaggerOptions.ContactUri) } }); - c.DocumentFilter(); c.SchemaFilter(); @@ -176,6 +176,13 @@ public static IApplicationBuilder UseApiGenerator(this IApplicationBuilder app) app.UseSwaggerUI(c => { c.InjectStylesheet("/SwaggerDarkTheme.css"); + c.OAuthConfigObject = new OAuthConfigObject() + { + AppName = "ApiGenerator", + ClientId = string.Empty, + ClientSecret = string.Empty, + + }; c.SwaggerEndpoint( "/swagger/v1/swagger.json", $"{ApiGeneratorConfig.SwaggerOptions.Title} {ApiGeneratorConfig.SwaggerOptions.Version}" diff --git a/src/TCDev.APIGenerator/Services/Generator.cs b/src/TCDev.APIGenerator/Services/Generator.cs index 233ea47..2386b82 100644 --- a/src/TCDev.APIGenerator/Services/Generator.cs +++ b/src/TCDev.APIGenerator/Services/Generator.cs @@ -78,13 +78,18 @@ public static SyntaxTree CreateTree(JsonClassDefinition definition) namespace TCDev.ApiGenerator {{ - [Api(""{definition.RouteTemplate}"", - authorize: {definition.Authorize}, - requiredReadScopes: new string[] {{ \""car.read\"" }} - requiredWriteScopes: new string[] {{\""car.read\""}})] - - )] - public class {definition.Name} : IObjectBase<{definition.IdType}> + [Api(""{definition.RouteTemplate}"" "; + + if (definition.Authorize) + { + classCode += $@",authorize: true"; + if(definition.ScopesRead != string.Empty) classCode += $@",requiredReadScopes: new string[] {{ { definition.ScopesRead } }}"; + if (definition.ScopesWrite != string.Empty) classCode += $@",requiredWriteScopes: new string[] {{ { definition.ScopesWrite } }}"; + } + + classCode += $@")] + + public class {definition.Name} : IObjectBase<{definition.IdType}> // Add Properties {{ From d7889ff1a4c9392a979e682c282c43f46272230f Mon Sep 17 00:00:00 2001 From: Tim Cadenbach Date: Fri, 15 Apr 2022 17:30:44 +0200 Subject: [PATCH 17/17] changes --- sample/ApiGeneratorSampleApp/appsettings.json | 4 +- sample/SampleAppJson/Program.cs | 2 +- .../Controller/GenericController.cs | 8 ++-- .../Extension/ApiGeneratorConfig.cs | 2 + .../Extension/ApiGeneratorExtension.cs | 7 ++++ .../Extension/Auth/ServiceExtension.cs | 7 +++- .../Extension/Swagger/EnableQueryFilter.cs | 42 +++++++++++++++++++ .../IgnoreODataQueryOptionOperationFilter.cs | 39 +++++++++++++++++ .../ShowInSwaggerFilter.cs} | 0 .../Extension/Swagger/SwaggerSchemaFilter.cs | 39 +++++++++++++++++ .../Extension/SwaggerSchemaFilter.cs | 37 ---------------- .../TCDev.APIGenerator.csproj | 2 +- 12 files changed, 143 insertions(+), 46 deletions(-) create mode 100644 src/TCDev.APIGenerator/Extension/Swagger/EnableQueryFilter.cs create mode 100644 src/TCDev.APIGenerator/Extension/Swagger/IgnoreODataQueryOptionOperationFilter.cs rename src/TCDev.APIGenerator/Extension/{SwaggerExcludeDisabledFunctions.cs => Swagger/ShowInSwaggerFilter.cs} (100%) create mode 100644 src/TCDev.APIGenerator/Extension/Swagger/SwaggerSchemaFilter.cs delete mode 100644 src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs diff --git a/sample/ApiGeneratorSampleApp/appsettings.json b/sample/ApiGeneratorSampleApp/appsettings.json index 8137544..ae045f9 100644 --- a/sample/ApiGeneratorSampleApp/appsettings.json +++ b/sample/ApiGeneratorSampleApp/appsettings.json @@ -27,9 +27,9 @@ For more info see https://aka.ms/dotnet-template-ms-identity-platform "Api": { "Swagger": { "EnableProduction": "false", // Enable/Disable for production builds - "Description": "Sample Swagger Config", + "Description": "Smoower API Sample", "Version": "v1", - "Title": "ssass Swagger Config Title", + "Title": "Smoower sample config", "ContactMail": "Me@me.de", "ContactUri": "https://www.myuri.com" }, diff --git a/sample/SampleAppJson/Program.cs b/sample/SampleAppJson/Program.cs index e731b50..501788d 100644 --- a/sample/SampleAppJson/Program.cs +++ b/sample/SampleAppJson/Program.cs @@ -14,7 +14,7 @@ var app = builder.Build(); // Configure the HTTP request pipeline. - +app.UseAutomaticApiMigrations(); app.UseHttpsRedirection(); app.UseAuthorization(); diff --git a/src/TCDev.APIGenerator/Controller/GenericController.cs b/src/TCDev.APIGenerator/Controller/GenericController.cs index 5f50d12..0e0afd8 100644 --- a/src/TCDev.APIGenerator/Controller/GenericController.cs +++ b/src/TCDev.APIGenerator/Controller/GenericController.cs @@ -58,7 +58,7 @@ private void ConfigureController() AllowedFunctions = AllowedFunctions.All, PageSize = 20) ] - public IActionResult Query(ODataQueryOptions options) + public IActionResult Query(ODataQueryOptions ODataOpts) { // Check if post is enabled if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Get)) @@ -66,7 +66,7 @@ public IActionResult Query(ODataQueryOptions options) return BadRequest($"GET is disabled for {typeof(T).Name}"); } - if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.scopeLookup.GetRequestedScopes(options), "")) + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.scopeLookup.GetRequestedScopes(ODataOpts), "")) { return Forbid(); } @@ -81,7 +81,7 @@ public IActionResult Query(ODataQueryOptions options) } [HttpGet("{id}")] - public async Task Find(TEntityId id, ODataQueryOptions options) + public async Task Find(ODataQueryOptions ODataOpts,TEntityId id ) { // Check if post is enabled if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Get)) @@ -89,7 +89,7 @@ public async Task Find(TEntityId id, ODataQueryOptions options return BadRequest($"GET is disabled for {typeof(T).Name}"); } - if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.scopeLookup.GetRequestedScopes(options), "")) + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.scopeLookup.GetRequestedScopes(ODataOpts), "")) { return Forbid(); } diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs index f316eb6..b561a3f 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs @@ -97,6 +97,8 @@ public class IdentityOptions public string[] Scopes { get; set; } = { "ReadWrite.All" }; public bool ValidateIssuer { get; set; } = true; + public string MetaDataUri { get; set; } = ""; + public bool ValidateAudience { get; set; } = true; public bool ValidateLifetime { get; set; } = true; public bool ValidateIssuerSigningKey { get; set; } = true; diff --git a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs index 8d48b15..d883f73 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -22,6 +22,7 @@ using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Data; using TCDev.APIGenerator.Extension; +using TCDev.APIGenerator.Extension.Swagger; using TCDev.ApiGenerator.Json; using TCDev.APIGenerator.Schema; using TCDev.APIGenerator.Services; @@ -110,7 +111,13 @@ public static class ApiGeneratorExtension }); c.DocumentFilter(); c.SchemaFilter(); + c.OperationFilter(); + if (ApiGeneratorConfig.ODataOptions.Enabled) + { + c.OperationFilter(); + } + if (ApiGeneratorConfig.ApiOptions.UseXmlComments) { if (!string.IsNullOrEmpty(ApiGeneratorConfig.ApiOptions.XmlCommentsFile)) diff --git a/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs b/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs index 9b14588..b7d6083 100644 --- a/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs +++ b/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs @@ -20,9 +20,14 @@ public static IServiceCollection AddApiGeneratorIdentity(this IServiceCollection .AddJwtBearer(options => { options.Authority = config.IdentityOptions.Authority; - options.Audience = "https://www.smoower.com"; + options.Audience = config.IdentityOptions.Audience; + options.RequireHttpsMetadata = false; + options.MetadataAddress = config.IdentityOptions.MetaDataUri; options.TokenValidationParameters = new TokenValidationParameters { + ValidateIssuer = config.IdentityOptions.ValidateIssuer, + ValidateLifetime = config.IdentityOptions.ValidateLifetime, + ValidateIssuerSigningKey = config.IdentityOptions.ValidateIssuerSigningKey, NameClaimType = ClaimTypes.NameIdentifier }; }); diff --git a/src/TCDev.APIGenerator/Extension/Swagger/EnableQueryFilter.cs b/src/TCDev.APIGenerator/Extension/Swagger/EnableQueryFilter.cs new file mode 100644 index 0000000..9e9eb0e --- /dev/null +++ b/src/TCDev.APIGenerator/Extension/Swagger/EnableQueryFilter.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace TCDev.APIGenerator.Extension.Swagger +{ + class EnableQueryFiler : IOperationFilter + { + static List s_Parameters = (new List<(string Name, string Description)>() + { + ( "$top", "The max number of records."), + ( "$skip", "The number of records to skip."), + ( "$filter", "A function that must evaluate to true for a record to be returned."), + ( "$select", "Specifies a subset of properties to return. Use a comma separated list."), + ( "$orderby", "Determines what values are used to order a collection of records."), + ( "$expand", "Use to add related query data.") + }).Select(pair => new OpenApiParameter + { + Name = pair.Name, + Required = false, + Schema = new OpenApiSchema { Type = "String" }, + In = ParameterLocation.Query, + Description = pair.Description, + + }).ToList(); + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + if (context.ApiDescription.ActionDescriptor.EndpointMetadata.Any(em => em is Microsoft.AspNetCore.OData.Query.EnableQueryAttribute)) + { + + operation.Parameters ??= new List(); + foreach (var item in s_Parameters) + operation.Parameters.Add(item); + } + } + } +} diff --git a/src/TCDev.APIGenerator/Extension/Swagger/IgnoreODataQueryOptionOperationFilter.cs b/src/TCDev.APIGenerator/Extension/Swagger/IgnoreODataQueryOptionOperationFilter.cs new file mode 100644 index 0000000..f4a8d78 --- /dev/null +++ b/src/TCDev.APIGenerator/Extension/Swagger/IgnoreODataQueryOptionOperationFilter.cs @@ -0,0 +1,39 @@ +// TCDev.de 2022/04/15 +// TCDev.APIGenerator.IgnoreODataQueryOptionOperationFilter.cs +// https://github.com/DeeJayTC/net-dynamic-api + +using System.Linq; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace TCDev.APIGenerator.Extension +{ + public class IgnoreODataQueryOptionOperationFilter : IOperationFilter + { + + public void Apply(OpenApiOperation operation, OperationFilterContext context) + { + context.ApiDescription.ParameterDescriptions.ToList() + .ForEach(x => + { + if (x.Name == "ODataOpts") + { + if (operation.Parameters.Any(x => x.Name == "ODataOpts")) + { + try + { + operation.Parameters.Remove(operation.Parameters.Single(p => x.Name == "ODataOpts")); + } + catch + { + + } + + } + } + + }); + + } + } +} diff --git a/src/TCDev.APIGenerator/Extension/SwaggerExcludeDisabledFunctions.cs b/src/TCDev.APIGenerator/Extension/Swagger/ShowInSwaggerFilter.cs similarity index 100% rename from src/TCDev.APIGenerator/Extension/SwaggerExcludeDisabledFunctions.cs rename to src/TCDev.APIGenerator/Extension/Swagger/ShowInSwaggerFilter.cs diff --git a/src/TCDev.APIGenerator/Extension/Swagger/SwaggerSchemaFilter.cs b/src/TCDev.APIGenerator/Extension/Swagger/SwaggerSchemaFilter.cs new file mode 100644 index 0000000..a4b8e8c --- /dev/null +++ b/src/TCDev.APIGenerator/Extension/Swagger/SwaggerSchemaFilter.cs @@ -0,0 +1,39 @@ +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.OData.Query; +using TCDev.ApiGenerator.Attributes; + +namespace TCDev.APIGenerator.Extension +{ + public class SwaggerSchemaFilter : ISchemaFilter + { + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (schema?.Properties == null) + { + return; + } + + var ignoreDataMemberProperties = context.Type.GetProperties() + .Where(t => t.GetCustomAttribute() != null); + + foreach (var ignoreDataMemberProperty in ignoreDataMemberProperties) + { + var propertyToHide = schema.Properties.Keys + .SingleOrDefault(x => string.Equals(x, ignoreDataMemberProperty.Name, StringComparison.CurrentCultureIgnoreCase)); + + if (propertyToHide != null) + { + schema.Properties.Remove(propertyToHide); + } + } + } + } +} diff --git a/src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs b/src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs deleted file mode 100644 index 631e2ed..0000000 --- a/src/TCDev.APIGenerator/Extension/SwaggerSchemaFilter.cs +++ /dev/null @@ -1,37 +0,0 @@ -using Microsoft.OpenApi.Models; -using Swashbuckle.AspNetCore.SwaggerGen; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using TCDev.ApiGenerator.Attributes; - -namespace TCDev.APIGenerator.Extension -{ - public class SwaggerSchemaFilter : ISchemaFilter - { - public void Apply(OpenApiSchema schema, SchemaFilterContext context) - { - if (schema?.Properties == null) - { - return; - } - - var ignoreDataMemberProperties = context.Type.GetProperties() - .Where(t => t.GetCustomAttribute() != null); - - foreach (var ignoreDataMemberProperty in ignoreDataMemberProperties) - { - var propertyToHide = schema.Properties.Keys - .SingleOrDefault(x => string.Equals(x, ignoreDataMemberProperty.Name, StringComparison.CurrentCultureIgnoreCase)); - - if (propertyToHide != null) - { - schema.Properties.Remove(propertyToHide); - } - } - } - } -} diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj index 05b8587..4b274bd 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj @@ -3,7 +3,7 @@ net6.0 TCDev.APIGenerator - 0.1.0 + 0.1.2 Tim Cadenbach TCDev Creates fully working CRUD Apis from just models