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/apigenerator.yml b/.github/workflows/apigenerator.yml new file mode 100644 index 0000000..d86858a --- /dev/null +++ b/.github/workflows/apigenerator.yml @@ -0,0 +1,74 @@ +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: 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/TCDev.APIGenerator.sln b/TCDev.APIGenerator.sln index b748b30..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}" @@ -24,11 +22,16 @@ 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.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 @@ -38,33 +41,56 @@ 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 {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 {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}.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 {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 + {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}.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 @@ -73,9 +99,10 @@ 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} + {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..4ffc03e --- /dev/null +++ b/sample/ApiGeneratorSampleApp/ApiDefinition.json @@ -0,0 +1,45 @@ +[ + + { + "name": "MakeJSON", + "route": "/MakeJSON", + "idType": "int", + "authorize": true, + "scopesRead": [ "all.read" ], + "scopesWrite": [ "all.write" ], + "Fields": [ + { + "name": "Name", + "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" + } + ] + } +] \ No newline at end of file diff --git a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.csproj index d25d36b..ad64568 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,24 +14,36 @@ ApiGeneratorSampleApI.xml + + ApiGeneratorSampleApI.xml + + - - - - + + + + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + + + + + - + + Always + diff --git a/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.xml b/sample/ApiGeneratorSampleApp/ApiGeneratorSampleApI.xml index 2d38add..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/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/Car.cs b/sample/ApiGeneratorSampleApp/Model/Car.cs new file mode 100644 index 0000000..4cb3091 --- /dev/null +++ b/sample/ApiGeneratorSampleApp/Model/Car.cs @@ -0,0 +1,70 @@ +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using TCDev.ApiGenerator.Attributes; +using TCDev.ApiGenerator.Interfaces; + +namespace ApiGeneratorSampleApI.Model +{ + + [Api("/car", + authorize: true, + requiredReadScopes: new string[] { "car.read" }, + requiredWriteScopes: new string[] { "car.write" })] + public class Car : IObjectBase + { + [Key] + [DatabaseGenerated(DatabaseGeneratedOption.Identity)] + [SwaggerIgnore] + public Guid Id { get; set; } = Guid.NewGuid(); + + + [EmailAddress] + public string Name { get; set; } + + public string Description { get; set; } + + public string Color { get; set; } + + public Make? Make { get; set; } + + public Model? Model { get; set; } + } + + + [Api("/carMakes", + authorize: true, + requiredReadScopes: new string[] { "make.read" }, + requiredWriteScopes: new string[] { "make.write" })] + 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; } + + + public Model? Model { get; set; } + } + + + + [Api("/carModel", + authorize: true, + requiredReadScopes: new string[] { "model.read" }, + requiredWriteScopes: new string[] { "model.write" })] + 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/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs b/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs index 6526985..1e96359 100644 --- a/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs +++ b/sample/ApiGeneratorSampleApp/Model/MinimalSample.cs @@ -1,17 +1,20 @@ -using TCDev.ApiGenerator.Attributes; +// TCDev.de 2022/03/24 +// ApiGeneratorSampleApI.MinimalSample.cs +// https://github.com/DeeJayTC/net-dynamic-api + +using TCDev.ApiGenerator.Attributes; using TCDev.ApiGenerator.Interfaces; namespace ApiGeneratorSampleApI.Model { - /// - /// This is the minimal sample, yes this is a working api ;) + /// 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; } - } + [Api("/minimal")] + public class MinimalSample : IObjectBase + { + public string Name { get; set; } + public int Value { get; set; } + public int Id { get; set; } + } } diff --git a/sample/ApiGeneratorSampleApp/Model/Person.cs b/sample/ApiGeneratorSampleApp/Model/Person.cs index 06d456c..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.Schemes.Interfaces; -using TCDev.APIGenerator.Schema.Interfaces; 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 e32e537..fec1391 100644 --- a/sample/ApiGeneratorSampleApp/Program.cs +++ b/sample/ApiGeneratorSampleApp/Program.cs @@ -1,15 +1,16 @@ +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.Identity; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); + +builder.Services.AddApiGeneratorIdentity(builder.Configuration); builder.Services.AddApiGeneratorServices(builder.Configuration, Assembly.GetExecutingAssembly()); var app = builder.Build(); @@ -17,16 +18,16 @@ // Configure the HTTP request pipeline. app.UseApiGenerator(); -app.UseAutomaticAPIMigrations(true); +app.UseAutomaticApiMigrations(true); app.UseHttpsRedirection(); - +app.UseStaticFiles(); app.UseRouting(); -app.UseAuthentication(); -app.UseAuthorization(); +app.UseApiGeneratorAuthentication(); -app.UseEndpoints(endpoints => { +app.UseEndpoints(endpoints => +{ endpoints.UseApiGeneratorEndpoints(); endpoints.MapControllers(); }); 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 diff --git a/sample/ApiGeneratorSampleApp/appsettings.json b/sample/ApiGeneratorSampleApp/appsettings.json index d9adf60..ae045f9 100644 --- a/sample/ApiGeneratorSampleApp/appsettings.json +++ b/sample/ApiGeneratorSampleApp/appsettings.json @@ -25,25 +25,22 @@ 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", + "Description": "Smoower API Sample", "Version": "v1", - "Title": "Sample Swagger Config Title", + "Title": "Smoower sample config", "ContactMail": "Me@me.de", "ContactUri": "https://www.myuri.com" }, "Database": { - "DatabaseType": "SQL|InMemory|SQLite" + "DatabaseType": "InMemory" }, "Odata": { + "Enabled": true, "EnableSelect": true, - "EnableFilter": true, - "EnableSort": true + "EnableFilter": false, + "EnableSort": false } } } 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/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..501788d --- /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.UseAutomaticApiMigrations(); +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/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..e034d84 100644 --- a/sample/SampleAppNuget/SampleAppNuget.csproj +++ b/sample/SampleAppNuget/SampleAppNuget.csproj @@ -1,10 +1,10 @@ - + net6.0 enable enable - Debug;Release;DebugWithSampleApp;SampleAppNuget + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson @@ -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/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 new file mode 100644 index 0000000..34c527b --- /dev/null +++ b/src/TCDev.APIGenerator.DbFirst/Generator.cs @@ -0,0 +1,106 @@ +// 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}{(field.Nullable ? "?" : "")} {{ 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.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/TCDev.APIGenerator.Json.csproj b/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.Json.csproj new file mode 100644 index 0000000..2922adf --- /dev/null +++ b/src/TCDev.APIGenerator.DbFirst/TCDev.APIGenerator.Json.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson + + + + + + + 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.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.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.Schema/Attributes/SwaggerIgnoreAttribute.cs b/src/TCDev.APIGenerator.Schema/Attributes/SwaggerIgnoreAttribute.cs new file mode 100644 index 0000000..e1e0159 --- /dev/null +++ b/src/TCDev.APIGenerator.Schema/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.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 3623713..5f2f71d 100644 --- a/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs +++ b/src/TCDev.APIGenerator.Schema/Interfaces/IObjectBase.cs @@ -2,15 +2,16 @@ // Apache 2.0 License // https://www.github.com/deejaytc/dotnet-utils +using Swashbuckle.AspNetCore.Annotations; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; +using TCDev.ApiGenerator.Attributes; namespace TCDev.ApiGenerator.Interfaces { public interface IObjectBase { [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] 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..65a855c --- /dev/null +++ b/src/TCDev.APIGenerator.Schema/JsonClassDefinition.cs @@ -0,0 +1,71 @@ +// 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 Newtonsoft.Json; + +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; } + + [JsonProperty("idType")] + public string IdType { get; set; } = "int"; + + public bool Authorize { get; set; } = false; + + [JsonProperty("ScopesRead")] + public List ScopesReadList { get; set; } = new List(); + + [JsonProperty("ScopesWrite")] + public List ScopesWriteList { get; set; } = new List(); + + + [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 List Fields { 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.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.Schema/TCDev.APIGenerator.Schema.csproj b/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj index 00b4e92..ad03681 100644 --- a/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj +++ b/src/TCDev.APIGenerator.Schema/TCDev.APIGenerator.Schema.csproj @@ -2,11 +2,13 @@ net6.0 - Debug;Release;DebugWithSampleApp;SampleAppNuget + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson + + diff --git a/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs b/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs new file mode 100644 index 0000000..d36c21f --- /dev/null +++ b/src/TCDev.APIGenerator/Attributes/ApiAttribute.cs @@ -0,0 +1,54 @@ +// 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[] requiredReadScopes = null, + string[] requiredWriteScopes = null, + bool fireEvents = false, + bool authorize = true, + bool cache = false, + int cacheDuration = 50000) + { + this.Route = route; + this.Options = new ApiAttributeAttributeOptions + { + RequiredReadScopes = requiredReadScopes, + RequiredWriteScopes = requiredWriteScopes, + 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 85% rename from src/TCDev.APIGenerator/Attributes/GeneratedControllerAttributeOptions.cs rename to src/TCDev.APIGenerator/Attributes/ApiAttributeAttributeOptions.cs index 12f6932..89c4938 100644 --- a/src/TCDev.APIGenerator/Attributes/GeneratedControllerAttributeOptions.cs +++ b/src/TCDev.APIGenerator/Attributes/ApiAttributeAttributeOptions.cs @@ -12,17 +12,17 @@ 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 /// - 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 14d2110..0e0afd8 100644 --- a/src/TCDev.APIGenerator/Controller/GenericController.cs +++ b/src/TCDev.APIGenerator/Controller/GenericController.cs @@ -1,181 +1,238 @@ -// 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; +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 { - [Route("api/[controller]")] - [Produces("application/json")] - public class GenericController : ODataController - where T : class, - IObjectBase - { - public GenericController(IAuthorizationService authorizationService, IGenericRespository repository) - { - _repository = repository; - _authorizationService = authorizationService; - - ConfigureController(); - } - - private readonly IAuthorizationService _authorizationService; - private readonly IGenericRespository _repository; - private bool useCache { get; set; } - private bool fireEvent { get; set; } - - 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) - { - useCache = optionAttrib.Options.Cache; - fireEvent = optionAttrib.Options.FireEvents; - 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, - MaxTop = 200, - MaxSkip = 199, - PageSize = 20)] - public IActionResult Query() - { - // Check if post is enabled - if (!methodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) - return BadRequest($"GET is disabled for {typeof(T).Name}"); - - if (!ModelState.IsValid) - return BadRequest(); - - return Ok(_repository.Get()); - } - - [HttpGet("{id}")] - public async Task Find(TEntityId id) - { - // Check if post is enabled - if (!methodsToGenerate.HasFlag(ApiMethodsToGenerate.Get)) - return BadRequest($"GET is disabled for {typeof(T).Name}"); - - if (!ModelState.IsValid) - return BadRequest(); - - var record = await _repository.GetAsync(id); - - - return Ok(record); - } - - - [HttpPost] - public async Task Create([FromBody] T record) - { - try - { + [Route("api/[controller]")] + [Produces("application/json")] + [ApiAuthAttribute] + public class GenericController : ODataController + where T : class, + IObjectBase + { + private ApiAttributeAttributeOptions options; + private readonly IAuthorizationService authorizationService; + private readonly IGenericRespository repository; + private readonly ODataScopeLookup scopeLookup; + + 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.options = optionAttrib.Options; + } + 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(ODataQueryOptions ODataOpts) + { + // Check if post is enabled + if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Get)) + { + return BadRequest($"GET is disabled for {typeof(T).Name}"); + } + + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.scopeLookup.GetRequestedScopes(ODataOpts), "")) + { + return Forbid(); + } + + if (!this.ModelState.IsValid) + { + return BadRequest(); + } + + return Ok(this.repository.Get()); + + } + [HttpGet("{id}")] + public async Task Find(ODataQueryOptions ODataOpts,TEntityId id ) + { // 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); - } - } - - [HttpPut("{id}")] - public async Task Update(TEntityId id, [FromBody] T record) - { - try - { - if (!methodsToGenerate.HasFlag(ApiMethodsToGenerate.Update)) - return BadRequest($"PUT is disabled for {record.GetType().Name}"); - - if (!ModelState.IsValid) - return BadRequest(); - - var existingRecord = await _repository.GetAsync(id); - if (existingRecord == null) return NotFound(); - - _repository.Update(record, existingRecord); - await _repository.SaveAsync(); + if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Get)) + { + return BadRequest($"GET is disabled for {typeof(T).Name}"); + } + + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.scopeLookup.GetRequestedScopes(ODataOpts), "")) + { + return Forbid(); + } + + if (!this.ModelState.IsValid) + { + return BadRequest(); + } + + var record = await this.repository.GetAsync(id); + return Ok(record); - } - catch (Exception ex) - { - return BadRequest(ex.Message); - } - } - - [HttpDelete("{id}")] - public async Task Delete(TEntityId id) - { - 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); - } - } - } -} \ No newline at end of file + + } + + + [HttpPost] + public async Task Create([FromBody] T record) + { + try + { + // Check if post is enabled + if (!this.options.Methods.HasFlag(ApiMethodsToGenerate.Insert)) + { + return BadRequest($"POST is disabled for {record.GetType().Name}"); + } + + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.options.RequiredWriteScopes, "")) + { + 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.options.Methods.HasFlag(ApiMethodsToGenerate.Update)) + { + return BadRequest($"PUT is disabled for {record.GetType().Name}"); + } + + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.options.RequiredWriteScopes, "")) + { + 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.options.Methods.HasFlag(ApiMethodsToGenerate.Delete)) + { + return BadRequest("DELETE is disabled"); + } + + if (this.options.Authorize && !this.HttpContext.ValidateScopes(this.options.RequiredWriteScopes, "")) + { + 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, + 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 new file mode 100644 index 0000000..cbca258 --- /dev/null +++ b/src/TCDev.APIGenerator/Controller/ScopeValidator.cs @@ -0,0 +1,19 @@ +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.All(compareScopes.Contains); + } + } +} \ No newline at end of file diff --git a/src/TCDev.APIGenerator/Data/GenericDbContext.cs b/src/TCDev.APIGenerator/Data/GenericDbContext.cs index 711ed46..65caa68 100644 --- a/src/TCDev.APIGenerator/Data/GenericDbContext.cs +++ b/src/TCDev.APIGenerator/Data/GenericDbContext.cs @@ -1,167 +1,126 @@ -// 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; 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.Extension; +using TCDev.APIGenerator.Services; namespace TCDev.ApiGenerator.Data { - public class GenericDbContext : DbContext - { - public GenericDbContext() - { - } - - public GenericDbContext( - DbContextOptions options, - IConfiguration config, - IHttpContextAccessor httpContextAccessor) : base(options) - { - HttpContextAccessor = httpContextAccessor; - } - - protected IHttpContextAccessor HttpContextAccessor { get; } - - public static IModel StaticModel { get; } = BuildStaticModel(); - public static IEdmModel EdmModel { get; } = GetEdmModel(); - - protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) - { - if (!optionsBuilder.IsConfigured) - { - var config = new ApiGeneratorConfig(null); - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile("appsettings.json") - .Build(); - var connectionString = configuration.GetConnectionString("ApiGeneratorDatabase"); + 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: - optionsBuilder.UseSqlServer(connectionString); - break; - case DBType.SQLite: - optionsBuilder.UseSqlite(connectionString); - 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 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); + + //builder.Model.AddEntityType(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); + } - } - } - - // -> 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); - - 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 - } -} \ 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 a8d9854..b561a3f 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorConfig.cs @@ -1,82 +1,105 @@ -// 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 + 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 IdentityOptions IdentityOptions { get; set; } = new(); + private readonly IConfiguration configuration; + + public ApiGeneratorConfig(IConfiguration config) { - 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(); - } + this.configuration = config + ?? new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json") + .AddJsonFile("apiGeneratorConfig.json",true) + .AddJsonFile("secrets.json", true) + .AddEnvironmentVariables() + .Build(); - //Load Options - configuration.Bind("Api:Cache", CacheOptions); - configuration.Bind("Api:Swagger", SwaggerOptions); - configuration.Bind("Api:Database", DatabaseOptions); - } + //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); + this.configuration.Bind("Api:Identity", this.IdentityOptions); + } +} - private readonly IConfigurationRoot Configuration; - public CacheOptions CacheOptions { get; set; } = new CacheOptions(); - public SwaggerOptions SwaggerOptions { get; set; } = new SwaggerOptions(); - public DatabaseOptions DatabaseOptions { get; set; } = new DatabaseOptions(); +public class ApiOptions +{ + public bool UseXmlComments { get; set; } = false; + public string XmlCommentsFile { get; set; } = string.Empty; +} - public string MetadataRoute { get; set; } = "odata"; - } +public class CacheOptions +{ + public bool Enabled { get; set; } = true; + public string Connection { get; set; } = "redis"; +} - public class CacheOptions - { - public bool Enabled { get; set; } = true; - public string Connection { get; set; } = "redis"; - } +public enum DbType +{ + InMemory, + Sql, + //Postgres, + SqLite +} - public enum DBType - { - InMemory, - SQL, - //Postgres, - SQLite - } +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 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 ODataFunctions - { - 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 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 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 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 string MetaDataUri { get; set; } = ""; + + 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 1760972..d883f73 100644 --- a/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs +++ b/src/TCDev.APIGenerator/Extension/ApiGeneratorExtension.cs @@ -1,140 +1,207 @@ -// TCDev 2022/03/16 -// Apache 2.0 License -// https://www.github.com/deejaytc/dotnet-utils +// TCDev.de 2022/03/16 +// TCDev.APIGenerator.ApiGeneratorExtension.cs +// 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.OData; +using Microsoft.AspNetCore.Routing; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; 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.Controllers; -using Microsoft.AspNetCore.Routing; using TCDev.APIGenerator.Extension; -using Microsoft.OpenApi.Models; -using EFCore.AutomaticMigrations; -using System; +using TCDev.APIGenerator.Extension.Swagger; +using TCDev.ApiGenerator.Json; +using TCDev.APIGenerator.Schema; +using TCDev.APIGenerator.Services; +using TCDev.Controllers; namespace TCDev.ApiGenerator.Extension { - public static class ApiGeneratorExtension - { - - public static ApiGeneratorConfig ApiGeneratorConfig { get; set; } = new ApiGeneratorConfig(null); - - 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)); - - // Add Services - services.AddScoped(typeof(IGenericRespository<,>), typeof(GenericRespository<,>)); - - - //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 })) - ); - - - 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.IncludeXmlComments($"{assembly.GetName().Name}.xml", true); - }); - - - services.AddControllers().AddOData(opt => + 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) { - opt.AddRouteComponents("odata", GenericDbContext.EdmModel); - opt.EnableNoDollarQueryOptions = true; - opt.EnableQueryFeatures(20000); - opt.Select().Expand().Filter(); + 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(ODataScopeLookup<,>)) + .AddScoped(typeof(IGenericRespository<,>), typeof(GenericRespository<,>)); - 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 - }); + + + //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")); + assemblyService.Types.AddRange(JsonClassBuilder.CreateTypes(jsonDefs)); + 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(); + c.OperationFilter(); + + if (ApiGeneratorConfig.ODataOptions.Enabled) + { + c.OperationFilter(); + } + + 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() + .OrderBy() + .SetMaxTop(10000) + .Count() + .SkipToken() + .Filter(); + }) + .AddJsonOptions(o => { o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); } + ); + } + else + { + services + .AddControllers() + .AddJsonOptions(o => { o.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter()); }); } - } - - return app; - } - public static IApplicationBuilder UseApiGenerator(this IApplicationBuilder app) - { + return services; + } - 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; - } + 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; + } - } -} \ No newline at end of file + public static IApplicationBuilder UseApiGenerator(this IApplicationBuilder app) + { + app.UseSwagger(); + 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}" + ); + }); + + return app; + } + + public static IEndpointRouteBuilder UseApiGeneratorEndpoints(this IEndpointRouteBuilder builder) + { + return builder; + } + } +} 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..b7d6083 --- /dev/null +++ b/src/TCDev.APIGenerator/Extension/Auth/ServiceExtension.cs @@ -0,0 +1,49 @@ +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; +using TCDev.ApiGenerator; + +namespace TCDev.APIGenerator.Identity +{ + public static class ServiceExtension + { + public static IServiceCollection AddApiGeneratorIdentity(this IServiceCollection services, IConfiguration configuration) + { + var config = new ApiGeneratorConfig(configuration); + + services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(options => + { + options.Authority = config.IdentityOptions.Authority; + 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 + }; + }); + 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/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/FeatureProvider/GenericControllerRouteConvention.cs b/src/TCDev.APIGenerator/FeatureProvider/GenericControllerRouteConvention.cs index dbdf92d..212755b 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,31 +17,28 @@ namespace TCDev.Controllers /// public class GenericControllerRouteConvention : IControllerModelConvention { - public void Apply(ControllerModel controller) { - if (controller.ControllerType.IsGenericType) - { - var genericType = controller.ControllerType.GenericTypeArguments[0]; - var customNameAttribute = genericType.GetCustomAttribute(); - controller.ControllerName = genericType.Name; + if (!controller.ControllerType.IsGenericType) return; - 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)) - }); - } - } - } + 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/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/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/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/Services/Generator.cs b/src/TCDev.APIGenerator/Services/Generator.cs new file mode 100644 index 0000000..2386b82 --- /dev/null +++ b/src/TCDev.APIGenerator/Services/Generator.cs @@ -0,0 +1,122 @@ +// TCDev.de 2022/04/05 +// TCDev.APIGenerator.Generator.cs +// https://www.github.com/deejaytc/dotnet-utils + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using TCDev.ApiGenerator.Interfaces; +using TCDev.APIGenerator.Schema; + +namespace TCDev.ApiGenerator.Json; + + +public class JsonClassBuilder +{ + 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; + using System.ComponentModel.DataAnnotations.Schema; + using System.Text.Json.Serialization; + using TCDev.ApiGenerator.Attributes; + using TCDev.ApiGenerator.Interfaces; + + namespace TCDev.ApiGenerator + {{ + [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 + {{ + public {definition.IdType} Id {{ get; set;}} + "; + + // 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 += @"} }"; + + + classCode = FormatUsingRoslyn(classCode); + + return CSharpSyntaxTree.ParseText(classCode); + } + + 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/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 c5a54c4..4b274bd 100644 --- a/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj +++ b/src/TCDev.APIGenerator/TCDev.APIGenerator.csproj @@ -3,11 +3,11 @@ net6.0 TCDev.APIGenerator - 0.0.5-alpha + 0.1.2 Tim Cadenbach TCDev - Creates fully working CRUD Apis from just class files - Debug;Release;DebugWithSampleApp;SampleAppNuget + Creates fully working CRUD Apis from just models + Debug;Release;DebugWithSampleApp;SampleAppNuget;SampleAppJson @@ -18,6 +18,10 @@ TCDev.ApiGenerator.xml + + TCDev.ApiGenerator.xml + + @@ -33,12 +37,12 @@ + - - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -57,5 +61,9 @@ + + + + diff --git a/src/TCDev.APIGenerator/TCDev.APIGenerator.xml b/src/TCDev.APIGenerator/TCDev.APIGenerator.xml index 9e6423e..fee50e8 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 @@ -54,34 +52,57 @@ Default cache duration - + - Returns a list of entries + Returns a list of entries - + - Generate EDM Model for OData functionalities + Generate EDM Model for OData functionality - + - Enable Swagger in Production + 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 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 + + Names of assemblies to search for classes +