├── .dockerignore ├── .editorconfig ├── .github └── workflows │ ├── build.yaml │ ├── buildAndDeploy.yaml │ └── docs.yml ├── .gitignore ├── CODE-OF-CONDUCT.md ├── CONTRIBUTING.md ├── Fritz.InstantAPIs.Generators.Helpers.Tests ├── Fritz.InstantAPIs.Generators.Helpers.Tests.csproj ├── InstanceAPIGeneratorConfigBuilderTests.cs └── TableConfigTests.cs ├── Fritz.InstantAPIs.Generators.Helpers ├── ApisToGenerate.cs ├── Fritz.InstantAPIs.Generators.Helpers.csproj ├── Included.cs ├── InstanceAPIGeneratorConfig.cs ├── InstanceAPIGeneratorConfigBuilder.cs ├── InstantAPIsForDbContextAttribute.cs └── TableConfig.cs ├── Fritz.InstantAPIs.Generators.Tests ├── CSharpIncrementalSourceGeneratorVerifier.cs ├── DbContextAPIGeneratorTests.cs ├── Diagnostics │ ├── DuplicateDefinitionDiagnosticTests.cs │ └── NotADbContextDiagnosticTests.cs ├── Fritz.InstantAPIs.Generators.Tests.csproj └── TestAssistants.cs ├── Fritz.InstantAPIs.Generators ├── Builders │ ├── DbContextAPIBuilder.cs │ ├── IEndpointRouteBuilderExtensionsBuilder.cs │ └── TablesEnumBuilder.cs ├── DbContextAPIGenerator.cs ├── Diagnostics │ ├── DescriptorConstants.cs │ ├── DuplicateDefinitionDiagnostic.cs │ └── NotADbContextDiagnostic.cs ├── Fritz.InstantAPIs.Generators.csproj ├── HelpUrlBuilder.cs ├── NamespaceGatherer.cs └── TableData.cs ├── InstantAPIs.Generators.Helpers.Tests ├── InstanceAPIGeneratorConfigBuilderTests.cs ├── InstantAPIs.Generators.Helpers.Tests.csproj └── TableConfigTests.cs ├── InstantAPIs.Generators.Helpers ├── ApisToGenerate.cs ├── Included.cs ├── InstanceAPIGeneratorConfig.cs ├── InstanceAPIGeneratorConfigBuilder.cs ├── InstantAPIs.Generators.Helpers.csproj ├── InstantAPIsForDbContextAttribute.cs └── TableConfig.cs ├── InstantAPIs.Generators.Tests ├── CSharpIncrementalSourceGeneratorVerifier.cs ├── DbContextAPIGeneratorTests.cs ├── Diagnostics │ ├── DuplicateDefinitionDiagnosticTests.cs │ └── NotADbContextDiagnosticTests.cs ├── InstantAPIs.Generators.Tests.csproj └── TestAssistants.cs ├── InstantAPIs.Generators ├── Builders │ ├── DbContextAPIBuilder.cs │ ├── IEndpointRouteBuilderExtensionsBuilder.cs │ └── TablesEnumBuilder.cs ├── DbContextAPIGenerator.cs ├── Diagnostics │ ├── DescriptorConstants.cs │ ├── DuplicateDefinitionDiagnostic.cs │ └── NotADbContextDiagnostic.cs ├── HelpUrlBuilder.cs ├── InstantAPIs.Generators.csproj ├── NamespaceGatherer.cs └── TableData.cs ├── InstantAPIs.sln ├── InstantAPIs ├── ApiMethodsToGenerate.cs ├── InstantAPIs.csproj ├── InstantAPIsConfig.cs ├── InstantAPIsServiceCollectionExtensions.cs ├── InstantAPIsServiceOptions.cs ├── JsonAPIsConfig.cs ├── JsonApiExtensions.cs ├── MapApiExtensions.cs └── WebApplicationExtensions.cs ├── LICENSE ├── README.md ├── RELEASE_NOTES.txt ├── Test ├── BaseFixture.cs ├── Configuration │ ├── WhenIncludeDoesNotSpecifyBaseUrl.cs │ ├── WhenIncludeSpecifiesBaseUrl.cs │ ├── WithIncludesAndExcludes.cs │ ├── WithOnlyExcludes.cs │ ├── WithOnlyIncludes.cs │ └── WithoutIncludes.cs ├── GlobalSurpressions.cs ├── InstantAPIs │ └── WebApplicationExtensions.cs ├── StubData │ ├── Address.cs │ ├── Contact.cs │ └── MyContext.cs ├── Test.csproj └── XunitLogger.cs ├── TestJson ├── Program.cs ├── Properties │ └── launchSettings.json ├── TestJson.csproj ├── appsettings.Development.json ├── appsettings.json └── mock.json ├── WorkingApi.Generators ├── MyContext.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── WorkingApi.Generators.csproj ├── appsettings.Development.json └── appsettings.json ├── WorkingApi ├── Migrations │ ├── 20220217005021_Init.Designer.cs │ ├── 20220217005021_Init.cs │ └── MyContextModelSnapshot.cs ├── MyContext.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── WorkingApi.csproj ├── appsettings.Development.json ├── appsettings.json └── contacts.db ├── docs ├── Dockerfile └── README.md └── mkdocs.yml /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.env 4 | **/.git 5 | **/.gitignore 6 | **/.project 7 | **/.settings 8 | **/.toolstarget 9 | **/.vs 10 | **/.vscode 11 | **/*.*proj.user 12 | **/*.dbmdl 13 | **/*.jfm 14 | **/azds.yaml 15 | **/bin 16 | **/charts 17 | **/docker-compose* 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | LICENSE 25 | README.md -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Rules in this file were initially inferred by Visual Studio IntelliCode from the C:\dev\InstantAPIs codebase based on best match to current usage at 2/26/2022 2 | # You can modify the rules from these initially generated values to suit your own policies 3 | # You can learn more about editorconfig here: https://docs.microsoft.com/en-us/visualstudio/ide/editorconfig-code-style-settings-reference 4 | [*.cs] 5 | 6 | 7 | #Core editorconfig formatting - indentation 8 | 9 | #use hard tabs for indentation 10 | indent_style = tab 11 | 12 | #Formatting - new line options 13 | 14 | #place else statements on a new line 15 | csharp_new_line_before_else = true 16 | #require members of anonymous types to be on separate lines 17 | csharp_new_line_before_members_in_anonymous_types = true 18 | #require members of object intializers to be on separate lines 19 | csharp_new_line_before_members_in_object_initializers = true 20 | #require braces to be on a new line for object_collection_array_initializers, methods, control_blocks, types, and lambdas (also known as "Allman" style) 21 | csharp_new_line_before_open_brace = object_collection_array_initializers, methods, control_blocks, types, lambdas 22 | 23 | #Formatting - organize using options 24 | 25 | #do not place System.* using directives before other using directives 26 | dotnet_sort_system_directives_first = false 27 | 28 | #Formatting - spacing options 29 | 30 | #require NO space between a cast and the value 31 | csharp_space_after_cast = false 32 | #require a space before the colon for bases or interfaces in a type declaration 33 | csharp_space_after_colon_in_inheritance_clause = true 34 | #require a space after a keyword in a control flow statement such as a for loop 35 | csharp_space_after_keywords_in_control_flow_statements = true 36 | #require a space before the colon for bases or interfaces in a type declaration 37 | csharp_space_before_colon_in_inheritance_clause = true 38 | #remove space within empty argument list parentheses 39 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 40 | #remove space between method call name and opening parenthesis 41 | csharp_space_between_method_call_name_and_opening_parenthesis = false 42 | #do not place space characters after the opening parenthesis and before the closing parenthesis of a method call 43 | csharp_space_between_method_call_parameter_list_parentheses = false 44 | #remove space within empty parameter list parentheses for a method declaration 45 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 46 | #place a space character after the opening parenthesis and before the closing parenthesis of a method declaration parameter list. 47 | csharp_space_between_method_declaration_parameter_list_parentheses = false 48 | 49 | #Formatting - wrapping options 50 | 51 | #leave code block on single line 52 | csharp_preserve_single_line_blocks = true 53 | #leave statements and member declarations on the same line 54 | csharp_preserve_single_line_statements = true 55 | 56 | #Style - Code block preferences 57 | 58 | #prefer no curly braces if allowed 59 | csharp_prefer_braces = false:suggestion 60 | 61 | #Style - expression bodied member options 62 | 63 | #prefer block bodies for constructors 64 | csharp_style_expression_bodied_constructors = false:suggestion 65 | #prefer block bodies for methods 66 | csharp_style_expression_bodied_methods = false:suggestion 67 | #prefer expression-bodied members for properties 68 | csharp_style_expression_bodied_properties = true:suggestion 69 | 70 | #Style - expression level options 71 | 72 | #prefer the language keyword for member access expressions, instead of the type name, for types that have a keyword to represent them 73 | dotnet_style_predefined_type_for_member_access = true:suggestion 74 | 75 | #Style - Expression-level preferences 76 | 77 | #prefer default(T) over default 78 | csharp_prefer_simple_default_expression = false:suggestion 79 | #prefer objects to be initialized using object initializers when possible 80 | dotnet_style_object_initializer = true:suggestion 81 | #prefer inferred anonymous type member names 82 | dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion 83 | 84 | #Style - implicit and explicit types 85 | 86 | #prefer var over explicit type in all cases, unless overridden by another code style rule 87 | csharp_style_var_elsewhere = true:suggestion 88 | #prefer var is used to declare variables with built-in system types such as int 89 | csharp_style_var_for_built_in_types = true:suggestion 90 | #prefer var when the type is already mentioned on the right-hand side of a declaration expression 91 | csharp_style_var_when_type_is_apparent = true:suggestion 92 | 93 | #Style - language keyword and framework type options 94 | 95 | #prefer the language keyword for local variables, method parameters, and class members, instead of the type name, for types that have a keyword to represent them 96 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 97 | 98 | #Style - modifier options 99 | 100 | #prefer accessibility modifiers to be declared except for public interface members. This will currently not differ from always and will act as future proofing for if C# adds default interface methods. 101 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:suggestion 102 | 103 | #Style - Modifier preferences 104 | 105 | #when this rule is set to a list of modifiers, prefer the specified ordering. 106 | csharp_preferred_modifier_order = public,internal,private,protected,static,readonly,override,abstract:suggestion 107 | 108 | #Style - Pattern matching 109 | 110 | #prefer pattern matching instead of is expression with type casts 111 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 112 | 113 | #Style - qualification options 114 | 115 | #prefer fields not to be prefaced with this. or Me. in Visual Basic 116 | dotnet_style_qualification_for_field = false:suggestion 117 | #prefer methods not to be prefaced with this. or Me. in Visual Basic 118 | dotnet_style_qualification_for_method = false:suggestion 119 | #prefer properties not to be prefaced with this. or Me. in Visual Basic 120 | dotnet_style_qualification_for_property = false:suggestion 121 | csharp_indent_labels = one_less_than_current 122 | csharp_using_directive_placement = outside_namespace:silent 123 | csharp_prefer_simple_using_statement = true:suggestion 124 | csharp_style_namespace_declarations = block_scoped:silent 125 | csharp_style_prefer_method_group_conversion = true:silent 126 | csharp_style_expression_bodied_operators = false:silent 127 | csharp_style_expression_bodied_indexers = true:silent 128 | csharp_style_expression_bodied_accessors = true:silent 129 | csharp_style_expression_bodied_lambdas = true:silent 130 | csharp_style_expression_bodied_local_functions = false:silent 131 | csharp_style_inlined_variable_declaration = true:suggestion 132 | csharp_style_deconstructed_variable_declaration = true:suggestion 133 | csharp_style_prefer_null_check_over_type_check = true:suggestion 134 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion 135 | csharp_style_prefer_not_pattern = true:suggestion 136 | 137 | [*.{cs,vb}] 138 | #### Naming styles #### 139 | 140 | # Naming rules 141 | 142 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion 143 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface 144 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i 145 | 146 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion 147 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types 148 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case 149 | 150 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion 151 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members 152 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case 153 | 154 | # Symbol specifications 155 | 156 | dotnet_naming_symbols.interface.applicable_kinds = interface 157 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 158 | dotnet_naming_symbols.interface.required_modifiers = 159 | 160 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum 161 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 162 | dotnet_naming_symbols.types.required_modifiers = 163 | 164 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method 165 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected 166 | dotnet_naming_symbols.non_field_members.required_modifiers = 167 | 168 | # Naming styles 169 | 170 | dotnet_naming_style.begins_with_i.required_prefix = I 171 | dotnet_naming_style.begins_with_i.required_suffix = 172 | dotnet_naming_style.begins_with_i.word_separator = 173 | dotnet_naming_style.begins_with_i.capitalization = pascal_case 174 | 175 | dotnet_naming_style.pascal_case.required_prefix = 176 | dotnet_naming_style.pascal_case.required_suffix = 177 | dotnet_naming_style.pascal_case.word_separator = 178 | dotnet_naming_style.pascal_case.capitalization = pascal_case 179 | 180 | dotnet_naming_style.pascal_case.required_prefix = 181 | dotnet_naming_style.pascal_case.required_suffix = 182 | dotnet_naming_style.pascal_case.word_separator = 183 | dotnet_naming_style.pascal_case.capitalization = pascal_case 184 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 185 | tab_width = 2 186 | indent_size = 2 187 | end_of_line = crlf 188 | dotnet_style_coalesce_expression = true:suggestion 189 | dotnet_style_null_propagation = true:suggestion 190 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion 191 | dotnet_style_prefer_auto_properties = true:silent 192 | dotnet_style_object_initializer = true:suggestion 193 | dotnet_style_collection_initializer = true:suggestion 194 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 195 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 196 | dotnet_style_prefer_conditional_expression_over_return = true:silent 197 | dotnet_style_explicit_tuple_names = true:suggestion 198 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 199 | dotnet_style_prefer_inferred_anonymous_type_member_names = false:suggestion 200 | dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion 201 | dotnet_style_predefined_type_for_member_access = true:suggestion 202 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths-ignore: 8 | - '**/*.md' 9 | - '**/*.gitignore' 10 | - '**/*.gitattributes' 11 | pull_request: 12 | branches: 13 | - main 14 | - release 15 | paths-ignore: 16 | - '**/*.md' 17 | - '**/*.gitignore' 18 | - '**/*.gitattributes' 19 | workflow_dispatch: 20 | branches: 21 | - main 22 | - release 23 | paths-ignore: 24 | - '**/*.md' 25 | - '**/*.gitignore' 26 | - '**/*.gitattributes' 27 | 28 | jobs: 29 | build: 30 | outputs: 31 | version: ${{ steps.set_proj_version.outputs.PKG_VERSION }} 32 | relnotes: ${{ steps.set_proj_version.outputs.RELNOTES }} 33 | name: Build 34 | runs-on: ubuntu-latest 35 | env: 36 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 37 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 38 | DOTNET_NOLOGO: true 39 | DOTNET_GENERATE_ASPNET_CERTIFICATE: false 40 | DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false 41 | DOTNET_MULTILEVEL_LOOKUP: 0 42 | 43 | steps: 44 | - uses: actions/checkout@v2 45 | 46 | - name: Setup .NET Core SDK 47 | uses: actions/setup-dotnet@v1.9.0 48 | with: 49 | dotnet-version: 6.0.x 50 | 51 | - name: Restore 52 | run: dotnet restore 53 | 54 | - name: Build 55 | run: dotnet build --configuration Release --no-restore 56 | 57 | - name: Test 58 | run: dotnet test 59 | 60 | - name: Pack 61 | run: dotnet pack InstantAPIs/InstantAPIs.csproj --configuration Release -o finalpackage --no-build 62 | 63 | - name: Publish artifact 64 | uses: actions/upload-artifact@master 65 | with: 66 | name: nupkg 67 | path: finalpackage 68 | 69 | - name: Get version 70 | id: set_proj_version 71 | shell: pwsh 72 | run: | 73 | [xml]$nuspec = Get-Content InstantAPIs/InstantAPIs.csproj 74 | $version=$nuspec.project.propertygroup.version 75 | $relnotes=$nuspec.project.propertygroup.packagereleasenotes 76 | echo "PKG_VERSION=$version" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 77 | echo "RELNOTES=$relnotes" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append -------------------------------------------------------------------------------- /.github/workflows/buildAndDeploy.yaml: -------------------------------------------------------------------------------- 1 | name: "Build and Deploy" 2 | 3 | on: 4 | push: 5 | branches: 6 | - release 7 | paths-ignore: 8 | - '**/*.md' 9 | - '**/*.gitignore' 10 | - '**/*.gitattributes' 11 | workflow_dispatch: 12 | branches: 13 | - release 14 | paths-ignore: 15 | - '**/*.md' 16 | - '**/*.gitignore' 17 | - '**/*.gitattributes' 18 | 19 | jobs: 20 | build: 21 | outputs: 22 | version: ${{ steps.set_proj_version.outputs.PKG_VERSION }} 23 | relnotes: ${{ steps.set_proj_version.outputs.RELNOTES }} 24 | name: Build 25 | runs-on: ubuntu-latest 26 | env: 27 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 28 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 29 | DOTNET_NOLOGO: true 30 | DOTNET_GENERATE_ASPNET_CERTIFICATE: false 31 | DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false 32 | DOTNET_MULTILEVEL_LOOKUP: 0 33 | 34 | steps: 35 | - uses: actions/checkout@v2 36 | 37 | - name: Setup .NET Core SDK 38 | uses: actions/setup-dotnet@v1.9.0 39 | with: 40 | dotnet-version: 6.0.x 41 | 42 | - name: Restore 43 | run: dotnet restore 44 | 45 | - name: Build 46 | run: dotnet build --configuration Release --no-restore 47 | 48 | - name: Test 49 | run: dotnet test 50 | 51 | - name: Pack 52 | run: dotnet pack InstantAPIs/InstantAPIs.csproj --configuration Release -o finalpackage --no-build 53 | 54 | - name: Publish artifact 55 | uses: actions/upload-artifact@master 56 | with: 57 | name: nupkg 58 | path: finalpackage 59 | 60 | - name: Get version 61 | id: set_proj_version 62 | shell: pwsh 63 | run: | 64 | [xml]$nuspec = Get-Content InstantAPIs/InstantAPIs.csproj 65 | $version=$nuspec.project.propertygroup.version 66 | $relnotes=$nuspec.project.propertygroup.packagereleasenotes 67 | echo "PKG_VERSION=$version" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 68 | echo "RELNOTES=$relnotes" | Out-File -FilePath $Env:GITHUB_ENV -Encoding utf-8 -Append 69 | 70 | - name: Push to NuGet 71 | run: dotnet nuget push **/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate 72 | 73 | - name: Tag and Release 74 | id: tag_release 75 | uses: softprops/action-gh-release@v0.1.13 76 | with: 77 | body: ${{ env.RELNOTES }} 78 | tag_name: ${{ env.PKG_VERSION }} 79 | files: | 80 | **/*.nupkg 81 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: docs 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | - 'v*' 8 | tags: 9 | - 'v*' 10 | paths: 11 | - '.github/workflows/docs.yml' 12 | - 'docs/**' 13 | - 'mkdocs.yml' 14 | pull_request: 15 | branches: 16 | - 'main' 17 | - 'v*' 18 | paths: 19 | - '.github/workflows/docs.yml' 20 | - 'docs/**' 21 | - 'mkdocs.yml' 22 | 23 | jobs: 24 | publish: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - 28 | name: Checkout 29 | uses: actions/checkout@v2 30 | with: 31 | fetch-depth: 0 32 | - 33 | name: Prepare 34 | id: prepare 35 | run: | 36 | VERSION=edge 37 | RELEASE=false 38 | if [[ $GITHUB_REF == refs/tags/* ]]; then 39 | VERSION=${GITHUB_REF#refs/tags/v} 40 | fi 41 | if [[ $VERSION =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then 42 | RELEASE=true 43 | fi 44 | echo ::set-output name=release::${RELEASE} 45 | - 46 | name: Build mkdocs Docker image 47 | run: | 48 | docker build -t mkdocs -f ./docs/Dockerfile ./ 49 | - 50 | name: Build docs 51 | run: | 52 | docker run --rm -v "$(pwd):/docs" mkdocs build --strict 53 | sudo chown -R $(id -u):$(id -g) ./site 54 | - 55 | name: Check GitHub Pages status 56 | uses: crazy-max/ghaction-github-status@v1 57 | with: 58 | pages_threshold: major_outage 59 | - 60 | name: Deploy 61 | if: success() && github.event_name != 'pull_request' && (endsWith(github.ref, 'main') || steps.prepare.outputs.release == 'true') 62 | uses: crazy-max/ghaction-github-pages@v2.1.1 63 | with: 64 | target_branch: gh-pages 65 | build_dir: site 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | .idea/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET Core 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # StyleCop 66 | StyleCopReport.xml 67 | 68 | # Files built by Visual Studio 69 | *_i.c 70 | *_p.c 71 | *_h.h 72 | *.ilk 73 | *.meta 74 | *.obj 75 | *.iobj 76 | *.pch 77 | *.pdb 78 | *.ipdb 79 | *.pgc 80 | *.pgd 81 | *.rsp 82 | *.sbr 83 | *.tlb 84 | *.tli 85 | *.tlh 86 | *.tmp 87 | *.tmp_proj 88 | *_wpftmp.csproj 89 | *.log 90 | *.vspscc 91 | *.vssscc 92 | .builds 93 | *.pidb 94 | *.svclog 95 | *.scc 96 | 97 | # Chutzpah Test files 98 | _Chutzpah* 99 | 100 | # Visual C++ cache files 101 | ipch/ 102 | *.aps 103 | *.ncb 104 | *.opendb 105 | *.opensdf 106 | *.sdf 107 | *.cachefile 108 | *.VC.db 109 | *.VC.VC.opendb 110 | 111 | # Visual Studio profiler 112 | *.psess 113 | *.vsp 114 | *.vspx 115 | *.sap 116 | 117 | # Visual Studio Trace Files 118 | *.e2e 119 | 120 | # TFS 2012 Local Workspace 121 | $tf/ 122 | 123 | # Guidance Automation Toolkit 124 | *.gpState 125 | 126 | # ReSharper is a .NET coding add-in 127 | _ReSharper*/ 128 | *.[Rr]e[Ss]harper 129 | *.DotSettings.user 130 | 131 | # TeamCity is a build add-in 132 | _TeamCity* 133 | 134 | # DotCover is a Code Coverage Tool 135 | *.dotCover 136 | 137 | # AxoCover is a Code Coverage Tool 138 | .axoCover/* 139 | !.axoCover/settings.json 140 | 141 | # Visual Studio code coverage results 142 | *.coverage 143 | *.coveragexml 144 | 145 | # NCrunch 146 | _NCrunch_* 147 | .*crunch*.local.xml 148 | nCrunchTemp_* 149 | 150 | # MightyMoose 151 | *.mm.* 152 | AutoTest.Net/ 153 | 154 | # Web workbench (sass) 155 | .sass-cache/ 156 | 157 | # Installshield output folder 158 | [Ee]xpress/ 159 | 160 | # DocProject is a documentation generator add-in 161 | DocProject/buildhelp/ 162 | DocProject/Help/*.HxT 163 | DocProject/Help/*.HxC 164 | DocProject/Help/*.hhc 165 | DocProject/Help/*.hhk 166 | DocProject/Help/*.hhp 167 | DocProject/Help/Html2 168 | DocProject/Help/html 169 | 170 | # Click-Once directory 171 | publish/ 172 | 173 | # Publish Web Output 174 | *.[Pp]ublish.xml 175 | *.azurePubxml 176 | # Note: Comment the next line if you want to checkin your web deploy settings, 177 | # but database connection strings (with potential passwords) will be unencrypted 178 | *.pubxml 179 | *.publishproj 180 | 181 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 182 | # checkin your Azure Web App publish settings, but sensitive information contained 183 | # in these scripts will be unencrypted 184 | PublishScripts/ 185 | 186 | # NuGet Packages 187 | *.nupkg 188 | # NuGet Symbol Packages 189 | *.snupkg 190 | # The packages folder can be ignored because of Package Restore 191 | **/[Pp]ackages/* 192 | # except build/, which is used as an MSBuild target. 193 | !**/[Pp]ackages/build/ 194 | # Uncomment if necessary however generally it will be regenerated when needed 195 | #!**/[Pp]ackages/repositories.config 196 | # NuGet v3's project.json files produces more ignorable files 197 | *.nuget.props 198 | *.nuget.targets 199 | 200 | # Microsoft Azure Build Output 201 | csx/ 202 | *.build.csdef 203 | 204 | # Microsoft Azure Emulator 205 | ecf/ 206 | rcf/ 207 | 208 | # Windows Store app package directories and files 209 | AppPackages/ 210 | BundleArtifacts/ 211 | Package.StoreAssociation.xml 212 | _pkginfo.txt 213 | *.appx 214 | *.appxbundle 215 | *.appxupload 216 | 217 | # Visual Studio cache files 218 | # files ending in .cache can be ignored 219 | *.[Cc]ache 220 | # but keep track of directories ending in .cache 221 | !?*.[Cc]ache/ 222 | 223 | # Others 224 | ClientBin/ 225 | ~$* 226 | *~ 227 | *.dbmdl 228 | *.dbproj.schemaview 229 | *.jfm 230 | *.pfx 231 | *.publishsettings 232 | orleans.codegen.cs 233 | 234 | # Including strong name files can present a security risk 235 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 236 | #*.snk 237 | 238 | # Since there are multiple workflows, uncomment next line to ignore bower_components 239 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 240 | #bower_components/ 241 | 242 | # RIA/Silverlight projects 243 | Generated_Code/ 244 | 245 | # Backup & report files from converting an old project file 246 | # to a newer Visual Studio version. Backup files are not needed, 247 | # because we have git ;-) 248 | _UpgradeReport_Files/ 249 | Backup*/ 250 | UpgradeLog*.XML 251 | UpgradeLog*.htm 252 | ServiceFabricBackup/ 253 | *.rptproj.bak 254 | 255 | # SQL Server files 256 | *.mdf 257 | *.ldf 258 | *.ndf 259 | 260 | # Business Intelligence projects 261 | *.rdl.data 262 | *.bim.layout 263 | *.bim_*.settings 264 | *.rptproj.rsuser 265 | *- [Bb]ackup.rdl 266 | *- [Bb]ackup ([0-9]).rdl 267 | *- [Bb]ackup ([0-9][0-9]).rdl 268 | 269 | # Microsoft Fakes 270 | FakesAssemblies/ 271 | 272 | # GhostDoc plugin setting file 273 | *.GhostDoc.xml 274 | 275 | # Node.js Tools for Visual Studio 276 | .ntvs_analysis.dat 277 | node_modules/ 278 | 279 | # Visual Studio 6 build log 280 | *.plg 281 | 282 | # Visual Studio 6 workspace options file 283 | *.opt 284 | 285 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 286 | *.vbw 287 | 288 | # Visual Studio LightSwitch build output 289 | **/*.HTMLClient/GeneratedArtifacts 290 | **/*.DesktopClient/GeneratedArtifacts 291 | **/*.DesktopClient/ModelManifest.xml 292 | **/*.Server/GeneratedArtifacts 293 | **/*.Server/ModelManifest.xml 294 | _Pvt_Extensions 295 | 296 | # Paket dependency manager 297 | .paket/paket.exe 298 | paket-files/ 299 | 300 | # FAKE - F# Make 301 | .fake/ 302 | 303 | # CodeRush personal settings 304 | .cr/personal 305 | 306 | # Python Tools for Visual Studio (PTVS) 307 | __pycache__/ 308 | *.pyc 309 | 310 | # Cake - Uncomment if you are using it 311 | # tools/** 312 | # !tools/packages.config 313 | 314 | # Tabs Studio 315 | *.tss 316 | 317 | # Telerik's JustMock configuration file 318 | *.jmconfig 319 | 320 | # BizTalk build output 321 | *.btp.cs 322 | *.btm.cs 323 | *.odx.cs 324 | *.xsd.cs 325 | 326 | # OpenCover UI analysis results 327 | OpenCover/ 328 | 329 | # Azure Stream Analytics local run output 330 | ASALocalRun/ 331 | 332 | # MSBuild Binary and Structured Log 333 | *.binlog 334 | 335 | # NVidia Nsight GPU debugger configuration file 336 | *.nvuser 337 | 338 | # MFractors (Xamarin productivity tool) working folder 339 | .mfractor/ 340 | 341 | # Local History for Visual Studio 342 | .localhistory/ 343 | 344 | # BeatPulse healthcheck temp database 345 | healthchecksdb 346 | 347 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 348 | MigrationBackup/ 349 | 350 | # Ionide (cross platform F# VS Code tools) working folder 351 | .ionide/ 352 | -------------------------------------------------------------------------------- /CODE-OF-CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project leader at [jeff@jeffreyfritz.com](mailto:jeff@jeffreyfritz.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to InstantAPIs 2 | 3 | Thank you for taking the time to consider contributing to our project. 4 | 5 | The following is a set of guidelines for contributing to the project. These are mostly guidelines, not rules, and can be changed in the future. Please submit your suggestions with a pull-request to this document. 6 | 7 | 1. [Code of Conduct](#code-of-conduct) 8 | 1. [What should I know before I get started?](#what-should-i-know-before-i-get-started?) 9 | 1. [Project Folder Structure](#project-folder-structure) 10 | 1. [Design Decisions](#design-decisions) 11 | 1. [How can I contribute?](#how-can-i-contribute?) 12 | 1. [Work on an Issue in the project](#work-on-an-issue-in-the-project) 13 | 1. [Report a Bug](#report-a-bug) 14 | 1. [Write documentation](#write-documentation) 15 | 16 | 17 | ## Code of Conduct 18 | 19 | We have adopted a [code of conduct](https://github.com/csharpfritz/InstantAPIs/blob/main/CODE-OF-CONDUCT.md) from the Contributor Covenant. Contributors to this project are expected to adhere to this code. Please report unwanted behavior to [jeff@jeffreyfritz.com](mailto:jeff@jeffreyfritz.com) 20 | 21 | ## What should I know before I get started? 22 | 23 | This project is currently a proof-of-concept library that generates Minimal API endpoints for an Entity Framework context. You should be 24 | familiar with C# 10, .NET 6, ASP.NET Core, and Entity Framework Core. Reflection and Source Generators are a plus as this project will use those 25 | .NET features to generate HTTP APIs. 26 | 27 | ### Project Folder Structure 28 | 29 | The folders are a basic structure which will change as needed to support the project as it grows. The folders are configured as follows: 30 | 31 | ``` 32 | 33 | InstantAPIs The project code. 34 | WorkingApi The project to prototype and manually test the functionality being developed. 35 | 36 | ``` 37 | 38 | ### Design Decisions 39 | 40 | Design for this project is ultimately decided by the project lead, [Jeff Fritz](https://github.com/csharpfritz). The following project tenets are adhered to when making decisions: 41 | 42 | 1. This is a library to help make the simple API endpoints that every project needs. 43 | 1. This library is not intended to generate more complex API endpoints. 44 | 1. This toolset should help users to deliver APIs with .NET on any and all ASP.NET Core supported platforms 45 | 46 | ## How can I contribute? 47 | 48 | We are always looking for help on this project. There are several ways that you can help: 49 | 50 | #### Tool suggestions for contributing 51 | 52 | 1. [Visual Studio](https://visualstudio.microsoft.com/) (Windows) 53 | 2. [Visual Studio Code](https://visualstudio.microsoft.com/) (Windows, Linux, Mac) 54 | 3. [Visual Studio For Mac](https://visualstudio.microsoft.com/) 55 | 4. Any text editor (Windows, Linux, Mac) 56 | 5. Any Web browser. 57 | 58 | #### Work on an Issue in the project 59 | 60 | 1. [Work on an Issue](https://github.com/csharpfritz/InstantAPIs/issues) Choose an Issue that you are interested in working on follow the instruction provided in the link below. We thank you in advance for your contributions. 61 | 62 | #### Report a Bug 63 | 64 | 1. [Report a Bug](https://github.com/csharpfritz/InstantAPIs/issues) with the details of a bug that you have found. Be sure to tag it as a `Bug` so that we can triage and track it. 65 | 66 | #### Write documentation 67 | 68 | We are always looking for help to add content to the project. 69 | 70 | #### Recources 71 | 72 | [cmjchrisjones Blog: Contributing To Someone else's git repository](https://cmjchrisjones.dev/posts/contributing-to-someone-elses-git-repository/) 73 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Helpers.Tests/Fritz.InstantAPIs.Generators.Helpers.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | enable 5 | 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Helpers.Tests/InstanceAPIGeneratorConfigBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace Fritz.InstantAPIs.Generators.Helpers.Tests; 5 | 6 | public static class InstanceAPIGeneratorConfigBuilderTests 7 | { 8 | [Fact] 9 | public static void BuildWithNoConfiguration() 10 | { 11 | var builder = new InstanceAPIGeneratorConfigBuilder(); 12 | var config = builder.Build(); 13 | 14 | foreach (var key in Enum.GetValues()) 15 | { 16 | var tableConfig = config[key]; 17 | 18 | Assert.Equal(key, tableConfig.Key); 19 | Assert.Equal(key.ToString(), tableConfig.Name); 20 | Assert.Equal(Included.Yes, tableConfig.Included); 21 | Assert.Equal(ApisToGenerate.All, tableConfig.APIs); 22 | Assert.Equal("/api/a/{id}", tableConfig.RouteDeleteById("a")); 23 | Assert.Equal("/api/a", tableConfig.RouteGet("a")); 24 | Assert.Equal("/api/a/{id}", tableConfig.RouteGetById("a")); 25 | Assert.Equal("/api/a", tableConfig.RoutePost("a")); 26 | Assert.Equal("/api/a/{id}", tableConfig.RoutePut("a")); 27 | } 28 | } 29 | 30 | [Fact] 31 | public static void BuildWithCustomInclude() 32 | { 33 | var builder = new InstanceAPIGeneratorConfigBuilder(); 34 | builder.Include(Values.Two, "a", ApisToGenerate.Get, 35 | routeGet: value => $"get/{value}", 36 | routeGetById: value => $"getById/{value}", 37 | routePost: value => $"post/{value}", 38 | routePut: value => $"put/{value}", 39 | routeDeleteById: value => $"delete/{value}"); 40 | var config = builder.Build(); 41 | 42 | foreach (var key in Enum.GetValues()) 43 | { 44 | var tableConfig = config[key]; 45 | 46 | if (key != Values.Two) 47 | { 48 | Assert.Equal(key, tableConfig.Key); 49 | Assert.Equal(key.ToString(), tableConfig.Name); 50 | Assert.Equal(Included.Yes, tableConfig.Included); 51 | Assert.Equal(ApisToGenerate.All, tableConfig.APIs); 52 | Assert.Equal("/api/a/{id}", tableConfig.RouteDeleteById("a")); 53 | Assert.Equal("/api/a", tableConfig.RouteGet("a")); 54 | Assert.Equal("/api/a/{id}", tableConfig.RouteGetById("a")); 55 | Assert.Equal("/api/a", tableConfig.RoutePost("a")); 56 | Assert.Equal("/api/a/{id}", tableConfig.RoutePut("a")); 57 | } 58 | else 59 | { 60 | Assert.Equal(Values.Two, tableConfig.Key); 61 | Assert.Equal("a", tableConfig.Name); 62 | Assert.Equal(Included.Yes, tableConfig.Included); 63 | Assert.Equal(ApisToGenerate.Get, tableConfig.APIs); 64 | Assert.Equal("delete/a", tableConfig.RouteDeleteById("a")); 65 | Assert.Equal("get/a", tableConfig.RouteGet("a")); 66 | Assert.Equal("getById/a", tableConfig.RouteGetById("a")); 67 | Assert.Equal("post/a", tableConfig.RoutePost("a")); 68 | Assert.Equal("put/a", tableConfig.RoutePut("a")); 69 | } 70 | } 71 | } 72 | 73 | [Fact] 74 | public static void BuildWithCustomExclude() 75 | { 76 | var builder = new InstanceAPIGeneratorConfigBuilder(); 77 | builder.Exclude(Values.Two); 78 | var config = builder.Build(); 79 | 80 | foreach (var key in Enum.GetValues()) 81 | { 82 | var tableConfig = config[key]; 83 | 84 | Assert.Equal(key, tableConfig.Key); 85 | Assert.Equal(key.ToString(), tableConfig.Name); 86 | Assert.Equal(key != Values.Two ? Included.Yes : Included.No, tableConfig.Included); 87 | Assert.Equal(ApisToGenerate.All, tableConfig.APIs); 88 | Assert.Equal("/api/a/{id}", tableConfig.RouteDeleteById("a")); 89 | Assert.Equal("/api/a", tableConfig.RouteGet("a")); 90 | Assert.Equal("/api/a/{id}", tableConfig.RouteGetById("a")); 91 | Assert.Equal("/api/a", tableConfig.RoutePost("a")); 92 | Assert.Equal("/api/a/{id}", tableConfig.RoutePut("a")); 93 | } 94 | } 95 | 96 | private enum Values 97 | { 98 | One, Two, Three 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Helpers.Tests/TableConfigTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Fritz.InstantAPIs.Generators.Helpers.Tests 4 | { 5 | public static class TableConfigTests 6 | { 7 | [Fact] 8 | public static void Create() 9 | { 10 | var config = new TableConfig(Values.Three); 11 | 12 | Assert.Equal(Values.Three, config.Key); 13 | Assert.Equal("Three", config.Name); 14 | Assert.Equal(Included.Yes, config.Included); 15 | Assert.Equal(ApisToGenerate.All, config.APIs); 16 | Assert.Equal("/api/a/{id}", config.RouteDeleteById("a")); 17 | Assert.Equal("/api/a", config.RouteGet("a")); 18 | Assert.Equal("/api/a/{id}", config.RouteGetById("a")); 19 | Assert.Equal("/api/a", config.RoutePost("a")); 20 | Assert.Equal("/api/a/{id}", config.RoutePut("a")); 21 | } 22 | 23 | [Fact] 24 | public static void CreateWithCustomization() 25 | { 26 | var config = new TableConfig(Values.Three, 27 | Included.No, "a", ApisToGenerate.Get, 28 | routeGet: value => $"get/{value}", 29 | routeGetById: value => $"getById/{value}", 30 | routePost: value => $"post/{value}", 31 | routePut: value => $"put/{value}", 32 | routeDeleteById: value => $"delete/{value}"); 33 | 34 | Assert.Equal(Values.Three, config.Key); 35 | Assert.Equal("a", config.Name); 36 | Assert.Equal(Included.No, config.Included); 37 | Assert.Equal(ApisToGenerate.Get, config.APIs); 38 | Assert.Equal("delete/a", config.RouteDeleteById("a")); 39 | Assert.Equal("get/a", config.RouteGet("a")); 40 | Assert.Equal("getById/a", config.RouteGetById("a")); 41 | Assert.Equal("post/a", config.RoutePost("a")); 42 | Assert.Equal("put/a", config.RoutePut("a")); 43 | } 44 | 45 | private enum Values 46 | { 47 | One, Two, Three 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Helpers/ApisToGenerate.cs: -------------------------------------------------------------------------------- 1 | namespace Fritz.InstantAPIs.Generators.Helpers 2 | { 3 | [Flags] 4 | public enum ApisToGenerate 5 | { 6 | Get = 1, 7 | GetById = 2, 8 | Insert = 4, 9 | Update = 8, 10 | Delete = 16, 11 | All = 31 12 | } 13 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Helpers/Fritz.InstantAPIs.Generators.Helpers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Helpers/Included.cs: -------------------------------------------------------------------------------- 1 | namespace Fritz.InstantAPIs.Generators.Helpers 2 | { 3 | public enum Included 4 | { 5 | Yes, No 6 | } 7 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Helpers/InstanceAPIGeneratorConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace Fritz.InstantAPIs.Generators.Helpers 4 | { 5 | public class InstanceAPIGeneratorConfig 6 | where T : struct, Enum 7 | { 8 | private readonly ImmutableDictionary> _tablesConfig; 9 | 10 | internal InstanceAPIGeneratorConfig(ImmutableDictionary> tablesConfig) 11 | { 12 | _tablesConfig = tablesConfig ?? throw new ArgumentNullException(nameof(tablesConfig)); 13 | } 14 | 15 | public virtual TableConfig this[T key] => _tablesConfig[key]; 16 | } 17 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Helpers/InstanceAPIGeneratorConfigBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace Fritz.InstantAPIs.Generators.Helpers 4 | { 5 | public sealed class InstanceAPIGeneratorConfigBuilder 6 | where T : struct, Enum 7 | { 8 | private readonly Dictionary> _tablesConfig = new(); 9 | 10 | public InstanceAPIGeneratorConfigBuilder() 11 | { 12 | foreach(var key in Enum.GetValues()) 13 | { 14 | _tablesConfig.Add(key, new TableConfig(key)); 15 | } 16 | } 17 | 18 | public InstanceAPIGeneratorConfigBuilder Include(T key, string? name = null, ApisToGenerate apis = ApisToGenerate.All, 19 | Func? routeGet = null, Func? routeGetById = null, 20 | Func? routePost = null, Func? routePut = null, 21 | Func? routeDeleteById = null) 22 | { 23 | _tablesConfig[key] = new TableConfig(key, Included.Yes, name: name, 24 | apis: apis, routeGet: routeGet, routeGetById: routeGetById, 25 | routePost: routePost, routePut: routePut, routeDeleteById: routeDeleteById); 26 | return this; 27 | } 28 | 29 | public InstanceAPIGeneratorConfigBuilder Exclude(T key) 30 | { 31 | _tablesConfig[key] = new TableConfig(key, Included.No); 32 | return this; 33 | } 34 | 35 | public InstanceAPIGeneratorConfig Build() => 36 | new InstanceAPIGeneratorConfig(_tablesConfig.ToImmutableDictionary()); 37 | } 38 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Helpers/InstantAPIsForDbContextAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Fritz.InstantAPIs.Generators.Helpers 2 | { 3 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 4 | public sealed class InstantAPIsForDbContextAttribute 5 | : Attribute 6 | { 7 | public InstantAPIsForDbContextAttribute(Type dbContextType) => 8 | DbContextType = dbContextType ?? throw new ArgumentNullException(nameof(dbContextType)); 9 | 10 | public Type DbContextType { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Helpers/TableConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Fritz.InstantAPIs.Generators.Helpers 2 | { 3 | public sealed class TableConfig 4 | where T : struct, Enum 5 | { 6 | public TableConfig(T key) 7 | { 8 | Key = key; 9 | Name = Enum.GetName(key); 10 | } 11 | 12 | public TableConfig(T key, Included included, string? name = null, ApisToGenerate apis = ApisToGenerate.All, 13 | Func? routeGet = null, Func? routeGetById = null, 14 | Func? routePost = null, Func? routePut = null, 15 | Func? routeDeleteById = null) 16 | : this(key) 17 | { 18 | Included = included; 19 | APIs = apis; 20 | if (!string.IsNullOrWhiteSpace(name)) { Name = name; } 21 | if (routeGet is not null) { RouteGet = routeGet; } 22 | if (routeGetById is not null) { RouteGetById = routeGetById; } 23 | if (routePost is not null) { RoutePost = routePost; } 24 | if (routePut is not null) { RoutePut = routePut; } 25 | if (routeDeleteById is not null) { RouteDeleteById = routeDeleteById; } 26 | } 27 | 28 | public T Key { get; } 29 | 30 | public string? Name { get; } = null; 31 | 32 | public Included Included { get; } = Included.Yes; 33 | 34 | public ApisToGenerate APIs { get; } = ApisToGenerate.All; 35 | 36 | public Func RouteDeleteById { get; } = value => $"/api/{value}/{{id}}"; 37 | 38 | public Func RouteGet { get; } = value => $"/api/{value}"; 39 | 40 | public Func RouteGetById { get; } = value => $"/api/{value}/{{id}}"; 41 | 42 | public Func RoutePost { get; } = value => $"/api/{value}"; 43 | 44 | public Func RoutePut { get; } = value => $"/api/{value}/{{id}}"; 45 | } 46 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Tests/CSharpIncrementalSourceGeneratorVerifier.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp.Testing; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.Testing.Verifiers; 4 | using Microsoft.CodeAnalysis.Testing; 5 | using Microsoft.CodeAnalysis; 6 | using System.Collections.Immutable; 7 | using System; 8 | using System.Collections.Generic; 9 | 10 | namespace Fritz.InstantAPIs.Generators.Tests; 11 | 12 | // All of this code was grabbed from Refit 13 | // (https://github.com/reactiveui/refit/pull/1216/files) 14 | // based on a suggestion from 15 | // sharwell - https://discord.com/channels/732297728826277939/732297994699014164/910258213532876861 16 | // If the .NET Roslyn testing packages get updated to have something like this in the future 17 | // I'll remove these helpers. 18 | public static partial class CSharpIncrementalSourceGeneratorVerifier 19 | where TIncrementalGenerator : IIncrementalGenerator, new() 20 | { 21 | #pragma warning disable CA1034 // Nested types should not be visible 22 | public class Test : CSharpSourceGeneratorTest 23 | #pragma warning restore CA1034 // Nested types should not be visible 24 | { 25 | public Test() => 26 | this.SolutionTransforms.Add((solution, projectId) => 27 | { 28 | if (solution is null) 29 | { 30 | throw new ArgumentNullException(nameof(solution)); 31 | } 32 | 33 | if (projectId is null) 34 | { 35 | throw new ArgumentNullException(nameof(projectId)); 36 | } 37 | 38 | var compilationOptions = solution.GetProject(projectId)!.CompilationOptions!; 39 | 40 | // NOTE: I commented this out, because I kept getting this error: 41 | // error CS8632: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. 42 | // Which makes NO sense because I have "#nullable enable" emitted in my 43 | // generated code. So, best to just remove this for now. 44 | 45 | //compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( 46 | // compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); 47 | 48 | solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); 49 | 50 | return solution; 51 | }); 52 | 53 | protected override IEnumerable GetSourceGenerators() 54 | { 55 | yield return new TIncrementalGenerator().AsSourceGenerator(); 56 | } 57 | 58 | protected override ParseOptions CreateParseOptions() 59 | { 60 | var parseOptions = (CSharpParseOptions)base.CreateParseOptions(); 61 | return parseOptions.WithLanguageVersion(LanguageVersion.Preview); 62 | } 63 | } 64 | 65 | static class CSharpVerifierHelper 66 | { 67 | /// 68 | /// By default, the compiler reports diagnostics for nullable reference types at 69 | /// , and the analyzer test framework defaults to only validating 70 | /// diagnostics at . This map contains all compiler diagnostic IDs 71 | /// related to nullability mapped to , which is then used to enable all 72 | /// of these warnings for default validation during analyzer and code fix tests. 73 | /// 74 | internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); 75 | 76 | static ImmutableDictionary GetNullableWarningsFromCompiler() 77 | { 78 | string[] args = { "/warnaserror:nullable" }; 79 | var commandLineArguments = CSharpCommandLineParser.Default.Parse( 80 | args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); 81 | return commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Tests/Diagnostics/DuplicateDefinitionDiagnosticTests.cs: -------------------------------------------------------------------------------- 1 | using Fritz.InstantAPIs.Generators.Diagnostics; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Xunit; 5 | 6 | namespace Fritz.InstantAPIs.Generators.Tests.Diagnostics; 7 | 8 | public static class DuplicateDefinitionDiagnosticTests 9 | { 10 | [Fact] 11 | public static void Create() 12 | { 13 | var diagnostic = DuplicateDefinitionDiagnostic.Create(SyntaxFactory.Attribute(SyntaxFactory.ParseName("A"))); 14 | 15 | Assert.Equal(DuplicateDefinitionDiagnostic.Message, diagnostic.GetMessage()); 16 | Assert.Equal(DuplicateDefinitionDiagnostic.Title, diagnostic.Descriptor.Title); 17 | Assert.Equal(DuplicateDefinitionDiagnostic.Id, diagnostic.Id); 18 | Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); 19 | Assert.Equal(DescriptorConstants.Usage, diagnostic.Descriptor.Category); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Tests/Diagnostics/NotADbContextDiagnosticTests.cs: -------------------------------------------------------------------------------- 1 | using Fritz.InstantAPIs.Generators.Diagnostics; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Microsoft.CodeAnalysis; 5 | using System; 6 | using Xunit; 7 | using System.Linq; 8 | 9 | namespace Fritz.InstantAPIs.Generators.Tests.Diagnostics; 10 | 11 | public static class NotADbContextDiagnosticTests 12 | { 13 | [Fact] 14 | public static void Create() 15 | { 16 | var syntaxTree = CSharpSyntaxTree.ParseText("public class A { }"); 17 | var typeSyntax = syntaxTree.GetRoot().DescendantNodes(_ => true).OfType().Single(); 18 | var references = AppDomain.CurrentDomain.GetAssemblies() 19 | .Where(_ => !_.IsDynamic && !string.IsNullOrWhiteSpace(_.Location)) 20 | .Select(_ => MetadataReference.CreateFromFile(_.Location)); 21 | var compilation = CSharpCompilation.Create("generator", new SyntaxTree[] { syntaxTree }, 22 | references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); 23 | var model = compilation.GetSemanticModel(syntaxTree, true); 24 | 25 | var typeSymbol = model.GetDeclaredSymbol(typeSyntax)!; 26 | 27 | var diagnostic = NotADbContextDiagnostic.Create(typeSymbol, typeSyntax); 28 | 29 | Assert.Equal("The given type, A, does not derive from DbContext.", diagnostic.GetMessage()); 30 | Assert.Equal(NotADbContextDiagnostic.Title, diagnostic.Descriptor.Title); 31 | Assert.Equal(NotADbContextDiagnostic.Id, diagnostic.Id); 32 | Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); 33 | Assert.Equal(DescriptorConstants.Usage, diagnostic.Descriptor.Category); 34 | } 35 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Tests/Fritz.InstantAPIs.Generators.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | enable 5 | 6 | 7 | 8 | 9 | NU1608 10 | 11 | 12 | NU1608 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators.Tests/TestAssistants.cs: -------------------------------------------------------------------------------- 1 | using Fritz.InstantAPIs.Generators.Helpers; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Routing; 6 | using Microsoft.CodeAnalysis.Testing; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Extensions.Logging.Abstractions; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.ComponentModel.DataAnnotations; 15 | using System.Reflection; 16 | using System.Threading.Tasks; 17 | 18 | namespace Fritz.InstantAPIs.Generators.Tests; 19 | 20 | using GeneratorTest = CSharpIncrementalSourceGeneratorVerifier; 21 | 22 | internal static class TestAssistants 23 | { 24 | internal static async Task RunAsync(string code, 25 | IEnumerable<(Type, string, string)> generatedSources, 26 | IEnumerable expectedDiagnostics) 27 | { 28 | var test = new GeneratorTest.Test 29 | { 30 | ReferenceAssemblies = ReferenceAssemblies.Net.Net60, 31 | TestState = 32 | { 33 | Sources = { code }, 34 | }, 35 | }; 36 | 37 | foreach (var generatedSource in generatedSources) 38 | { 39 | test.TestState.GeneratedSources.Add(generatedSource); 40 | } 41 | 42 | var referencedAssemblies = new HashSet 43 | { 44 | typeof(DbContextAPIGenerator).Assembly, 45 | typeof(DbContext).Assembly, 46 | typeof(WebApplication).Assembly, 47 | typeof(FromServicesAttribute).Assembly, 48 | typeof(EndpointRouteBuilderExtensions).Assembly, 49 | typeof(IApplicationBuilder).Assembly, 50 | typeof(IHost).Assembly, 51 | typeof(KeyAttribute).Assembly, 52 | typeof(Included).Assembly, 53 | typeof(IEndpointRouteBuilder).Assembly, 54 | typeof(RouteData).Assembly, 55 | typeof(Results).Assembly, 56 | typeof(NullLogger).Assembly, 57 | typeof(ILogger).Assembly, 58 | typeof(ServiceProviderServiceExtensions).Assembly, 59 | //typeof(IServiceProvider).Assembly 60 | }; 61 | 62 | foreach(var referencedAssembly in referencedAssemblies) 63 | { 64 | test.TestState.AdditionalReferences.Add(referencedAssembly); 65 | } 66 | 67 | test.TestState.ExpectedDiagnostics.AddRange(expectedDiagnostics); 68 | await test.RunAsync().ConfigureAwait(false); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/Builders/DbContextAPIBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | using System; 4 | using System.CodeDom.Compiler; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace Fritz.InstantAPIs.Generators.Builders 11 | { 12 | public static class DbContextAPIBuilder 13 | { 14 | public static SourceText? Build(INamedTypeSymbol type) 15 | { 16 | var tables = new List(); 17 | 18 | foreach(var property in type.GetMembers().OfType() 19 | .Where(_ => !_.IsStatic && _.DeclaredAccessibility == Accessibility.Public && 20 | _.Type.ToDisplayString().StartsWith("Microsoft.EntityFrameworkCore.DbSet"))) 21 | { 22 | var propertyType = (INamedTypeSymbol)property.Type; 23 | var propertySetType = (INamedTypeSymbol)propertyType.TypeArguments.First()!; 24 | 25 | var idProperty = propertySetType.GetMembers().OfType() 26 | .FirstOrDefault(_ => string.Equals(_.Name, "id", StringComparison.OrdinalIgnoreCase) && 27 | !_.IsStatic && _.DeclaredAccessibility == Accessibility.Public); 28 | 29 | if (idProperty is null) 30 | { 31 | idProperty = propertySetType.GetMembers().OfType() 32 | .FirstOrDefault(_ => _.GetAttributes().Any(_ => _.AttributeClass!.Name == "Key" || _.AttributeClass.Name == "KeyAttribute")); 33 | } 34 | 35 | tables.Add(new TableData(property.Name, propertySetType, idProperty?.Type as INamedTypeSymbol, idProperty?.Name)); 36 | } 37 | 38 | if(tables.Count > 0) 39 | { 40 | using var writer = new StringWriter(); 41 | using var indentWriter = new IndentedTextWriter(writer, "\t"); 42 | 43 | var namespaces = new NamespaceGatherer(); 44 | namespaces.Add("System"); 45 | namespaces.Add("System.Collections.Generic"); 46 | namespaces.Add("Fritz.InstantAPIs.Generators.Helpers"); 47 | namespaces.Add("Microsoft.EntityFrameworkCore"); 48 | namespaces.Add("Microsoft.Extensions.Logging"); 49 | namespaces.Add("Microsoft.Extensions.Logging.Abstractions"); 50 | namespaces.Add("Microsoft.AspNetCore.Builder"); 51 | namespaces.Add("Microsoft.AspNetCore.Mvc"); 52 | namespaces.Add("Microsoft.AspNetCore.Routing"); 53 | namespaces.Add("Microsoft.AspNetCore.Http"); 54 | namespaces.Add("Microsoft.Extensions.DependencyInjection"); 55 | 56 | if (!type.ContainingNamespace.IsGlobalNamespace) 57 | { 58 | indentWriter.WriteLine($"namespace {type.ContainingNamespace.ToDisplayString()}"); 59 | indentWriter.WriteLine("{"); 60 | indentWriter.Indent++; 61 | } 62 | 63 | TablesEnumBuilder.Build(indentWriter, type.Name, tables); 64 | indentWriter.WriteLine(); 65 | IEndpointRouteBuilderExtensionsBuilder.Build(indentWriter, type, tables, namespaces); 66 | 67 | if (!type.ContainingNamespace.IsGlobalNamespace) 68 | { 69 | indentWriter.Indent--; 70 | indentWriter.WriteLine("}"); 71 | } 72 | 73 | var code = namespaces.Values.Count > 0 ? 74 | string.Join(Environment.NewLine, 75 | string.Join(Environment.NewLine, namespaces.Values.Select(_ => $"using {_};")), 76 | string.Empty, "#nullable enable", string.Empty, writer.ToString()) : 77 | string.Join(Environment.NewLine, "#nullable enable", string.Empty, writer.ToString()); 78 | 79 | return SourceText.From(code, Encoding.UTF8); 80 | } 81 | 82 | return null; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/Builders/IEndpointRouteBuilderExtensionsBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.CodeDom.Compiler; 3 | using System.Collections.Generic; 4 | 5 | namespace Fritz.InstantAPIs.Generators.Builders 6 | { 7 | internal static class IEndpointRouteBuilderExtensionsBuilder 8 | { 9 | internal static void Build(IndentedTextWriter indentWriter, INamedTypeSymbol type, List tables, 10 | NamespaceGatherer namespaces) 11 | { 12 | indentWriter.WriteLine("public static partial class IEndpointRouteBuilderExtensions"); 13 | indentWriter.WriteLine("{"); 14 | indentWriter.Indent++; 15 | 16 | indentWriter.WriteLine($"public static IEndpointRouteBuilder Map{type.Name}ToAPIs(this IEndpointRouteBuilder app, Action>? options = null)"); 17 | indentWriter.WriteLine("{"); 18 | indentWriter.Indent++; 19 | 20 | indentWriter.WriteLine("ILogger logger = NullLogger.Instance;"); 21 | indentWriter.WriteLine("if (app.ServiceProvider is not null)"); 22 | indentWriter.WriteLine("{"); 23 | indentWriter.Indent++; 24 | 25 | indentWriter.WriteLine("var loggerFactory = app.ServiceProvider.GetRequiredService();"); 26 | indentWriter.WriteLine("logger = loggerFactory.CreateLogger(\"InstantAPIs\");"); 27 | 28 | indentWriter.Indent--; 29 | indentWriter.WriteLine("}"); 30 | indentWriter.WriteLine(); 31 | 32 | indentWriter.WriteLine($"var builder = new InstanceAPIGeneratorConfigBuilder<{type.Name}Tables>();"); 33 | indentWriter.WriteLine("if (options is not null) { options(builder); }"); 34 | indentWriter.WriteLine("var config = builder.Build();"); 35 | indentWriter.WriteLine(); 36 | 37 | foreach (var table in tables) 38 | { 39 | if (!table.PropertyType.ContainingNamespace.Equals(type.ContainingNamespace, SymbolEqualityComparer.Default)) 40 | { 41 | namespaces.Add(table.PropertyType.ContainingNamespace); 42 | } 43 | 44 | var tableVariableName = $"table{table.Name}"; 45 | 46 | indentWriter.WriteLine($"var {tableVariableName} = config[{type.Name}Tables.{table.Name}];"); 47 | indentWriter.WriteLine(); 48 | indentWriter.WriteLine($"if ({tableVariableName}.Included == Included.Yes)"); 49 | indentWriter.WriteLine("{"); 50 | indentWriter.Indent++; 51 | 52 | BuildGet(indentWriter, type, table, tableVariableName); 53 | indentWriter.WriteLine(); 54 | 55 | if (table.IdType is not null) 56 | { 57 | BuildGetById(indentWriter, type, table, tableVariableName); 58 | indentWriter.WriteLine(); 59 | BuildPost(indentWriter, type, table, tableVariableName); 60 | indentWriter.WriteLine(); 61 | } 62 | 63 | BuildPut(indentWriter, type, table, tableVariableName); 64 | 65 | if (table.IdType is not null) 66 | { 67 | indentWriter.WriteLine(); 68 | BuildDeleteById(indentWriter, type, table, tableVariableName); 69 | } 70 | 71 | indentWriter.Indent--; 72 | indentWriter.WriteLine("}"); 73 | } 74 | 75 | indentWriter.WriteLine(); 76 | indentWriter.WriteLine("return app;"); 77 | indentWriter.Indent--; 78 | indentWriter.WriteLine("}"); 79 | 80 | indentWriter.Indent--; 81 | indentWriter.WriteLine("}"); 82 | } 83 | 84 | private static void BuildGet(IndentedTextWriter indentWriter, INamedTypeSymbol type, TableData table, string tableVariableName) 85 | { 86 | indentWriter.WriteLine($"if ({tableVariableName}.APIs.HasFlag(ApisToGenerate.Get))"); 87 | indentWriter.WriteLine("{"); 88 | indentWriter.Indent++; 89 | 90 | indentWriter.WriteLine($"var url = {tableVariableName}.RouteGet.Invoke({tableVariableName}.Name);"); 91 | indentWriter.WriteLine($"app.MapGet(url, ([FromServices] {type.Name} db) =>"); 92 | indentWriter.Indent++; 93 | indentWriter.WriteLine($"Results.Ok(db.{table.Name}));"); 94 | indentWriter.Indent--; 95 | indentWriter.WriteLine(); 96 | indentWriter.WriteLine("logger.LogInformation($\"Created API: HTTP GET\\t{url}\");"); 97 | 98 | indentWriter.Indent--; 99 | indentWriter.WriteLine("}"); 100 | } 101 | 102 | private static void BuildGetById(IndentedTextWriter indentWriter, INamedTypeSymbol type, TableData table, string tableVariableName) 103 | { 104 | indentWriter.WriteLine($"if ({tableVariableName}.APIs.HasFlag(ApisToGenerate.GetById))"); 105 | indentWriter.WriteLine("{"); 106 | indentWriter.Indent++; 107 | 108 | indentWriter.WriteLine($"var url = {tableVariableName}.RouteGetById.Invoke({tableVariableName}.Name);"); 109 | indentWriter.WriteLine($"app.MapGet(url, async ([FromServices] {type.Name} db, [FromRoute] string id) =>"); 110 | indentWriter.WriteLine("{"); 111 | indentWriter.Indent++; 112 | 113 | indentWriter.WriteLine($"var outValue = await db.{table.Name}.FindAsync({GetIdParseCode(table.IdType!)});"); 114 | indentWriter.WriteLine("if (outValue is null) { return Results.NotFound(); }"); 115 | indentWriter.WriteLine("return Results.Ok(outValue);"); 116 | 117 | indentWriter.Indent--; 118 | indentWriter.WriteLine("});"); 119 | 120 | indentWriter.WriteLine(); 121 | indentWriter.WriteLine("logger.LogInformation($\"Created API: HTTP GET\\t{url}\");"); 122 | 123 | indentWriter.Indent--; 124 | indentWriter.WriteLine("}"); 125 | } 126 | 127 | private static void BuildPost(IndentedTextWriter indentWriter, INamedTypeSymbol type, TableData table, string tableVariableName) 128 | { 129 | indentWriter.WriteLine($"if ({tableVariableName}.APIs.HasFlag(ApisToGenerate.Insert))"); 130 | indentWriter.WriteLine("{"); 131 | indentWriter.Indent++; 132 | 133 | indentWriter.WriteLine($"var url = {tableVariableName}.RoutePost.Invoke({tableVariableName}.Name);"); 134 | indentWriter.WriteLine($"app.MapPost(url, async ([FromServices] {type.Name} db, [FromBody] {table.PropertyType.Name} newObj) =>"); 135 | indentWriter.WriteLine("{"); 136 | indentWriter.Indent++; 137 | 138 | indentWriter.WriteLine("db.Add(newObj);"); 139 | indentWriter.WriteLine("await db.SaveChangesAsync();"); 140 | indentWriter.WriteLine($"var id = newObj.{table.IdName!};"); 141 | // TODO: We're assuming that the "created" route is the same as POST/id, 142 | // and this may not be true. 143 | indentWriter.WriteLine($"return Results.Created($\"{{url}}/{{id}}\", newObj);"); 144 | 145 | indentWriter.Indent--; 146 | indentWriter.WriteLine("});"); 147 | 148 | indentWriter.WriteLine(); 149 | indentWriter.WriteLine("logger.LogInformation($\"Created API: HTTP POST\\t{url}\");"); 150 | 151 | indentWriter.Indent--; 152 | indentWriter.WriteLine("}"); 153 | } 154 | 155 | private static void BuildPut(IndentedTextWriter indentWriter, INamedTypeSymbol type, TableData table, string tableVariableName) 156 | { 157 | indentWriter.WriteLine($"if ({tableVariableName}.APIs.HasFlag(ApisToGenerate.Update))"); 158 | indentWriter.WriteLine("{"); 159 | indentWriter.Indent++; 160 | 161 | indentWriter.WriteLine($"var url = {tableVariableName}.RoutePut.Invoke({tableVariableName}.Name);"); 162 | indentWriter.WriteLine($"app.MapPut(url, async ([FromServices] {type.Name} db, [FromRoute] string id, [FromBody] {table.PropertyType.Name} newObj) =>"); 163 | indentWriter.WriteLine("{"); 164 | indentWriter.Indent++; 165 | 166 | indentWriter.WriteLine($"db.{table.Name}.Attach(newObj);"); 167 | indentWriter.WriteLine("db.Entry(newObj).State = EntityState.Modified;"); 168 | indentWriter.WriteLine("await db.SaveChangesAsync();"); 169 | indentWriter.WriteLine("return Results.NoContent();"); 170 | 171 | indentWriter.Indent--; 172 | indentWriter.WriteLine("});"); 173 | 174 | indentWriter.WriteLine(); 175 | indentWriter.WriteLine("logger.LogInformation($\"Created API: HTTP PUT\\t{url}\");"); 176 | 177 | indentWriter.Indent--; 178 | indentWriter.WriteLine("}"); 179 | } 180 | 181 | private static void BuildDeleteById(IndentedTextWriter indentWriter, INamedTypeSymbol type, TableData table, string tableVariableName) 182 | { 183 | indentWriter.WriteLine($"if ({tableVariableName}.APIs.HasFlag(ApisToGenerate.Delete))"); 184 | indentWriter.WriteLine("{"); 185 | indentWriter.Indent++; 186 | 187 | indentWriter.WriteLine($"var url = {tableVariableName}.RouteDeleteById.Invoke({tableVariableName}.Name);"); 188 | indentWriter.WriteLine($"app.MapDelete(url, async ([FromServices] {type.Name} db, [FromRoute] string id) =>"); 189 | indentWriter.WriteLine("{"); 190 | indentWriter.Indent++; 191 | 192 | indentWriter.WriteLine($"{table.PropertyType.Name}? obj = await db.{table.Name}.FindAsync({GetIdParseCode(table.IdType!)});"); 193 | indentWriter.WriteLine(); 194 | indentWriter.WriteLine("if (obj is null) { return Results.NotFound(); }"); 195 | indentWriter.WriteLine(); 196 | indentWriter.WriteLine($"db.{table.Name}.Remove(obj);"); 197 | indentWriter.WriteLine("await db.SaveChangesAsync();"); 198 | indentWriter.WriteLine("return Results.NoContent();"); 199 | 200 | indentWriter.Indent--; 201 | indentWriter.WriteLine("});"); 202 | 203 | indentWriter.WriteLine(); 204 | indentWriter.WriteLine("logger.LogInformation($\"Created API: HTTP DELETE\\t{url}\");"); 205 | 206 | indentWriter.Indent--; 207 | indentWriter.WriteLine("}"); 208 | } 209 | 210 | private static string GetIdParseCode(INamedTypeSymbol tableType) 211 | { 212 | var idValue = "id"; 213 | 214 | if (tableType.SpecialType == SpecialType.System_Int32) 215 | { 216 | idValue = "int.Parse(id)"; 217 | } 218 | else if (tableType.SpecialType == SpecialType.System_Int64) 219 | { 220 | idValue = "long.Parse(id)"; 221 | } 222 | // TODO: This is not ideal for identifying a Guid...I think... 223 | else if (tableType.ToDisplayString() == "System.Guid") 224 | { 225 | idValue = "Guid.Parse(id)"; 226 | } 227 | 228 | return idValue; 229 | } 230 | } 231 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/Builders/TablesEnumBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.CodeDom.Compiler; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace Fritz.InstantAPIs.Generators.Builders 6 | { 7 | internal sealed class TablesEnumBuilder 8 | { 9 | internal static void Build(IndentedTextWriter indentWriter, string name, List tables) 10 | { 11 | indentWriter.WriteLine($"public enum {name}Tables"); 12 | indentWriter.WriteLine("{"); 13 | indentWriter.Indent++; 14 | indentWriter.WriteLine(string.Join(", ", tables.Select(_ => _.Name))); 15 | indentWriter.Indent--; 16 | indentWriter.WriteLine("}"); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/DbContextAPIGenerator.cs: -------------------------------------------------------------------------------- 1 | using Fritz.InstantAPIs.Generators.Builders; 2 | using Fritz.InstantAPIs.Generators.Diagnostics; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using System.Linq; 8 | using System.Threading; 9 | 10 | namespace Fritz.InstantAPIs.Generators; 11 | 12 | [Generator] 13 | public sealed class DbContextAPIGenerator 14 | : IIncrementalGenerator 15 | { 16 | public void Initialize(IncrementalGeneratorInitializationContext context) 17 | { 18 | static bool IsSyntaxTargetForGeneration(SyntaxNode node, CancellationToken token) => 19 | node is AttributeSyntax attributeNode && 20 | (attributeNode.Name.ToString() == "InstantAPIsForDbContext" || attributeNode.Name.ToString() == "InstantAPIsForDbContextAttribute"); 21 | 22 | static (AttributeSyntax, INamedTypeSymbol)? TransformTargets(GeneratorSyntaxContext context, CancellationToken token) 23 | { 24 | // We only want to return types with our attribute 25 | var node = (AttributeSyntax)context.Node; 26 | var model = context.SemanticModel; 27 | 28 | // AttributeSyntax maps to a IMethodSymbol (you're basically calling a constructor 29 | // when you declare an attribute on a member). 30 | var symbol = model.GetSymbolInfo(node, token).Symbol as IMethodSymbol; 31 | 32 | if (symbol is not null) 33 | { 34 | // Let's do a best guess that it's the attribute we're looking for. 35 | if(symbol.ContainingType.Name == "InstantAPIsForDbContextAttribute" && 36 | symbol.ContainingNamespace.ToDisplayString() == "Fritz.InstantAPIs.Generators.Helpers") 37 | { 38 | // Find the attribute data for the node. 39 | var attributeData = model.Compilation.Assembly.GetAttributes().SingleOrDefault( 40 | _ => _.ApplicationSyntaxReference!.GetSyntax() == node); 41 | 42 | if (attributeData is not null && 43 | attributeData.ConstructorArguments[0].Value is not null && 44 | attributeData.ConstructorArguments[0].Value is INamedTypeSymbol typeSymbol) 45 | { 46 | return (node, typeSymbol); 47 | } 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | var provider = context.SyntaxProvider 55 | .CreateSyntaxProvider(IsSyntaxTargetForGeneration, TransformTargets) 56 | .Where(static _ => _ is not null); 57 | var output = context.CompilationProvider.Combine(provider.Collect()); 58 | 59 | context.RegisterSourceOutput(output, 60 | (context, source) => CreateOutput(source.Right, context)); 61 | } 62 | 63 | private static void CreateOutput(ImmutableArray<(AttributeSyntax, INamedTypeSymbol)?> symbols, SourceProductionContext context) 64 | { 65 | static bool IsDbContext(INamedTypeSymbol type) 66 | { 67 | var baseType = type.BaseType; 68 | 69 | while (baseType is not null) 70 | { 71 | if (baseType.Name == "DbContext" && baseType.ContainingNamespace.ToDisplayString() == "Microsoft.EntityFrameworkCore") 72 | { 73 | return true; 74 | } 75 | 76 | baseType = baseType.BaseType; 77 | } 78 | 79 | return false; 80 | } 81 | 82 | var dbTypes = new HashSet(SymbolEqualityComparer.Default); 83 | 84 | foreach(var symbol in symbols) 85 | { 86 | var node = symbol!.Value.Item1; 87 | var typeSymbol = symbol!.Value.Item2; 88 | 89 | if (!IsDbContext(typeSymbol)) 90 | { 91 | context.ReportDiagnostic(NotADbContextDiagnostic.Create(typeSymbol, node)); 92 | } 93 | else 94 | { 95 | if(!dbTypes.Add(typeSymbol)) 96 | { 97 | context.ReportDiagnostic(DuplicateDefinitionDiagnostic.Create(node)); 98 | } 99 | else 100 | { 101 | var text = DbContextAPIBuilder.Build(typeSymbol); 102 | 103 | if (text is not null) 104 | { 105 | context.AddSource($"{typeSymbol.Name}_DbContextAPIGenerator.g.cs", text); 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/Diagnostics/DescriptorConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Fritz.InstantAPIs.Generators.Diagnostics; 2 | 3 | public static class DescriptorConstants 4 | { 5 | public const string Usage = nameof(DescriptorConstants.Usage); 6 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/Diagnostics/DuplicateDefinitionDiagnostic.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace Fritz.InstantAPIs.Generators.Diagnostics; 4 | 5 | public class DuplicateDefinitionDiagnostic 6 | { 7 | public static Diagnostic Create(SyntaxNode currentNode) => 8 | Diagnostic.Create(new DiagnosticDescriptor( 9 | DuplicateDefinitionDiagnostic.Id, DuplicateDefinitionDiagnostic.Title, 10 | DuplicateDefinitionDiagnostic.Message, DescriptorConstants.Usage, DiagnosticSeverity.Warning, true, 11 | helpLinkUri: HelpUrlBuilder.Build( 12 | DuplicateDefinitionDiagnostic.Id, DuplicateDefinitionDiagnostic.Title)), 13 | currentNode.GetLocation()); 14 | 15 | public const string Id = "IA2"; 16 | public const string Message = "The given DbContext has already been defined."; 17 | public const string Title = "Duplicate DbContext Definition"; 18 | } 19 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/Diagnostics/NotADbContextDiagnostic.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Globalization; 3 | 4 | namespace Fritz.InstantAPIs.Generators.Diagnostics; 5 | 6 | public static class NotADbContextDiagnostic 7 | { 8 | public static Diagnostic Create(INamedTypeSymbol type, SyntaxNode attribute) => 9 | Diagnostic.Create(new DiagnosticDescriptor( 10 | NotADbContextDiagnostic.Id, NotADbContextDiagnostic.Title, 11 | string.Format(CultureInfo.CurrentCulture, NotADbContextDiagnostic.Message, type.Name), 12 | DescriptorConstants.Usage, DiagnosticSeverity.Error, true, 13 | helpLinkUri: HelpUrlBuilder.Build( 14 | NotADbContextDiagnostic.Id, NotADbContextDiagnostic.Title)), 15 | attribute.GetLocation()); 16 | 17 | public const string Id = "IA1"; 18 | public const string Message = "The given type, {0}, does not derive from DbContext."; 19 | public const string Title = "Not a DbContext"; 20 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/Fritz.InstantAPIs.Generators.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | latest 4 | enable 5 | netstandard2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/HelpUrlBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace Fritz.InstantAPIs.Generators; 2 | 3 | internal static class HelpUrlBuilder 4 | { 5 | internal static string Build(string identifier, string title) => 6 | $"https://github.com/csharpfritz/InstantAPIs/tree/main/docs/{identifier}-{title}.md"; 7 | } -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/NamespaceGatherer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | 6 | namespace Fritz.InstantAPIs.Generators; 7 | 8 | internal sealed class NamespaceGatherer 9 | { 10 | private readonly ImmutableHashSet.Builder builder = 11 | ImmutableHashSet.CreateBuilder(); 12 | 13 | public void Add(INamespaceSymbol @namespace) 14 | { 15 | if (!@namespace.IsGlobalNamespace) 16 | { 17 | this.builder.Add(@namespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); 18 | } 19 | } 20 | 21 | public void Add(string @namespace) => 22 | this.builder.Add(@namespace); 23 | 24 | public void Add(Type type) 25 | { 26 | if (!string.IsNullOrWhiteSpace(type.Namespace)) 27 | { 28 | this.builder.Add(type.Namespace); 29 | } 30 | } 31 | 32 | public void AddRange(IEnumerable namespaces) 33 | { 34 | foreach (var @namespace in namespaces) 35 | { 36 | this.Add(@namespace); 37 | } 38 | } 39 | 40 | public IImmutableSet Values => this.builder.ToImmutableSortedSet(); 41 | } 42 | -------------------------------------------------------------------------------- /Fritz.InstantAPIs.Generators/TableData.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace Fritz.InstantAPIs.Generators; 4 | 5 | internal sealed class TableData 6 | { 7 | internal TableData(string name, INamedTypeSymbol propertyType, INamedTypeSymbol? idType, string? idName) => 8 | (Name, PropertyType, IdType, IdName) = (name, propertyType, idType, idName); 9 | 10 | public INamedTypeSymbol PropertyType { get; } 11 | public string? IdName { get; } 12 | public INamedTypeSymbol? IdType { get; } 13 | internal string Name { get; } 14 | } 15 | -------------------------------------------------------------------------------- /InstantAPIs.Generators.Helpers.Tests/InstanceAPIGeneratorConfigBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace InstantAPIs.Generators.Helpers.Tests; 5 | 6 | public static class InstanceAPIGeneratorConfigBuilderTests 7 | { 8 | [Fact] 9 | public static void BuildWithNoConfiguration() 10 | { 11 | var builder = new InstanceAPIGeneratorConfigBuilder(); 12 | var config = builder.Build(); 13 | 14 | foreach (var key in Enum.GetValues()) 15 | { 16 | var tableConfig = config[key]; 17 | 18 | Assert.Equal(key, tableConfig.Key); 19 | Assert.Equal(key.ToString(), tableConfig.Name); 20 | Assert.Equal(Included.Yes, tableConfig.Included); 21 | Assert.Equal(ApisToGenerate.All, tableConfig.APIs); 22 | Assert.Equal("/api/a/{id}", tableConfig.RouteDeleteById("a")); 23 | Assert.Equal("/api/a", tableConfig.RouteGet("a")); 24 | Assert.Equal("/api/a/{id}", tableConfig.RouteGetById("a")); 25 | Assert.Equal("/api/a", tableConfig.RoutePost("a")); 26 | Assert.Equal("/api/a/{id}", tableConfig.RoutePut("a")); 27 | } 28 | } 29 | 30 | [Fact] 31 | public static void BuildWithCustomInclude() 32 | { 33 | var builder = new InstanceAPIGeneratorConfigBuilder(); 34 | builder.Include(Values.Two, "a", ApisToGenerate.Get, 35 | routeGet: value => $"get/{value}", 36 | routeGetById: value => $"getById/{value}", 37 | routePost: value => $"post/{value}", 38 | routePut: value => $"put/{value}", 39 | routeDeleteById: value => $"delete/{value}"); 40 | var config = builder.Build(); 41 | 42 | foreach (var key in Enum.GetValues()) 43 | { 44 | var tableConfig = config[key]; 45 | 46 | if (key != Values.Two) 47 | { 48 | Assert.Equal(key, tableConfig.Key); 49 | Assert.Equal(key.ToString(), tableConfig.Name); 50 | Assert.Equal(Included.Yes, tableConfig.Included); 51 | Assert.Equal(ApisToGenerate.All, tableConfig.APIs); 52 | Assert.Equal("/api/a/{id}", tableConfig.RouteDeleteById("a")); 53 | Assert.Equal("/api/a", tableConfig.RouteGet("a")); 54 | Assert.Equal("/api/a/{id}", tableConfig.RouteGetById("a")); 55 | Assert.Equal("/api/a", tableConfig.RoutePost("a")); 56 | Assert.Equal("/api/a/{id}", tableConfig.RoutePut("a")); 57 | } 58 | else 59 | { 60 | Assert.Equal(Values.Two, tableConfig.Key); 61 | Assert.Equal("a", tableConfig.Name); 62 | Assert.Equal(Included.Yes, tableConfig.Included); 63 | Assert.Equal(ApisToGenerate.Get, tableConfig.APIs); 64 | Assert.Equal("delete/a", tableConfig.RouteDeleteById("a")); 65 | Assert.Equal("get/a", tableConfig.RouteGet("a")); 66 | Assert.Equal("getById/a", tableConfig.RouteGetById("a")); 67 | Assert.Equal("post/a", tableConfig.RoutePost("a")); 68 | Assert.Equal("put/a", tableConfig.RoutePut("a")); 69 | } 70 | } 71 | } 72 | 73 | [Fact] 74 | public static void BuildWithCustomExclude() 75 | { 76 | var builder = new InstanceAPIGeneratorConfigBuilder(); 77 | builder.Exclude(Values.Two); 78 | var config = builder.Build(); 79 | 80 | foreach (var key in Enum.GetValues()) 81 | { 82 | var tableConfig = config[key]; 83 | 84 | Assert.Equal(key, tableConfig.Key); 85 | Assert.Equal(key.ToString(), tableConfig.Name); 86 | Assert.Equal(key != Values.Two ? Included.Yes : Included.No, tableConfig.Included); 87 | Assert.Equal(ApisToGenerate.All, tableConfig.APIs); 88 | Assert.Equal("/api/a/{id}", tableConfig.RouteDeleteById("a")); 89 | Assert.Equal("/api/a", tableConfig.RouteGet("a")); 90 | Assert.Equal("/api/a/{id}", tableConfig.RouteGetById("a")); 91 | Assert.Equal("/api/a", tableConfig.RoutePost("a")); 92 | Assert.Equal("/api/a/{id}", tableConfig.RoutePut("a")); 93 | } 94 | } 95 | 96 | private enum Values 97 | { 98 | One, Two, Three 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /InstantAPIs.Generators.Helpers.Tests/InstantAPIs.Generators.Helpers.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | enable 5 | 6 | 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /InstantAPIs.Generators.Helpers.Tests/TableConfigTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace InstantAPIs.Generators.Helpers.Tests 4 | { 5 | public static class TableConfigTests 6 | { 7 | [Fact] 8 | public static void Create() 9 | { 10 | var config = new TableConfig(Values.Three); 11 | 12 | Assert.Equal(Values.Three, config.Key); 13 | Assert.Equal("Three", config.Name); 14 | Assert.Equal(Included.Yes, config.Included); 15 | Assert.Equal(ApisToGenerate.All, config.APIs); 16 | Assert.Equal("/api/a/{id}", config.RouteDeleteById("a")); 17 | Assert.Equal("/api/a", config.RouteGet("a")); 18 | Assert.Equal("/api/a/{id}", config.RouteGetById("a")); 19 | Assert.Equal("/api/a", config.RoutePost("a")); 20 | Assert.Equal("/api/a/{id}", config.RoutePut("a")); 21 | } 22 | 23 | [Fact] 24 | public static void CreateWithCustomization() 25 | { 26 | var config = new TableConfig(Values.Three, 27 | Included.No, "a", ApisToGenerate.Get, 28 | routeGet: value => $"get/{value}", 29 | routeGetById: value => $"getById/{value}", 30 | routePost: value => $"post/{value}", 31 | routePut: value => $"put/{value}", 32 | routeDeleteById: value => $"delete/{value}"); 33 | 34 | Assert.Equal(Values.Three, config.Key); 35 | Assert.Equal("a", config.Name); 36 | Assert.Equal(Included.No, config.Included); 37 | Assert.Equal(ApisToGenerate.Get, config.APIs); 38 | Assert.Equal("delete/a", config.RouteDeleteById("a")); 39 | Assert.Equal("get/a", config.RouteGet("a")); 40 | Assert.Equal("getById/a", config.RouteGetById("a")); 41 | Assert.Equal("post/a", config.RoutePost("a")); 42 | Assert.Equal("put/a", config.RoutePut("a")); 43 | } 44 | 45 | private enum Values 46 | { 47 | One, Two, Three 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /InstantAPIs.Generators.Helpers/ApisToGenerate.cs: -------------------------------------------------------------------------------- 1 | namespace InstantAPIs.Generators.Helpers 2 | { 3 | [Flags] 4 | public enum ApisToGenerate 5 | { 6 | Get = 1, 7 | GetById = 2, 8 | Insert = 4, 9 | Update = 8, 10 | Delete = 16, 11 | All = 31 12 | } 13 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators.Helpers/Included.cs: -------------------------------------------------------------------------------- 1 | namespace InstantAPIs.Generators.Helpers 2 | { 3 | public enum Included 4 | { 5 | Yes, No 6 | } 7 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators.Helpers/InstanceAPIGeneratorConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace InstantAPIs.Generators.Helpers 4 | { 5 | public class InstanceAPIGeneratorConfig 6 | where T : struct, Enum 7 | { 8 | private readonly ImmutableDictionary> _tablesConfig; 9 | 10 | internal InstanceAPIGeneratorConfig(ImmutableDictionary> tablesConfig) 11 | { 12 | _tablesConfig = tablesConfig ?? throw new ArgumentNullException(nameof(tablesConfig)); 13 | } 14 | 15 | public virtual TableConfig this[T key] => _tablesConfig[key]; 16 | } 17 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators.Helpers/InstanceAPIGeneratorConfigBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | 3 | namespace InstantAPIs.Generators.Helpers 4 | { 5 | public sealed class InstanceAPIGeneratorConfigBuilder 6 | where T : struct, Enum 7 | { 8 | private readonly Dictionary> _tablesConfig = new(); 9 | 10 | public InstanceAPIGeneratorConfigBuilder() 11 | { 12 | foreach(var key in Enum.GetValues()) 13 | { 14 | _tablesConfig.Add(key, new TableConfig(key)); 15 | } 16 | } 17 | 18 | public InstanceAPIGeneratorConfigBuilder Include(T key, string? name = null, ApisToGenerate apis = ApisToGenerate.All, 19 | Func? routeGet = null, Func? routeGetById = null, 20 | Func? routePost = null, Func? routePut = null, 21 | Func? routeDeleteById = null) 22 | { 23 | _tablesConfig[key] = new TableConfig(key, Included.Yes, name: name, 24 | apis: apis, routeGet: routeGet, routeGetById: routeGetById, 25 | routePost: routePost, routePut: routePut, routeDeleteById: routeDeleteById); 26 | return this; 27 | } 28 | 29 | public InstanceAPIGeneratorConfigBuilder Exclude(T key) 30 | { 31 | _tablesConfig[key] = new TableConfig(key, Included.No); 32 | return this; 33 | } 34 | 35 | public InstanceAPIGeneratorConfig Build() => 36 | new InstanceAPIGeneratorConfig(_tablesConfig.ToImmutableDictionary()); 37 | } 38 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators.Helpers/InstantAPIs.Generators.Helpers.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /InstantAPIs.Generators.Helpers/InstantAPIsForDbContextAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace InstantAPIs.Generators.Helpers 2 | { 3 | [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true)] 4 | public sealed class InstantAPIsForDbContextAttribute 5 | : Attribute 6 | { 7 | public InstantAPIsForDbContextAttribute(Type dbContextType) => 8 | DbContextType = dbContextType ?? throw new ArgumentNullException(nameof(dbContextType)); 9 | 10 | public Type DbContextType { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators.Helpers/TableConfig.cs: -------------------------------------------------------------------------------- 1 | namespace InstantAPIs.Generators.Helpers 2 | { 3 | public sealed class TableConfig 4 | where T : struct, Enum 5 | { 6 | public TableConfig(T key) 7 | { 8 | Key = key; 9 | Name = Enum.GetName(key); 10 | } 11 | 12 | public TableConfig(T key, Included included, string? name = null, ApisToGenerate apis = ApisToGenerate.All, 13 | Func? routeGet = null, Func? routeGetById = null, 14 | Func? routePost = null, Func? routePut = null, 15 | Func? routeDeleteById = null) 16 | : this(key) 17 | { 18 | Included = included; 19 | APIs = apis; 20 | if (!string.IsNullOrWhiteSpace(name)) { Name = name; } 21 | if (routeGet is not null) { RouteGet = routeGet; } 22 | if (routeGetById is not null) { RouteGetById = routeGetById; } 23 | if (routePost is not null) { RoutePost = routePost; } 24 | if (routePut is not null) { RoutePut = routePut; } 25 | if (routeDeleteById is not null) { RouteDeleteById = routeDeleteById; } 26 | } 27 | 28 | public T Key { get; } 29 | 30 | public string? Name { get; } = null; 31 | 32 | public Included Included { get; } = Included.Yes; 33 | 34 | public ApisToGenerate APIs { get; } = ApisToGenerate.All; 35 | 36 | public Func RouteDeleteById { get; } = value => $"/api/{value}/{{id}}"; 37 | 38 | public Func RouteGet { get; } = value => $"/api/{value}"; 39 | 40 | public Func RouteGetById { get; } = value => $"/api/{value}/{{id}}"; 41 | 42 | public Func RoutePost { get; } = value => $"/api/{value}"; 43 | 44 | public Func RoutePut { get; } = value => $"/api/{value}/{{id}}"; 45 | } 46 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators.Tests/CSharpIncrementalSourceGeneratorVerifier.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis.CSharp.Testing; 2 | using Microsoft.CodeAnalysis.CSharp; 3 | using Microsoft.CodeAnalysis.Testing.Verifiers; 4 | using Microsoft.CodeAnalysis.Testing; 5 | using Microsoft.CodeAnalysis; 6 | using System.Collections.Immutable; 7 | using System; 8 | using System.Collections.Generic; 9 | 10 | namespace InstantAPIs.Generators.Tests; 11 | 12 | // All of this code was grabbed from Refit 13 | // (https://github.com/reactiveui/refit/pull/1216/files) 14 | // based on a suggestion from 15 | // sharwell - https://discord.com/channels/732297728826277939/732297994699014164/910258213532876861 16 | // If the .NET Roslyn testing packages get updated to have something like this in the future 17 | // I'll remove these helpers. 18 | public static partial class CSharpIncrementalSourceGeneratorVerifier 19 | where TIncrementalGenerator : IIncrementalGenerator, new() 20 | { 21 | #pragma warning disable CA1034 // Nested types should not be visible 22 | public class Test : CSharpSourceGeneratorTest 23 | #pragma warning restore CA1034 // Nested types should not be visible 24 | { 25 | public Test() => 26 | this.SolutionTransforms.Add((solution, projectId) => 27 | { 28 | if (solution is null) 29 | { 30 | throw new ArgumentNullException(nameof(solution)); 31 | } 32 | 33 | if (projectId is null) 34 | { 35 | throw new ArgumentNullException(nameof(projectId)); 36 | } 37 | 38 | var compilationOptions = solution.GetProject(projectId)!.CompilationOptions!; 39 | 40 | // NOTE: I commented this out, because I kept getting this error: 41 | // error CS8632: The annotation for nullable reference types should only be used in code within a '#nullable' annotations context. 42 | // Which makes NO sense because I have "#nullable enable" emitted in my 43 | // generated code. So, best to just remove this for now. 44 | 45 | //compilationOptions = compilationOptions.WithSpecificDiagnosticOptions( 46 | // compilationOptions.SpecificDiagnosticOptions.SetItems(CSharpVerifierHelper.NullableWarnings)); 47 | 48 | solution = solution.WithProjectCompilationOptions(projectId, compilationOptions); 49 | 50 | return solution; 51 | }); 52 | 53 | protected override IEnumerable GetSourceGenerators() 54 | { 55 | yield return new TIncrementalGenerator().AsSourceGenerator(); 56 | } 57 | 58 | protected override ParseOptions CreateParseOptions() 59 | { 60 | var parseOptions = (CSharpParseOptions)base.CreateParseOptions(); 61 | return parseOptions.WithLanguageVersion(LanguageVersion.Preview); 62 | } 63 | } 64 | 65 | static class CSharpVerifierHelper 66 | { 67 | /// 68 | /// By default, the compiler reports diagnostics for nullable reference types at 69 | /// , and the analyzer test framework defaults to only validating 70 | /// diagnostics at . This map contains all compiler diagnostic IDs 71 | /// related to nullability mapped to , which is then used to enable all 72 | /// of these warnings for default validation during analyzer and code fix tests. 73 | /// 74 | internal static ImmutableDictionary NullableWarnings { get; } = GetNullableWarningsFromCompiler(); 75 | 76 | static ImmutableDictionary GetNullableWarningsFromCompiler() 77 | { 78 | string[] args = { "/warnaserror:nullable" }; 79 | var commandLineArguments = CSharpCommandLineParser.Default.Parse( 80 | args, baseDirectory: Environment.CurrentDirectory, sdkDirectory: Environment.CurrentDirectory); 81 | return commandLineArguments.CompilationOptions.SpecificDiagnosticOptions; 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /InstantAPIs.Generators.Tests/Diagnostics/DuplicateDefinitionDiagnosticTests.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs.Generators.Diagnostics; 2 | using Microsoft.CodeAnalysis; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Xunit; 5 | 6 | namespace InstantAPIs.Generators.Tests.Diagnostics; 7 | 8 | public static class DuplicateDefinitionDiagnosticTests 9 | { 10 | [Fact] 11 | public static void Create() 12 | { 13 | var diagnostic = DuplicateDefinitionDiagnostic.Create(SyntaxFactory.Attribute(SyntaxFactory.ParseName("A"))); 14 | 15 | Assert.Equal(DuplicateDefinitionDiagnostic.Message, diagnostic.GetMessage()); 16 | Assert.Equal(DuplicateDefinitionDiagnostic.Title, diagnostic.Descriptor.Title); 17 | Assert.Equal(DuplicateDefinitionDiagnostic.Id, diagnostic.Id); 18 | Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity); 19 | Assert.Equal(DescriptorConstants.Usage, diagnostic.Descriptor.Category); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /InstantAPIs.Generators.Tests/Diagnostics/NotADbContextDiagnosticTests.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs.Generators.Diagnostics; 2 | using Microsoft.CodeAnalysis.CSharp.Syntax; 3 | using Microsoft.CodeAnalysis.CSharp; 4 | using Microsoft.CodeAnalysis; 5 | using System; 6 | using Xunit; 7 | using System.Linq; 8 | 9 | namespace InstantAPIs.Generators.Tests.Diagnostics; 10 | 11 | public static class NotADbContextDiagnosticTests 12 | { 13 | [Fact] 14 | public static void Create() 15 | { 16 | var syntaxTree = CSharpSyntaxTree.ParseText("public class A { }"); 17 | var typeSyntax = syntaxTree.GetRoot().DescendantNodes(_ => true).OfType().Single(); 18 | var references = AppDomain.CurrentDomain.GetAssemblies() 19 | .Where(_ => !_.IsDynamic && !string.IsNullOrWhiteSpace(_.Location)) 20 | .Select(_ => MetadataReference.CreateFromFile(_.Location)); 21 | var compilation = CSharpCompilation.Create("generator", new SyntaxTree[] { syntaxTree }, 22 | references, new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)); 23 | var model = compilation.GetSemanticModel(syntaxTree, true); 24 | 25 | var typeSymbol = model.GetDeclaredSymbol(typeSyntax)!; 26 | 27 | var diagnostic = NotADbContextDiagnostic.Create(typeSymbol, typeSyntax); 28 | 29 | Assert.Equal("The given type, A, does not derive from DbContext.", diagnostic.GetMessage()); 30 | Assert.Equal(NotADbContextDiagnostic.Title, diagnostic.Descriptor.Title); 31 | Assert.Equal(NotADbContextDiagnostic.Id, diagnostic.Id); 32 | Assert.Equal(DiagnosticSeverity.Error, diagnostic.Severity); 33 | Assert.Equal(DescriptorConstants.Usage, diagnostic.Descriptor.Category); 34 | } 35 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators.Tests/InstantAPIs.Generators.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | enable 5 | 6 | 7 | 8 | 9 | NU1608 10 | 11 | 12 | NU1608 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /InstantAPIs.Generators.Tests/TestAssistants.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs.Generators.Helpers; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Routing; 6 | using Microsoft.CodeAnalysis.Testing; 7 | using Microsoft.EntityFrameworkCore; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Hosting; 10 | using Microsoft.Extensions.Logging; 11 | using Microsoft.Extensions.Logging.Abstractions; 12 | using System; 13 | using System.Collections.Generic; 14 | using System.ComponentModel.DataAnnotations; 15 | using System.Reflection; 16 | using System.Threading.Tasks; 17 | 18 | namespace InstantAPIs.Generators.Tests; 19 | 20 | using GeneratorTest = CSharpIncrementalSourceGeneratorVerifier; 21 | 22 | internal static class TestAssistants 23 | { 24 | internal static async Task RunAsync(string code, 25 | IEnumerable<(Type, string, string)> generatedSources, 26 | IEnumerable expectedDiagnostics) 27 | { 28 | var test = new GeneratorTest.Test 29 | { 30 | ReferenceAssemblies = ReferenceAssemblies.Net.Net60, 31 | TestState = 32 | { 33 | Sources = { code }, 34 | }, 35 | }; 36 | 37 | foreach (var generatedSource in generatedSources) 38 | { 39 | test.TestState.GeneratedSources.Add(generatedSource); 40 | } 41 | 42 | var referencedAssemblies = new HashSet 43 | { 44 | typeof(DbContextAPIGenerator).Assembly, 45 | typeof(DbContext).Assembly, 46 | typeof(WebApplication).Assembly, 47 | typeof(FromServicesAttribute).Assembly, 48 | typeof(EndpointRouteBuilderExtensions).Assembly, 49 | typeof(IApplicationBuilder).Assembly, 50 | typeof(IHost).Assembly, 51 | typeof(KeyAttribute).Assembly, 52 | typeof(Included).Assembly, 53 | typeof(IEndpointRouteBuilder).Assembly, 54 | typeof(RouteData).Assembly, 55 | typeof(Results).Assembly, 56 | typeof(NullLogger).Assembly, 57 | typeof(ILogger).Assembly, 58 | typeof(ServiceProviderServiceExtensions).Assembly, 59 | //typeof(IServiceProvider).Assembly 60 | }; 61 | 62 | foreach(var referencedAssembly in referencedAssemblies) 63 | { 64 | test.TestState.AdditionalReferences.Add(referencedAssembly); 65 | } 66 | 67 | test.TestState.ExpectedDiagnostics.AddRange(expectedDiagnostics); 68 | await test.RunAsync().ConfigureAwait(false); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /InstantAPIs.Generators/Builders/DbContextAPIBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using Microsoft.CodeAnalysis.Text; 3 | using System; 4 | using System.CodeDom.Compiler; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Text; 9 | 10 | namespace InstantAPIs.Generators.Builders 11 | { 12 | public static class DbContextAPIBuilder 13 | { 14 | public static SourceText? Build(INamedTypeSymbol type) 15 | { 16 | var tables = new List(); 17 | 18 | foreach(var property in type.GetMembers().OfType() 19 | .Where(_ => !_.IsStatic && _.DeclaredAccessibility == Accessibility.Public && 20 | _.Type.ToDisplayString().StartsWith("Microsoft.EntityFrameworkCore.DbSet"))) 21 | { 22 | var propertyType = (INamedTypeSymbol)property.Type; 23 | var propertySetType = (INamedTypeSymbol)propertyType.TypeArguments.First()!; 24 | 25 | var idProperty = propertySetType.GetMembers().OfType() 26 | .FirstOrDefault(_ => string.Equals(_.Name, "id", StringComparison.OrdinalIgnoreCase) && 27 | !_.IsStatic && _.DeclaredAccessibility == Accessibility.Public); 28 | 29 | if (idProperty is null) 30 | { 31 | idProperty = propertySetType.GetMembers().OfType() 32 | .FirstOrDefault(_ => _.GetAttributes().Any(_ => _.AttributeClass!.Name == "Key" || _.AttributeClass.Name == "KeyAttribute")); 33 | } 34 | 35 | tables.Add(new TableData(property.Name, propertySetType, idProperty?.Type as INamedTypeSymbol, idProperty?.Name)); 36 | } 37 | 38 | if(tables.Count > 0) 39 | { 40 | using var writer = new StringWriter(); 41 | using var indentWriter = new IndentedTextWriter(writer, "\t"); 42 | 43 | var namespaces = new NamespaceGatherer(); 44 | namespaces.Add("System"); 45 | namespaces.Add("System.Collections.Generic"); 46 | namespaces.Add("InstantAPIs.Generators.Helpers"); 47 | namespaces.Add("Microsoft.EntityFrameworkCore"); 48 | namespaces.Add("Microsoft.Extensions.Logging"); 49 | namespaces.Add("Microsoft.Extensions.Logging.Abstractions"); 50 | namespaces.Add("Microsoft.AspNetCore.Builder"); 51 | namespaces.Add("Microsoft.AspNetCore.Mvc"); 52 | namespaces.Add("Microsoft.AspNetCore.Routing"); 53 | namespaces.Add("Microsoft.AspNetCore.Http"); 54 | namespaces.Add("Microsoft.Extensions.DependencyInjection"); 55 | 56 | if (!type.ContainingNamespace.IsGlobalNamespace) 57 | { 58 | indentWriter.WriteLine($"namespace {type.ContainingNamespace.ToDisplayString()}"); 59 | indentWriter.WriteLine("{"); 60 | indentWriter.Indent++; 61 | } 62 | 63 | TablesEnumBuilder.Build(indentWriter, type.Name, tables); 64 | indentWriter.WriteLine(); 65 | IEndpointRouteBuilderExtensionsBuilder.Build(indentWriter, type, tables, namespaces); 66 | 67 | if (!type.ContainingNamespace.IsGlobalNamespace) 68 | { 69 | indentWriter.Indent--; 70 | indentWriter.WriteLine("}"); 71 | } 72 | 73 | var code = namespaces.Values.Count > 0 ? 74 | string.Join(Environment.NewLine, 75 | string.Join(Environment.NewLine, namespaces.Values.Select(_ => $"using {_};")), 76 | string.Empty, "#nullable enable", string.Empty, writer.ToString()) : 77 | string.Join(Environment.NewLine, "#nullable enable", string.Empty, writer.ToString()); 78 | 79 | return SourceText.From(code, Encoding.UTF8); 80 | } 81 | 82 | return null; 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /InstantAPIs.Generators/Builders/IEndpointRouteBuilderExtensionsBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.CodeDom.Compiler; 3 | using System.Collections.Generic; 4 | 5 | namespace InstantAPIs.Generators.Builders 6 | { 7 | internal static class IEndpointRouteBuilderExtensionsBuilder 8 | { 9 | internal static void Build(IndentedTextWriter indentWriter, INamedTypeSymbol type, List tables, 10 | NamespaceGatherer namespaces) 11 | { 12 | indentWriter.WriteLine("public static partial class IEndpointRouteBuilderExtensions"); 13 | indentWriter.WriteLine("{"); 14 | indentWriter.Indent++; 15 | 16 | indentWriter.WriteLine($"public static IEndpointRouteBuilder Map{type.Name}ToAPIs(this IEndpointRouteBuilder app, Action>? options = null)"); 17 | indentWriter.WriteLine("{"); 18 | indentWriter.Indent++; 19 | 20 | indentWriter.WriteLine("ILogger logger = NullLogger.Instance;"); 21 | indentWriter.WriteLine("if (app.ServiceProvider is not null)"); 22 | indentWriter.WriteLine("{"); 23 | indentWriter.Indent++; 24 | 25 | indentWriter.WriteLine("var loggerFactory = app.ServiceProvider.GetRequiredService();"); 26 | indentWriter.WriteLine("logger = loggerFactory.CreateLogger(\"InstantAPIs\");"); 27 | 28 | indentWriter.Indent--; 29 | indentWriter.WriteLine("}"); 30 | indentWriter.WriteLine(); 31 | 32 | indentWriter.WriteLine($"var builder = new InstanceAPIGeneratorConfigBuilder<{type.Name}Tables>();"); 33 | indentWriter.WriteLine("if (options is not null) { options(builder); }"); 34 | indentWriter.WriteLine("var config = builder.Build();"); 35 | indentWriter.WriteLine(); 36 | 37 | foreach (var table in tables) 38 | { 39 | if (!table.PropertyType.ContainingNamespace.Equals(type.ContainingNamespace, SymbolEqualityComparer.Default)) 40 | { 41 | namespaces.Add(table.PropertyType.ContainingNamespace); 42 | } 43 | 44 | var tableVariableName = $"table{table.Name}"; 45 | 46 | indentWriter.WriteLine($"var {tableVariableName} = config[{type.Name}Tables.{table.Name}];"); 47 | indentWriter.WriteLine(); 48 | indentWriter.WriteLine($"if ({tableVariableName}.Included == Included.Yes)"); 49 | indentWriter.WriteLine("{"); 50 | indentWriter.Indent++; 51 | 52 | BuildGet(indentWriter, type, table, tableVariableName); 53 | indentWriter.WriteLine(); 54 | 55 | if (table.IdType is not null) 56 | { 57 | BuildGetById(indentWriter, type, table, tableVariableName); 58 | indentWriter.WriteLine(); 59 | BuildPost(indentWriter, type, table, tableVariableName); 60 | indentWriter.WriteLine(); 61 | } 62 | 63 | BuildPut(indentWriter, type, table, tableVariableName); 64 | 65 | if (table.IdType is not null) 66 | { 67 | indentWriter.WriteLine(); 68 | BuildDeleteById(indentWriter, type, table, tableVariableName); 69 | } 70 | 71 | indentWriter.Indent--; 72 | indentWriter.WriteLine("}"); 73 | } 74 | 75 | indentWriter.WriteLine(); 76 | indentWriter.WriteLine("return app;"); 77 | indentWriter.Indent--; 78 | indentWriter.WriteLine("}"); 79 | 80 | indentWriter.Indent--; 81 | indentWriter.WriteLine("}"); 82 | } 83 | 84 | private static void BuildGet(IndentedTextWriter indentWriter, INamedTypeSymbol type, TableData table, string tableVariableName) 85 | { 86 | indentWriter.WriteLine($"if ({tableVariableName}.APIs.HasFlag(ApisToGenerate.Get))"); 87 | indentWriter.WriteLine("{"); 88 | indentWriter.Indent++; 89 | 90 | indentWriter.WriteLine($"var url = {tableVariableName}.RouteGet.Invoke({tableVariableName}.Name);"); 91 | indentWriter.WriteLine($"app.MapGet(url, ([FromServices] {type.Name} db) =>"); 92 | indentWriter.Indent++; 93 | indentWriter.WriteLine($"Results.Ok(db.{table.Name}));"); 94 | indentWriter.Indent--; 95 | indentWriter.WriteLine(); 96 | indentWriter.WriteLine("logger.LogInformation($\"Created API: HTTP GET\\t{url}\");"); 97 | 98 | indentWriter.Indent--; 99 | indentWriter.WriteLine("}"); 100 | } 101 | 102 | private static void BuildGetById(IndentedTextWriter indentWriter, INamedTypeSymbol type, TableData table, string tableVariableName) 103 | { 104 | indentWriter.WriteLine($"if ({tableVariableName}.APIs.HasFlag(ApisToGenerate.GetById))"); 105 | indentWriter.WriteLine("{"); 106 | indentWriter.Indent++; 107 | 108 | indentWriter.WriteLine($"var url = {tableVariableName}.RouteGetById.Invoke({tableVariableName}.Name);"); 109 | indentWriter.WriteLine($"app.MapGet(url, async ([FromServices] {type.Name} db, [FromRoute] string id) =>"); 110 | indentWriter.WriteLine("{"); 111 | indentWriter.Indent++; 112 | 113 | indentWriter.WriteLine($"var outValue = await db.{table.Name}.FindAsync({GetIdParseCode(table.IdType!)});"); 114 | indentWriter.WriteLine("if (outValue is null) { return Results.NotFound(); }"); 115 | indentWriter.WriteLine("return Results.Ok(outValue);"); 116 | 117 | indentWriter.Indent--; 118 | indentWriter.WriteLine("});"); 119 | 120 | indentWriter.WriteLine(); 121 | indentWriter.WriteLine("logger.LogInformation($\"Created API: HTTP GET\\t{url}\");"); 122 | 123 | indentWriter.Indent--; 124 | indentWriter.WriteLine("}"); 125 | } 126 | 127 | private static void BuildPost(IndentedTextWriter indentWriter, INamedTypeSymbol type, TableData table, string tableVariableName) 128 | { 129 | indentWriter.WriteLine($"if ({tableVariableName}.APIs.HasFlag(ApisToGenerate.Insert))"); 130 | indentWriter.WriteLine("{"); 131 | indentWriter.Indent++; 132 | 133 | indentWriter.WriteLine($"var url = {tableVariableName}.RoutePost.Invoke({tableVariableName}.Name);"); 134 | indentWriter.WriteLine($"app.MapPost(url, async ([FromServices] {type.Name} db, [FromBody] {table.PropertyType.Name} newObj) =>"); 135 | indentWriter.WriteLine("{"); 136 | indentWriter.Indent++; 137 | 138 | indentWriter.WriteLine("db.Add(newObj);"); 139 | indentWriter.WriteLine("await db.SaveChangesAsync();"); 140 | indentWriter.WriteLine($"var id = newObj.{table.IdName!};"); 141 | // TODO: We're assuming that the "created" route is the same as POST/id, 142 | // and this may not be true. 143 | indentWriter.WriteLine($"return Results.Created($\"{{url}}/{{id}}\", newObj);"); 144 | 145 | indentWriter.Indent--; 146 | indentWriter.WriteLine("});"); 147 | 148 | indentWriter.WriteLine(); 149 | indentWriter.WriteLine("logger.LogInformation($\"Created API: HTTP POST\\t{url}\");"); 150 | 151 | indentWriter.Indent--; 152 | indentWriter.WriteLine("}"); 153 | } 154 | 155 | private static void BuildPut(IndentedTextWriter indentWriter, INamedTypeSymbol type, TableData table, string tableVariableName) 156 | { 157 | indentWriter.WriteLine($"if ({tableVariableName}.APIs.HasFlag(ApisToGenerate.Update))"); 158 | indentWriter.WriteLine("{"); 159 | indentWriter.Indent++; 160 | 161 | indentWriter.WriteLine($"var url = {tableVariableName}.RoutePut.Invoke({tableVariableName}.Name);"); 162 | indentWriter.WriteLine($"app.MapPut(url, async ([FromServices] {type.Name} db, [FromRoute] string id, [FromBody] {table.PropertyType.Name} newObj) =>"); 163 | indentWriter.WriteLine("{"); 164 | indentWriter.Indent++; 165 | 166 | indentWriter.WriteLine($"db.{table.Name}.Attach(newObj);"); 167 | indentWriter.WriteLine("db.Entry(newObj).State = EntityState.Modified;"); 168 | indentWriter.WriteLine("await db.SaveChangesAsync();"); 169 | indentWriter.WriteLine("return Results.NoContent();"); 170 | 171 | indentWriter.Indent--; 172 | indentWriter.WriteLine("});"); 173 | 174 | indentWriter.WriteLine(); 175 | indentWriter.WriteLine("logger.LogInformation($\"Created API: HTTP PUT\\t{url}\");"); 176 | 177 | indentWriter.Indent--; 178 | indentWriter.WriteLine("}"); 179 | } 180 | 181 | private static void BuildDeleteById(IndentedTextWriter indentWriter, INamedTypeSymbol type, TableData table, string tableVariableName) 182 | { 183 | indentWriter.WriteLine($"if ({tableVariableName}.APIs.HasFlag(ApisToGenerate.Delete))"); 184 | indentWriter.WriteLine("{"); 185 | indentWriter.Indent++; 186 | 187 | indentWriter.WriteLine($"var url = {tableVariableName}.RouteDeleteById.Invoke({tableVariableName}.Name);"); 188 | indentWriter.WriteLine($"app.MapDelete(url, async ([FromServices] {type.Name} db, [FromRoute] string id) =>"); 189 | indentWriter.WriteLine("{"); 190 | indentWriter.Indent++; 191 | 192 | indentWriter.WriteLine($"{table.PropertyType.Name}? obj = await db.{table.Name}.FindAsync({GetIdParseCode(table.IdType!)});"); 193 | indentWriter.WriteLine(); 194 | indentWriter.WriteLine("if (obj is null) { return Results.NotFound(); }"); 195 | indentWriter.WriteLine(); 196 | indentWriter.WriteLine($"db.{table.Name}.Remove(obj);"); 197 | indentWriter.WriteLine("await db.SaveChangesAsync();"); 198 | indentWriter.WriteLine("return Results.NoContent();"); 199 | 200 | indentWriter.Indent--; 201 | indentWriter.WriteLine("});"); 202 | 203 | indentWriter.WriteLine(); 204 | indentWriter.WriteLine("logger.LogInformation($\"Created API: HTTP DELETE\\t{url}\");"); 205 | 206 | indentWriter.Indent--; 207 | indentWriter.WriteLine("}"); 208 | } 209 | 210 | private static string GetIdParseCode(INamedTypeSymbol tableType) 211 | { 212 | var idValue = "id"; 213 | 214 | if (tableType.SpecialType == SpecialType.System_Int32) 215 | { 216 | idValue = "int.Parse(id)"; 217 | } 218 | else if (tableType.SpecialType == SpecialType.System_Int64) 219 | { 220 | idValue = "long.Parse(id)"; 221 | } 222 | // TODO: This is not ideal for identifying a Guid...I think... 223 | else if (tableType.ToDisplayString() == "System.Guid") 224 | { 225 | idValue = "Guid.Parse(id)"; 226 | } 227 | 228 | return idValue; 229 | } 230 | } 231 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators/Builders/TablesEnumBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.CodeDom.Compiler; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | namespace InstantAPIs.Generators.Builders 6 | { 7 | internal sealed class TablesEnumBuilder 8 | { 9 | internal static void Build(IndentedTextWriter indentWriter, string name, List tables) 10 | { 11 | indentWriter.WriteLine($"public enum {name}Tables"); 12 | indentWriter.WriteLine("{"); 13 | indentWriter.Indent++; 14 | indentWriter.WriteLine(string.Join(", ", tables.Select(_ => _.Name))); 15 | indentWriter.Indent--; 16 | indentWriter.WriteLine("}"); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators/DbContextAPIGenerator.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs.Generators.Builders; 2 | using InstantAPIs.Generators.Diagnostics; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CSharp.Syntax; 5 | using System.Collections.Generic; 6 | using System.Collections.Immutable; 7 | using System.Linq; 8 | using System.Threading; 9 | 10 | namespace InstantAPIs.Generators; 11 | 12 | [Generator] 13 | public sealed class DbContextAPIGenerator 14 | : IIncrementalGenerator 15 | { 16 | public void Initialize(IncrementalGeneratorInitializationContext context) 17 | { 18 | static bool IsSyntaxTargetForGeneration(SyntaxNode node, CancellationToken token) => 19 | node is AttributeSyntax attributeNode && 20 | (attributeNode.Name.ToString() == "InstantAPIsForDbContext" || attributeNode.Name.ToString() == "InstantAPIsForDbContextAttribute"); 21 | 22 | static (AttributeSyntax, INamedTypeSymbol)? TransformTargets(GeneratorSyntaxContext context, CancellationToken token) 23 | { 24 | // We only want to return types with our attribute 25 | var node = (AttributeSyntax)context.Node; 26 | var model = context.SemanticModel; 27 | 28 | // AttributeSyntax maps to a IMethodSymbol (you're basically calling a constructor 29 | // when you declare an attribute on a member). 30 | var symbol = model.GetSymbolInfo(node, token).Symbol as IMethodSymbol; 31 | 32 | if (symbol is not null) 33 | { 34 | // Let's do a best guess that it's the attribute we're looking for. 35 | if(symbol.ContainingType.Name == "InstantAPIsForDbContextAttribute" && 36 | symbol.ContainingNamespace.ToDisplayString() == "InstantAPIs.Generators.Helpers") 37 | { 38 | // Find the attribute data for the node. 39 | var attributeData = model.Compilation.Assembly.GetAttributes().SingleOrDefault( 40 | _ => _.ApplicationSyntaxReference!.GetSyntax() == node); 41 | 42 | if (attributeData is not null && 43 | attributeData.ConstructorArguments[0].Value is not null && 44 | attributeData.ConstructorArguments[0].Value is INamedTypeSymbol typeSymbol) 45 | { 46 | return (node, typeSymbol); 47 | } 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | var provider = context.SyntaxProvider 55 | .CreateSyntaxProvider(IsSyntaxTargetForGeneration, TransformTargets) 56 | .Where(static _ => _ is not null); 57 | var output = context.CompilationProvider.Combine(provider.Collect()); 58 | 59 | context.RegisterSourceOutput(output, 60 | (context, source) => CreateOutput(source.Right, context)); 61 | } 62 | 63 | private static void CreateOutput(ImmutableArray<(AttributeSyntax, INamedTypeSymbol)?> symbols, SourceProductionContext context) 64 | { 65 | static bool IsDbContext(INamedTypeSymbol type) 66 | { 67 | var baseType = type.BaseType; 68 | 69 | while (baseType is not null) 70 | { 71 | if (baseType.Name == "DbContext" && baseType.ContainingNamespace.ToDisplayString() == "Microsoft.EntityFrameworkCore") 72 | { 73 | return true; 74 | } 75 | 76 | baseType = baseType.BaseType; 77 | } 78 | 79 | return false; 80 | } 81 | 82 | var dbTypes = new HashSet(SymbolEqualityComparer.Default); 83 | 84 | foreach(var symbol in symbols) 85 | { 86 | var node = symbol!.Value.Item1; 87 | var typeSymbol = symbol!.Value.Item2; 88 | 89 | if (!IsDbContext(typeSymbol)) 90 | { 91 | context.ReportDiagnostic(NotADbContextDiagnostic.Create(typeSymbol, node)); 92 | } 93 | else 94 | { 95 | if(!dbTypes.Add(typeSymbol)) 96 | { 97 | context.ReportDiagnostic(DuplicateDefinitionDiagnostic.Create(node)); 98 | } 99 | else 100 | { 101 | var text = DbContextAPIBuilder.Build(typeSymbol); 102 | 103 | if (text is not null) 104 | { 105 | context.AddSource($"{typeSymbol.Name}_DbContextAPIGenerator.g.cs", text); 106 | } 107 | } 108 | } 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /InstantAPIs.Generators/Diagnostics/DescriptorConstants.cs: -------------------------------------------------------------------------------- 1 | namespace InstantAPIs.Generators.Diagnostics; 2 | 3 | public static class DescriptorConstants 4 | { 5 | public const string Usage = nameof(DescriptorConstants.Usage); 6 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators/Diagnostics/DuplicateDefinitionDiagnostic.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace InstantAPIs.Generators.Diagnostics; 4 | 5 | public class DuplicateDefinitionDiagnostic 6 | { 7 | public static Diagnostic Create(SyntaxNode currentNode) => 8 | Diagnostic.Create(new DiagnosticDescriptor( 9 | DuplicateDefinitionDiagnostic.Id, DuplicateDefinitionDiagnostic.Title, 10 | DuplicateDefinitionDiagnostic.Message, DescriptorConstants.Usage, DiagnosticSeverity.Warning, true, 11 | helpLinkUri: HelpUrlBuilder.Build( 12 | DuplicateDefinitionDiagnostic.Id, DuplicateDefinitionDiagnostic.Title)), 13 | currentNode.GetLocation()); 14 | 15 | public const string Id = "IA2"; 16 | public const string Message = "The given DbContext has already been defined."; 17 | public const string Title = "Duplicate DbContext Definition"; 18 | } 19 | -------------------------------------------------------------------------------- /InstantAPIs.Generators/Diagnostics/NotADbContextDiagnostic.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System.Globalization; 3 | 4 | namespace InstantAPIs.Generators.Diagnostics; 5 | 6 | public static class NotADbContextDiagnostic 7 | { 8 | public static Diagnostic Create(INamedTypeSymbol type, SyntaxNode attribute) => 9 | Diagnostic.Create(new DiagnosticDescriptor( 10 | NotADbContextDiagnostic.Id, NotADbContextDiagnostic.Title, 11 | string.Format(CultureInfo.CurrentCulture, NotADbContextDiagnostic.Message, type.Name), 12 | DescriptorConstants.Usage, DiagnosticSeverity.Error, true, 13 | helpLinkUri: HelpUrlBuilder.Build( 14 | NotADbContextDiagnostic.Id, NotADbContextDiagnostic.Title)), 15 | attribute.GetLocation()); 16 | 17 | public const string Id = "IA1"; 18 | public const string Message = "The given type, {0}, does not derive from DbContext."; 19 | public const string Title = "Not a DbContext"; 20 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators/HelpUrlBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace InstantAPIs.Generators; 2 | 3 | internal static class HelpUrlBuilder 4 | { 5 | internal static string Build(string identifier, string title) => 6 | $"https://github.com/csharpfritz/InstantAPIs/tree/main/docs/{identifier}-{title}.md"; 7 | } -------------------------------------------------------------------------------- /InstantAPIs.Generators/InstantAPIs.Generators.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | latest 4 | enable 5 | netstandard2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /InstantAPIs.Generators/NamespaceGatherer.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Collections.Immutable; 5 | 6 | namespace InstantAPIs.Generators; 7 | 8 | internal sealed class NamespaceGatherer 9 | { 10 | private readonly ImmutableHashSet.Builder builder = 11 | ImmutableHashSet.CreateBuilder(); 12 | 13 | public void Add(INamespaceSymbol @namespace) 14 | { 15 | if (!@namespace.IsGlobalNamespace) 16 | { 17 | this.builder.Add(@namespace.ToDisplayString(SymbolDisplayFormat.CSharpErrorMessageFormat)); 18 | } 19 | } 20 | 21 | public void Add(string @namespace) => 22 | this.builder.Add(@namespace); 23 | 24 | public void Add(Type type) 25 | { 26 | if (!string.IsNullOrWhiteSpace(type.Namespace)) 27 | { 28 | this.builder.Add(type.Namespace); 29 | } 30 | } 31 | 32 | public void AddRange(IEnumerable namespaces) 33 | { 34 | foreach (var @namespace in namespaces) 35 | { 36 | this.Add(@namespace); 37 | } 38 | } 39 | 40 | public IImmutableSet Values => this.builder.ToImmutableSortedSet(); 41 | } 42 | -------------------------------------------------------------------------------- /InstantAPIs.Generators/TableData.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.CodeAnalysis; 2 | 3 | namespace InstantAPIs.Generators; 4 | 5 | internal sealed class TableData 6 | { 7 | internal TableData(string name, INamedTypeSymbol propertyType, INamedTypeSymbol? idType, string? idName) => 8 | (Name, PropertyType, IdType, IdName) = (name, propertyType, idType, idName); 9 | 10 | public INamedTypeSymbol PropertyType { get; } 11 | public string? IdName { get; } 12 | public INamedTypeSymbol? IdType { get; } 13 | internal string Name { get; } 14 | } 15 | -------------------------------------------------------------------------------- /InstantAPIs.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.2.32216.311 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkingApi", "WorkingApi\WorkingApi.csproj", "{67B3C4BF-3F0F-4179-8B43-008F0B18761A}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InstantAPIs", "InstantAPIs\InstantAPIs.csproj", "{C945FAF9-D8A9-48EE-8A19-4AB0996CABEC}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Workflows", "Workflows", "{CDEC8FD4-C50A-4FCA-BF93-6C7AA3F24BB4}" 11 | ProjectSection(SolutionItems) = preProject 12 | .github\workflows\build.yaml = .github\workflows\build.yaml 13 | EndProjectSection 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InstantAPIs.Generators", "InstantAPIs.Generators\InstantAPIs.Generators.csproj", "{56ABEEC4-77C0-42CE-A68B-592E4705D90E}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InstantAPIs.Generators.Tests", "InstantAPIs.Generators.Tests\InstantAPIs.Generators.Tests.csproj", "{EE295501-1A67-4E55-A1AD-B98C290F604D}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InstantAPIs.Generators.Helpers", "InstantAPIs.Generators.Helpers\InstantAPIs.Generators.Helpers.csproj", "{2FF08F79-C2C5-4DD4-9A95-EABFA09A054C}" 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{C73968E9-8A12-401C-BA5D-127C6D5A55D6}" 22 | ProjectSection(SolutionItems) = preProject 23 | .editorconfig = .editorconfig 24 | CODE-OF-CONDUCT.md = CODE-OF-CONDUCT.md 25 | CONTRIBUTING.md = CONTRIBUTING.md 26 | RELEASE_NOTES.txt = RELEASE_NOTES.txt 27 | EndProjectSection 28 | EndProject 29 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Test", "Test\Test.csproj", "{CD123B01-1B52-4E80-84F7-4D10E01EE10F}" 30 | EndProject 31 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestJson", "TestJson\TestJson.csproj", "{99D818F8-63A2-4004-87AB-CA61E1B125CC}" 32 | EndProject 33 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WorkingApi.Generators", "WorkingApi.Generators\WorkingApi.Generators.csproj", "{4BDA89CB-733A-49DE-A5C5-D4695EB8A483}" 34 | EndProject 35 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "InstantAPIs.Generators.Helpers.Tests", "InstantAPIs.Generators.Helpers.Tests\InstantAPIs.Generators.Helpers.Tests.csproj", "{0F5A7147-94E3-4BAA-AC94-A6EBD44B8966}" 36 | EndProject 37 | Global 38 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 39 | Debug|Any CPU = Debug|Any CPU 40 | Release|Any CPU = Release|Any CPU 41 | EndGlobalSection 42 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 43 | {67B3C4BF-3F0F-4179-8B43-008F0B18761A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {67B3C4BF-3F0F-4179-8B43-008F0B18761A}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {67B3C4BF-3F0F-4179-8B43-008F0B18761A}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {67B3C4BF-3F0F-4179-8B43-008F0B18761A}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {C945FAF9-D8A9-48EE-8A19-4AB0996CABEC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {C945FAF9-D8A9-48EE-8A19-4AB0996CABEC}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {C945FAF9-D8A9-48EE-8A19-4AB0996CABEC}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {C945FAF9-D8A9-48EE-8A19-4AB0996CABEC}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {56ABEEC4-77C0-42CE-A68B-592E4705D90E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {56ABEEC4-77C0-42CE-A68B-592E4705D90E}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {56ABEEC4-77C0-42CE-A68B-592E4705D90E}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {56ABEEC4-77C0-42CE-A68B-592E4705D90E}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {EE295501-1A67-4E55-A1AD-B98C290F604D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {EE295501-1A67-4E55-A1AD-B98C290F604D}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {EE295501-1A67-4E55-A1AD-B98C290F604D}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {EE295501-1A67-4E55-A1AD-B98C290F604D}.Release|Any CPU.Build.0 = Release|Any CPU 59 | {2FF08F79-C2C5-4DD4-9A95-EABFA09A054C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {2FF08F79-C2C5-4DD4-9A95-EABFA09A054C}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {2FF08F79-C2C5-4DD4-9A95-EABFA09A054C}.Release|Any CPU.ActiveCfg = Release|Any CPU 62 | {2FF08F79-C2C5-4DD4-9A95-EABFA09A054C}.Release|Any CPU.Build.0 = Release|Any CPU 63 | {CD123B01-1B52-4E80-84F7-4D10E01EE10F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 64 | {CD123B01-1B52-4E80-84F7-4D10E01EE10F}.Debug|Any CPU.Build.0 = Debug|Any CPU 65 | {CD123B01-1B52-4E80-84F7-4D10E01EE10F}.Release|Any CPU.ActiveCfg = Release|Any CPU 66 | {CD123B01-1B52-4E80-84F7-4D10E01EE10F}.Release|Any CPU.Build.0 = Release|Any CPU 67 | {99D818F8-63A2-4004-87AB-CA61E1B125CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {99D818F8-63A2-4004-87AB-CA61E1B125CC}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {99D818F8-63A2-4004-87AB-CA61E1B125CC}.Release|Any CPU.ActiveCfg = Release|Any CPU 70 | {99D818F8-63A2-4004-87AB-CA61E1B125CC}.Release|Any CPU.Build.0 = Release|Any CPU 71 | {4BDA89CB-733A-49DE-A5C5-D4695EB8A483}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 72 | {4BDA89CB-733A-49DE-A5C5-D4695EB8A483}.Debug|Any CPU.Build.0 = Debug|Any CPU 73 | {4BDA89CB-733A-49DE-A5C5-D4695EB8A483}.Release|Any CPU.ActiveCfg = Release|Any CPU 74 | {4BDA89CB-733A-49DE-A5C5-D4695EB8A483}.Release|Any CPU.Build.0 = Release|Any CPU 75 | {0F5A7147-94E3-4BAA-AC94-A6EBD44B8966}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 76 | {0F5A7147-94E3-4BAA-AC94-A6EBD44B8966}.Debug|Any CPU.Build.0 = Debug|Any CPU 77 | {0F5A7147-94E3-4BAA-AC94-A6EBD44B8966}.Release|Any CPU.ActiveCfg = Release|Any CPU 78 | {0F5A7147-94E3-4BAA-AC94-A6EBD44B8966}.Release|Any CPU.Build.0 = Release|Any CPU 79 | EndGlobalSection 80 | GlobalSection(SolutionProperties) = preSolution 81 | HideSolutionNode = FALSE 82 | EndGlobalSection 83 | GlobalSection(ExtensibilityGlobals) = postSolution 84 | SolutionGuid = {2E620812-0D88-48B8-A69F-578CBE43C457} 85 | EndGlobalSection 86 | EndGlobal 87 | -------------------------------------------------------------------------------- /InstantAPIs/ApiMethodsToGenerate.cs: -------------------------------------------------------------------------------- 1 | namespace InstantAPIs; 2 | 3 | [Flags] 4 | public enum ApiMethodsToGenerate 5 | { 6 | Get = 1, 7 | GetById = 2, 8 | Insert = 4, 9 | Update = 8, 10 | Delete = 16, 11 | All = 31 12 | } 13 | 14 | public record TableApiMapping( 15 | string TableName, 16 | ApiMethodsToGenerate MethodsToGenerate = ApiMethodsToGenerate.All, 17 | string BaseUrl = "" 18 | ); 19 | 20 | [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 21 | public class ApiMethodAttribute : Attribute 22 | { 23 | public ApiMethodsToGenerate MethodsToGenerate { get; set; } 24 | public ApiMethodAttribute(ApiMethodsToGenerate apiMethodsToGenerate) 25 | { 26 | this.MethodsToGenerate = apiMethodsToGenerate; 27 | } 28 | } -------------------------------------------------------------------------------- /InstantAPIs/InstantAPIs.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | csharpfritz 8 | A library that generates Minimal API endpoints for an Entity Framework context. 9 | MIT 10 | entity framework, ef, webapi 11 | git 12 | https://github.com/csharpfritz/InstantAPIs 13 | README.md 14 | https://github.com/csharpfritz/InstantAPIs 15 | true 16 | true 17 | embedded 18 | 0.2.1 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | @(ReleaseNoteLines, '%0a') 28 | 29 | 30 | 31 | 32 | true 33 | 34 | 35 | 36 | 37 | 38 | True 39 | \ 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | <_Parameter1>Test 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /InstantAPIs/InstantAPIsConfig.cs: -------------------------------------------------------------------------------- 1 | namespace InstantAPIs; 2 | 3 | internal class InstantAPIsConfig 4 | { 5 | 6 | internal HashSet Tables { get; } = new HashSet(); 7 | 8 | } 9 | 10 | 11 | public class InstantAPIsConfigBuilder where D : DbContext 12 | { 13 | 14 | private InstantAPIsConfig _Config = new(); 15 | private Type _ContextType = typeof(D); 16 | private D _TheContext; 17 | private readonly HashSet _IncludedTables = new(); 18 | private readonly List _ExcludedTables = new(); 19 | private const string DEFAULT_URI = "/api/"; 20 | 21 | public InstantAPIsConfigBuilder(D theContext) 22 | { 23 | this._TheContext = theContext; 24 | } 25 | 26 | #region Table Inclusion/Exclusion 27 | 28 | /// 29 | /// Specify individual tables to include in the API generation with the methods requested 30 | /// 31 | /// Select the EntityFramework DbSet to include - Required 32 | /// A flags enumerable indicating the methods to generate. By default ALL are generated 33 | /// Configuration builder with this configuration applied 34 | public InstantAPIsConfigBuilder IncludeTable(Func> entitySelector, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All, string baseUrl = "") where T : class 35 | { 36 | 37 | var theSetType = entitySelector(_TheContext).GetType().BaseType; 38 | var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType); 39 | 40 | if (!string.IsNullOrEmpty(baseUrl)) 41 | { 42 | try 43 | { 44 | var testUri = new Uri(baseUrl, UriKind.RelativeOrAbsolute); 45 | baseUrl = testUri.IsAbsoluteUri ? testUri.LocalPath : baseUrl; 46 | } 47 | catch 48 | { 49 | throw new ArgumentException(nameof(baseUrl), "Not a valid Uri"); 50 | } 51 | } 52 | else 53 | { 54 | baseUrl = String.Concat(DEFAULT_URI, property.Name); 55 | } 56 | 57 | var tableApiMapping = new TableApiMapping(property.Name, methodsToGenerate, baseUrl); 58 | _IncludedTables.Add(tableApiMapping); 59 | 60 | if (_ExcludedTables.Contains(tableApiMapping.TableName)) _ExcludedTables.Remove(tableApiMapping.TableName); 61 | _IncludedTables.Add(tableApiMapping); 62 | 63 | return this; 64 | 65 | } 66 | 67 | /// 68 | /// Exclude individual tables from the API generation. Exclusion takes priority over inclusion 69 | /// 70 | /// Select the entity to exclude from generation 71 | /// Configuration builder with this configuraiton applied 72 | public InstantAPIsConfigBuilder ExcludeTable(Func> entitySelector) where T : class 73 | { 74 | 75 | var theSetType = entitySelector(_TheContext).GetType().BaseType; 76 | var property = _ContextType.GetProperties().First(p => p.PropertyType == theSetType); 77 | 78 | if (_IncludedTables.Select(t => t.TableName).Contains(property.Name)) _IncludedTables.Remove(_IncludedTables.First(t => t.TableName == property.Name)); 79 | _ExcludedTables.Add(property.Name); 80 | 81 | return this; 82 | 83 | } 84 | 85 | private void BuildTables() 86 | { 87 | 88 | var tables = WebApplicationExtensions.GetDbTablesForContext().ToArray(); 89 | WebApplicationExtensions.TypeTable[]? outTables; 90 | 91 | // Add the Included tables 92 | if (_IncludedTables.Any()) 93 | { 94 | outTables = tables.Where(t => _IncludedTables.Any(i => i.TableName.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase))) 95 | .Select(t => new WebApplicationExtensions.TypeTable 96 | { 97 | Name = t.Name, 98 | InstanceType = t.InstanceType, 99 | ApiMethodsToGenerate = _IncludedTables.First(i => i.TableName.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).MethodsToGenerate, 100 | BaseUrl = new Uri(_IncludedTables.First(i => i.TableName.Equals(t.Name, StringComparison.InvariantCultureIgnoreCase)).BaseUrl, UriKind.Relative) 101 | }).ToArray(); 102 | } else { 103 | outTables = tables.Select(t => new WebApplicationExtensions.TypeTable 104 | { 105 | Name = t.Name, 106 | InstanceType = t.InstanceType, 107 | BaseUrl = new Uri(DEFAULT_URI + t.Name, uriKind: UriKind.Relative) 108 | }).ToArray(); 109 | } 110 | 111 | // Exit now if no tables were excluded 112 | if (!_ExcludedTables.Any()) 113 | { 114 | _Config.Tables.UnionWith(outTables); 115 | return; 116 | } 117 | 118 | // Remove the Excluded tables 119 | outTables = outTables.Where(t => !_ExcludedTables.Any(e => t.Name.Equals(e, StringComparison.InvariantCultureIgnoreCase))).ToArray(); 120 | 121 | if (outTables == null || !outTables.Any()) throw new ArgumentException("All tables were excluded from this configuration"); 122 | 123 | _Config.Tables.UnionWith(outTables); 124 | 125 | } 126 | 127 | #endregion 128 | 129 | internal InstantAPIsConfig Build() 130 | { 131 | 132 | BuildTables(); 133 | 134 | return _Config; 135 | } 136 | 137 | } -------------------------------------------------------------------------------- /InstantAPIs/InstantAPIsServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace InstantAPIs; 4 | 5 | public static class InstantAPIsServiceCollectionExtensions 6 | { 7 | public static IServiceCollection AddInstantAPIs(this IServiceCollection services, Action? setupAction = null) 8 | { 9 | var options = new InstantAPIsServiceOptions(); 10 | 11 | // Get the service options 12 | setupAction?.Invoke(options); 13 | 14 | if (options.EnableSwagger == null) 15 | { 16 | options.EnableSwagger = EnableSwagger.DevelopmentOnly; 17 | } 18 | 19 | // Add and configure Swagger services if it is enabled 20 | if (options.EnableSwagger != EnableSwagger.None) 21 | { 22 | services.AddEndpointsApiExplorer(); 23 | services.AddSwaggerGen(options.Swagger); 24 | } 25 | 26 | // Register the required options so that it can be accessed by InstantAPIs middleware 27 | services.Configure(config => 28 | { 29 | config.EnableSwagger = options.EnableSwagger; 30 | }); 31 | 32 | return services; 33 | } 34 | } -------------------------------------------------------------------------------- /InstantAPIs/InstantAPIsServiceOptions.cs: -------------------------------------------------------------------------------- 1 | using Swashbuckle.AspNetCore.SwaggerGen; 2 | 3 | namespace InstantAPIs; 4 | 5 | public enum EnableSwagger 6 | { 7 | None, 8 | DevelopmentOnly, 9 | Always 10 | } 11 | 12 | public class InstantAPIsServiceOptions 13 | { 14 | 15 | public EnableSwagger? EnableSwagger { get; set; } 16 | public Action? Swagger { get; set; } 17 | } -------------------------------------------------------------------------------- /InstantAPIs/JsonAPIsConfig.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace InstantAPIs; 4 | 5 | internal class JsonAPIsConfig 6 | { 7 | 8 | internal HashSet Tables { get; } = new HashSet(); 9 | 10 | internal string JsonFilename = "mock.json"; 11 | 12 | } 13 | 14 | 15 | public class JsonAPIsConfigBuilder 16 | { 17 | 18 | private JsonAPIsConfig _Config = new(); 19 | private string _FileName; 20 | private readonly HashSet _IncludedTables = new(); 21 | private readonly List _ExcludedTables = new(); 22 | 23 | public JsonAPIsConfigBuilder SetFilename(string fileName) 24 | { 25 | _FileName = fileName; 26 | return this; 27 | } 28 | 29 | #region Table Inclusion/Exclusion 30 | 31 | /// 32 | /// Specify individual entities to include in the API generation with the methods requested 33 | /// 34 | /// Name of the JSON entity collection to include 35 | /// A flags enumerable indicating the methods to generate. By default ALL are generated 36 | /// Configuration builder with this configuration applied 37 | public JsonAPIsConfigBuilder IncludeEntity(string entityName, ApiMethodsToGenerate methodsToGenerate = ApiMethodsToGenerate.All) 38 | { 39 | 40 | var tableApiMapping = new TableApiMapping(entityName, methodsToGenerate); 41 | _IncludedTables.Add(tableApiMapping); 42 | 43 | if (_ExcludedTables.Contains(entityName)) _ExcludedTables.Remove(tableApiMapping.TableName); 44 | 45 | return this; 46 | 47 | } 48 | 49 | /// 50 | /// Exclude individual entities from the API generation. Exclusion takes priority over inclusion 51 | /// 52 | /// Name of the JSON entity collection to exclude 53 | /// Configuration builder with this configuraiton applied 54 | public JsonAPIsConfigBuilder ExcludeTable(string entityName) 55 | { 56 | 57 | if (_IncludedTables.Select(t => t.TableName).Contains(entityName)) _IncludedTables.Remove(_IncludedTables.First(t => t.TableName == entityName)); 58 | _ExcludedTables.Add(entityName); 59 | 60 | return this; 61 | 62 | } 63 | 64 | private HashSet IdentifyEntities() 65 | { 66 | var writableDoc = JsonNode.Parse(File.ReadAllText(_FileName)); 67 | 68 | // print API 69 | return writableDoc?.Root.AsObject() 70 | .AsEnumerable().Select(x => x.Key) 71 | .ToHashSet(); 72 | 73 | } 74 | 75 | private void BuildTables() 76 | { 77 | 78 | var tables = IdentifyEntities(); 79 | 80 | if (!_IncludedTables.Any() && !_ExcludedTables.Any()) 81 | { 82 | _Config.Tables.UnionWith(tables.Select(t => new WebApplicationExtensions.TypeTable 83 | { 84 | Name = t, 85 | ApiMethodsToGenerate = ApiMethodsToGenerate.All 86 | })); 87 | return; 88 | } 89 | 90 | // Add the Included tables 91 | var outTables = _IncludedTables 92 | .Select(t => new WebApplicationExtensions.TypeTable 93 | { 94 | Name = t.TableName, 95 | ApiMethodsToGenerate = t.MethodsToGenerate 96 | }).ToArray(); 97 | 98 | // If no tables were added, added them all 99 | if (outTables.Length == 0) 100 | { 101 | outTables = tables.Select(t => new WebApplicationExtensions.TypeTable 102 | { 103 | Name = t, 104 | ApiMethodsToGenerate = ApiMethodsToGenerate.All 105 | }).ToArray(); 106 | } 107 | 108 | // Remove the Excluded tables 109 | outTables = outTables.Where(t => !_ExcludedTables.Any(e => t.Name.Equals(e, StringComparison.InvariantCultureIgnoreCase))).ToArray(); 110 | 111 | if (outTables == null || !outTables.Any()) throw new ArgumentException("All tables were excluded from this configuration"); 112 | 113 | _Config.Tables.UnionWith(outTables); 114 | 115 | } 116 | 117 | #endregion 118 | 119 | internal JsonAPIsConfig Build() 120 | { 121 | 122 | if (string.IsNullOrEmpty(_FileName)) throw new ArgumentNullException("Missing Json Filename for configuration"); 123 | if (!File.Exists(_FileName)) throw new ArgumentException($"Unable to locate the JSON file for APIs at {_FileName}"); 124 | _Config.JsonFilename = _FileName; 125 | 126 | BuildTables(); 127 | 128 | return _Config; 129 | } 130 | 131 | } -------------------------------------------------------------------------------- /InstantAPIs/JsonApiExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Http; 3 | using System.Text.Json.Nodes; 4 | 5 | namespace InstantAPIs; 6 | 7 | public static class JsonApiExtensions 8 | { 9 | 10 | static JsonAPIsConfig _Config; 11 | 12 | public static WebApplication UseJsonRoutes(this WebApplication app, Action options = null) 13 | { 14 | 15 | var builder = new JsonAPIsConfigBuilder(); 16 | _Config = new JsonAPIsConfig(); 17 | if (options != null) 18 | { 19 | options(builder); 20 | _Config = builder.Build(); 21 | } 22 | 23 | var writableDoc = JsonNode.Parse(File.ReadAllText(_Config.JsonFilename)); 24 | 25 | // print API 26 | foreach (var elem in writableDoc?.Root.AsObject().AsEnumerable()) 27 | { 28 | 29 | var thisEntity = _Config.Tables.FirstOrDefault(t => t.Name.Equals(elem.Key, StringComparison.InvariantCultureIgnoreCase)); 30 | if (thisEntity == null) continue; 31 | 32 | if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Get) == ApiMethodsToGenerate.Get) 33 | Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower())); 34 | 35 | if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.GetById) == ApiMethodsToGenerate.GetById) 36 | Console.WriteLine(string.Format("GET /{0}", elem.Key.ToLower()) + "/id"); 37 | 38 | if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Insert) == ApiMethodsToGenerate.Insert) 39 | Console.WriteLine(string.Format("POST /{0}", elem.Key.ToLower())); 40 | 41 | if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Update) == ApiMethodsToGenerate.Update) 42 | Console.WriteLine(string.Format("PUT /{0}", elem.Key.ToLower())); 43 | 44 | if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Delete) == ApiMethodsToGenerate.Delete) 45 | Console.WriteLine(string.Format("DELETE /{0}", elem.Key.ToLower()) + "/id"); 46 | 47 | Console.WriteLine(" "); 48 | } 49 | 50 | // setup routes 51 | foreach (var elem in writableDoc?.Root.AsObject().AsEnumerable()) 52 | { 53 | 54 | var thisEntity = _Config.Tables.FirstOrDefault(t => t.Name.Equals(elem.Key, StringComparison.InvariantCultureIgnoreCase)); 55 | if (thisEntity == null) continue; 56 | 57 | var arr = elem.Value.AsArray(); 58 | 59 | if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Get) == ApiMethodsToGenerate.Get) 60 | app.MapGet(string.Format("/{0}", elem.Key), () => elem.Value.ToString()); 61 | 62 | if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.GetById) == ApiMethodsToGenerate.GetById) 63 | app.MapGet(string.Format("/{0}", elem.Key) + "/{id}", (int id) => 64 | { 65 | var matchedItem = arr.SingleOrDefault(row => row 66 | .AsObject() 67 | .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) 68 | ); 69 | return matchedItem; 70 | }); 71 | 72 | if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Insert) == ApiMethodsToGenerate.Insert) 73 | app.MapPost(string.Format("/{0}", elem.Key), async (HttpRequest request) => 74 | { 75 | string content = string.Empty; 76 | using (StreamReader reader = new StreamReader(request.Body)) 77 | { 78 | content = await reader.ReadToEndAsync(); 79 | } 80 | var newNode = JsonNode.Parse(content); 81 | var array = elem.Value.AsArray(); 82 | newNode.AsObject().Add("Id", array.Count() + 1); 83 | array.Add(newNode); 84 | 85 | File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); 86 | return content; 87 | }); 88 | 89 | if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Update) == ApiMethodsToGenerate.Update) 90 | app.MapPut(string.Format("/{0}", elem.Key), async (HttpRequest request) => 91 | { 92 | string content = string.Empty; 93 | using (StreamReader reader = new StreamReader(request.Body)) 94 | { 95 | content = await reader.ReadToEndAsync(); 96 | } 97 | var newNode = JsonNode.Parse(content); 98 | var array = elem.Value.AsArray(); 99 | array.Add(newNode); 100 | 101 | File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); 102 | 103 | return "OK"; 104 | }); 105 | 106 | if ((thisEntity.ApiMethodsToGenerate & ApiMethodsToGenerate.Delete) == ApiMethodsToGenerate.Delete) 107 | app.MapDelete(string.Format("/{0}", elem.Key) + "/{id}", (int id) => 108 | { 109 | 110 | var matchedItem = arr 111 | .Select((value, index) => new { value, index }) 112 | .SingleOrDefault(row => row.value 113 | .AsObject() 114 | .Any(o => o.Key.ToLower() == "id" && int.Parse(o.Value.ToString()) == id) 115 | ); 116 | if (matchedItem != null) 117 | { 118 | arr.RemoveAt(matchedItem.index); 119 | File.WriteAllText(_Config.JsonFilename, writableDoc.ToString()); 120 | } 121 | 122 | return "OK"; 123 | }); 124 | 125 | }; 126 | 127 | return app; 128 | } 129 | } -------------------------------------------------------------------------------- /InstantAPIs/MapApiExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Routing; 4 | using Microsoft.AspNetCore.Http; 5 | using System.ComponentModel.DataAnnotations; 6 | using System.Reflection; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace InstantAPIs; 10 | 11 | internal class MapApiExtensions 12 | { 13 | 14 | // TODO: Authentication / Authorization 15 | private static Dictionary _IdLookup = new(); 16 | 17 | private static ILogger Logger; 18 | 19 | internal static void Initialize(ILogger logger) 20 | where D: DbContext 21 | where C: class 22 | { 23 | 24 | Logger = logger; 25 | 26 | var theType = typeof(C); 27 | var idProp = theType.GetProperty("id", BindingFlags.IgnoreCase | BindingFlags.Public | BindingFlags.Instance) ?? theType.GetProperties().FirstOrDefault(p => p.CustomAttributes.Any(a => a.AttributeType == typeof(KeyAttribute))); 28 | 29 | if (idProp != null) 30 | { 31 | _IdLookup.Add(theType, idProp); 32 | } 33 | 34 | } 35 | 36 | [ApiMethod(ApiMethodsToGenerate.Get)] 37 | internal static void MapInstantGetAll(IEndpointRouteBuilder app, string url) 38 | where D : DbContext where C : class 39 | { 40 | 41 | Logger.LogInformation($"Created API: HTTP GET\t{url}"); 42 | app.MapGet(url, ([FromServices] D db) => 43 | { 44 | return Results.Ok(db.Set()); 45 | }); 46 | 47 | } 48 | 49 | [ApiMethod(ApiMethodsToGenerate.GetById)] 50 | internal static void MapGetById(IEndpointRouteBuilder app, string url) 51 | where D: DbContext where C : class 52 | { 53 | 54 | // identify the ID field 55 | var theType = typeof(C); 56 | var idProp = _IdLookup[theType]; 57 | 58 | if (idProp == null) return; 59 | 60 | Logger.LogInformation($"Created API: HTTP GET\t{url}/{{id}}"); 61 | 62 | app.MapGet($"{url}/{{id}}", async ([FromServices] D db, [FromRoute] string id) => 63 | { 64 | 65 | C outValue = default(C); 66 | if (idProp.PropertyType == typeof(Guid)) 67 | outValue = await db.Set().FindAsync(Guid.Parse(id)); 68 | else if (idProp.PropertyType == typeof(int)) 69 | outValue = await db.Set().FindAsync(int.Parse(id)); 70 | else if (idProp.PropertyType == typeof(long)) 71 | outValue = await db.Set().FindAsync(long.Parse(id)); 72 | else //if (idProp.PropertyType == typeof(string)) 73 | outValue = await db.Set().FindAsync(id); 74 | 75 | if (outValue is null) return Results.NotFound(); 76 | return Results.Ok(outValue); 77 | }); 78 | 79 | 80 | } 81 | 82 | [ApiMethod(ApiMethodsToGenerate.Insert)] 83 | internal static void MapInstantPost(IEndpointRouteBuilder app, string url) 84 | where D : DbContext where C : class 85 | { 86 | 87 | Logger.LogInformation($"Created API: HTTP POST\t{url}"); 88 | 89 | app.MapPost(url, async ([FromServices] D db, [FromBody] C newObj) => 90 | { 91 | db.Add(newObj); 92 | await db.SaveChangesAsync(); 93 | var id = _IdLookup[typeof(C)].GetValue(newObj); 94 | return Results.Created($"{url}/{id.ToString()}", newObj); 95 | }); 96 | 97 | } 98 | 99 | [ApiMethod(ApiMethodsToGenerate.Update)] 100 | internal static void MapInstantPut(IEndpointRouteBuilder app, string url) 101 | where D : DbContext where C : class 102 | { 103 | 104 | Logger.LogInformation($"Created API: HTTP PUT\t{url}"); 105 | 106 | app.MapPut($"{url}/{{id}}", async ([FromServices] D db, [FromRoute] string id, [FromBody] C newObj) => 107 | { 108 | db.Set().Attach(newObj); 109 | db.Entry(newObj).State = EntityState.Modified; 110 | await db.SaveChangesAsync(); 111 | return Results.NoContent(); 112 | }); 113 | 114 | } 115 | 116 | [ApiMethod(ApiMethodsToGenerate.Delete)] 117 | internal static void MapDeleteById(IEndpointRouteBuilder app, string url) 118 | where D : DbContext where C : class 119 | { 120 | 121 | // identify the ID field 122 | var theType = typeof(C); 123 | var idProp = _IdLookup[theType]; 124 | 125 | if (idProp == null) return; 126 | Logger.LogInformation($"Created API: HTTP DELETE\t{url}"); 127 | 128 | app.MapDelete($"{url}/{{id}}", async ([FromServices] D db, [FromRoute] string id) => 129 | { 130 | 131 | var set = db.Set(); 132 | C? obj; 133 | 134 | if (idProp.PropertyType == typeof(Guid)) 135 | obj = await set.FindAsync(Guid.Parse(id)); 136 | else if (idProp.PropertyType == typeof(int)) 137 | obj = await set.FindAsync(int.Parse(id)); 138 | else if (idProp.PropertyType == typeof(long)) 139 | obj = await set.FindAsync(long.Parse(id)); 140 | else //if (idProp.PropertyType == typeof(string)) 141 | obj = await set.FindAsync(id); 142 | 143 | if (obj == null) return Results.NotFound(); 144 | 145 | db.Set().Remove(obj); 146 | await db.SaveChangesAsync(); 147 | return Results.NoContent(); 148 | 149 | }); 150 | 151 | 152 | } 153 | 154 | } 155 | -------------------------------------------------------------------------------- /InstantAPIs/WebApplicationExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Routing; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Logging.Abstractions; 7 | using Microsoft.Extensions.Options; 8 | using System.Reflection; 9 | 10 | namespace InstantAPIs; 11 | 12 | public static class WebApplicationExtensions 13 | { 14 | 15 | internal const string LOGGER_CATEGORY_NAME = "InstantAPI"; 16 | 17 | private static InstantAPIsConfig Configuration { get; set; } = new(); 18 | 19 | public static IEndpointRouteBuilder MapInstantAPIs(this IEndpointRouteBuilder app, Action> options = null) where D : DbContext 20 | { 21 | if (app is IApplicationBuilder applicationBuilder) 22 | { 23 | AddOpenAPIConfiguration(app, options, applicationBuilder); 24 | } 25 | 26 | // Get the tables on the DbContext 27 | var dbTables = GetDbTablesForContext(); 28 | 29 | var requestedTables = !Configuration.Tables.Any() ? 30 | dbTables : 31 | Configuration.Tables.Where(t => dbTables.Any(db => db.Name.Equals(t.Name, StringComparison.OrdinalIgnoreCase))).ToArray(); 32 | 33 | MapInstantAPIsUsingReflection(app, requestedTables); 34 | 35 | return app; 36 | } 37 | 38 | private static void MapInstantAPIsUsingReflection(IEndpointRouteBuilder app, IEnumerable requestedTables) where D : DbContext 39 | { 40 | 41 | ILogger logger = NullLogger.Instance; 42 | if (app.ServiceProvider != null) 43 | { 44 | var loggerFactory = app.ServiceProvider.GetRequiredService(); 45 | logger = loggerFactory.CreateLogger(LOGGER_CATEGORY_NAME); 46 | } 47 | 48 | var allMethods = typeof(MapApiExtensions).GetMethods(BindingFlags.NonPublic | BindingFlags.Static).Where(m => m.Name.StartsWith("Map")).ToArray(); 49 | var initialize = typeof(MapApiExtensions).GetMethod("Initialize", BindingFlags.NonPublic | BindingFlags.Static); 50 | foreach (var table in requestedTables) 51 | { 52 | 53 | // The default URL for an InstantAPI is /api/TABLENAME 54 | //var url = $"/api/{table.Name}"; 55 | 56 | initialize.MakeGenericMethod(typeof(D), table.InstanceType).Invoke(null, new[] { logger }); 57 | 58 | // The remaining private static methods in this class build out the Mapped API methods.. 59 | // let's use some reflection to get them 60 | foreach (var method in allMethods) 61 | { 62 | 63 | var sigAttr = method.CustomAttributes.First(x => x.AttributeType == typeof(ApiMethodAttribute)).ConstructorArguments.First(); 64 | var methodType = (ApiMethodsToGenerate)sigAttr.Value; 65 | if ((table.ApiMethodsToGenerate & methodType) != methodType) continue; 66 | 67 | var genericMethod = method.MakeGenericMethod(typeof(D), table.InstanceType); 68 | genericMethod.Invoke(null, new object[] { app, table.BaseUrl.ToString() }); 69 | } 70 | 71 | } 72 | } 73 | 74 | private static void AddOpenAPIConfiguration(IEndpointRouteBuilder app, Action> options, IApplicationBuilder applicationBuilder) where D : DbContext 75 | { 76 | // Check if AddInstantAPIs was called by getting the service options and evaluate EnableSwagger property 77 | var serviceOptions = applicationBuilder.ApplicationServices.GetRequiredService>().Value; 78 | if (serviceOptions == null || serviceOptions.EnableSwagger == null) 79 | { 80 | throw new ArgumentException("Call builder.Services.AddInstantAPIs(options) before MapInstantAPIs."); 81 | } 82 | 83 | var webApp = (WebApplication)app; 84 | if (serviceOptions.EnableSwagger == EnableSwagger.Always || 85 | (serviceOptions.EnableSwagger == EnableSwagger.DevelopmentOnly && webApp.Environment.IsDevelopment())) 86 | { 87 | applicationBuilder.UseSwagger(); 88 | applicationBuilder.UseSwaggerUI(); 89 | } 90 | 91 | var ctx = applicationBuilder.ApplicationServices.CreateScope().ServiceProvider.GetService(typeof(D)) as D; 92 | var builder = new InstantAPIsConfigBuilder(ctx); 93 | if (options != null) 94 | { 95 | options(builder); 96 | Configuration = builder.Build(); 97 | } 98 | } 99 | 100 | internal static IEnumerable GetDbTablesForContext() where D : DbContext 101 | { 102 | return typeof(D).GetProperties(BindingFlags.Instance | BindingFlags.Public) 103 | .Where(x => x.PropertyType.FullName.StartsWith("Microsoft.EntityFrameworkCore.DbSet") 104 | && x.PropertyType.GenericTypeArguments.First().GetCustomAttributes(typeof(KeylessAttribute), true).Length <= 0) 105 | .Select(x => new TypeTable { 106 | Name = x.Name, 107 | InstanceType = x.PropertyType.GenericTypeArguments.First(), 108 | BaseUrl = new Uri($"/api/{x.Name}", uriKind: UriKind.RelativeOrAbsolute) 109 | }) 110 | .ToArray(); 111 | } 112 | 113 | internal class TypeTable 114 | { 115 | public string Name { get; set; } 116 | public Type InstanceType { get; set; } 117 | public ApiMethodsToGenerate ApiMethodsToGenerate { get; set; } = ApiMethodsToGenerate.All; 118 | public Uri BaseUrl { get; set; } 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jeffrey T. Fritz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # InstantAPIs 2 | 3 | [![Nuget](https://img.shields.io/nuget/v/InstantAPIs)](https://www.nuget.org/packages/InstantAPIs/) 4 | [![Instant APIs Documentation](https://img.shields.io/badge/docs-ready!-blue)](https://csharpfritz.github.io/InstantAPIs) 5 | ![GitHub last commit](https://img.shields.io/github/last-commit/csharpfritz/InstantAPIs) 6 | ![GitHub contributors](https://img.shields.io/github/contributors/csharpfritz/InstantAPIs) 7 | 8 | This article contains two different ways to get an instant API: 9 | 10 | - An API based on a `DbContext`, it will generate the routes it needs given a database class. There are two implementations of this: Reflection and a source generator. 11 | - An API based on a JSON file. 12 | 13 | ## DbContext based API 14 | 15 | A proof-of-concept library that generates Minimal API endpoints for an Entity Framework context. Right now, there are two implementations of this: one that uses Reflection (this is the one currently in the NuGet package), the other uses a source generator. Let's see how both of them work. 16 | 17 | For a given Entity Framework context, `MyContext` 18 | 19 | ```csharp 20 | public class MyContext : DbContext 21 | { 22 | public MyContext(DbContextOptions options) : base(options) {} 23 | 24 | public DbSet Contacts => Set(); 25 | public DbSet
Addresses => Set
(); 26 | 27 | } 28 | ``` 29 | 30 | We can generate all of the standard CRUD API endpoints with the Reflection approach using this syntax in `Program.cs` 31 | 32 | ```csharp 33 | var builder = WebApplication.CreateBuilder(args); 34 | 35 | builder.Services.AddSqlite("Data Source=contacts.db"); 36 | 37 | var app = builder.Build(); 38 | 39 | app.MapInstantAPIs(); 40 | 41 | app.Run(); 42 | ``` 43 | 44 | Now we can navigate to `/api/Contacts` and see all of the Contacts in the database. We can filter for a specific Contact by navigating to `/api/Contacts/1` to get just the first contact returned. We can also post to `/api/Contacts` and add a new Contact to the database. Since there are multiple `DbSet`, you can make the same calls to `/api/Addresses`. 45 | 46 | You can also customize the APIs if you want 47 | ```csharp 48 | app.MapInstantAPIs(config => 49 | { 50 | config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.All, "addressBook"); 51 | }); 52 | ``` 53 | 54 | This specifies that the all of the CRUD methods should be created for the `Contacts` table, and it prepends the routes with `addressBook`. 55 | 56 | The source generator approach has an example in the `WorkingApi.Generators` project (at the moment a NuGet package hasn't been created for this implementation). You specify which `DbContext` classes you want to map with the `InstantAPIsForDbContextAttribute`, For each context, an extension method named `Map{DbContextName}ToAPIs` is created. The end result is similar to the Reflection approach: 57 | 58 | ```csharp 59 | [assembly: InstantAPIsForDbContext(typeof(MyContext))] 60 | 61 | var builder = WebApplication.CreateBuilder(args); 62 | 63 | builder.Services.AddSqlite("Data Source=contacts.db"); 64 | 65 | var app = builder.Build(); 66 | 67 | app.MapMyContextToAPIs(); 68 | 69 | app.Run(); 70 | ``` 71 | 72 | You can also do customization as well 73 | ```csharp 74 | app.MapMyContextToAPIs(options => 75 | options.Include(MyContext.Contacts, "addressBook", ApisToGenerate.Get)); 76 | ``` 77 | 78 | Feel free to try both approaches and let Fritz know if you have any issues with either one of them. The intent is to keep feature parity between the two for the forseable future. 79 | 80 | ### Demo 81 | 82 | Check out Fritz giving a demo, showing the advantage of InstantAPIs on YouTube: https://youtu.be/vCSWXAOEpBo 83 | 84 | 85 | 86 | ## A JSON based API 87 | 88 | An API will be generated based on JSON file, for now it needs to be named *mock.json*. 89 | 90 | A typical content in *mock.json* looks like so: 91 | 92 | ```json 93 | { 94 | "products" : [{ 95 | "id": 1, 96 | "name": "pizza" 97 | }, { 98 | "id": 2, 99 | "name": "pineapple pizza" 100 | }, { 101 | "id": 3, 102 | "name": "meat pizza" 103 | }], 104 | "customers" : [{ 105 | "id": 1, 106 | "name": "customer1" 107 | }] 108 | 109 | } 110 | ``` 111 | 112 | The above JSON will create the following routes: 113 | 114 | |HTTP Verb |Endpoint | 115 | |---------|---------| 116 | | GET | /products | 117 | | GET | /products/{id} | 118 | | POST | /products | 119 | | DELETE | /products/{id} | 120 | | GET | /customers | 121 | | GET | /customers/{id} | 122 | | POST | /customers | 123 | | DELETE | /customers/{id} | 124 | 125 | ### Demo 126 | 127 | To use this, do the following: 128 | 129 | 1. Create a new minimal API, if you don't already have one: 130 | 131 | ```bash 132 | dotnet new web -o DemoApi -f net6.0 133 | cd DemoApi 134 | ``` 135 | 136 | 1. Add the NuGet package for [InstantAPIs](https://www.nuget.org/packages/InstantAPIs/): 137 | 138 | ```bash 139 | dotnet add package InstantAPIs --prerelease 140 | ``` 141 | 142 | 1. In *Program.cs*, add the following namespace: 143 | 144 | ```csharp 145 | using Mock; 146 | ``` 147 | 148 | 1. Create a file *mock.json* and give it for example the following content: 149 | 150 | ```json 151 | { 152 | "products" : [{ 153 | "id": 1, 154 | "name": "pizza" 155 | }] 156 | } 157 | ``` 158 | 159 | 1. Now add the following code for the routes to be created: 160 | 161 | ```csharp 162 | app.UseJsonRoutes(); 163 | ``` 164 | 165 | Here's an example program: 166 | 167 | ```csharp 168 | using Mock; 169 | 170 | var builder = WebApplication.CreateBuilder(args); 171 | var app = builder.Build(); 172 | 173 | app.MapGet("/", () => "Hello World!"); 174 | app.UseJsonRoutes(); 175 | app.Run(); 176 | ``` 177 | 178 | ### Coming features 179 | 180 | Support for: 181 | 182 | - [query parameters](https://github.com/csharpfritz/InstantAPIs/issues/40) 183 | - [PUT](https://github.com/csharpfritz/InstantAPIs/issues/39) 184 | 185 | ## Community 186 | 187 | This project is covered by a [code of conduct](https://github.com/csharpfritz/InstantAPIs/blob/main/CODE-OF-CONDUCT.md) that all contributors must abide by. [Contributions are welcome and encouraged.](https://github.com/csharpfritz/InstantAPIs/blob/main/CONTRIBUTING.md). 188 | -------------------------------------------------------------------------------- /RELEASE_NOTES.txt: -------------------------------------------------------------------------------- 1 | v0.2.1 2 | - Enabled PUT methods with the JSON API 3 | v0.2.0 4 | - Introduced a fluent configuration API for defining which tables to include and exclude from the Entity Framework Context. 5 | - Can specify the BaseUrl for a table's APIs 6 | - Added the ability to create APIs from JSON files on disk 7 | - Added ability to specify OpenAPI configuration 8 | - Added Logging for reflection API generation 9 | - Added Codegen APIs 10 | - Added JSON mock APIs -------------------------------------------------------------------------------- /Test/BaseFixture.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.Extensions.Options; 4 | using Moq; 5 | 6 | namespace Test; 7 | 8 | public abstract class BaseFixture 9 | { 10 | 11 | protected MockRepository Mockery { get; private set; } = new MockRepository(MockBehavior.Loose); 12 | 13 | } -------------------------------------------------------------------------------- /Test/Configuration/WhenIncludeDoesNotSpecifyBaseUrl.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.EntityFrameworkCore; 4 | using Xunit; 5 | 6 | namespace Test.Configuration; 7 | 8 | public class WhenIncludeDoesNotSpecifyBaseUrl : BaseFixture 9 | { 10 | 11 | InstantAPIsConfigBuilder _Builder; 12 | 13 | public WhenIncludeDoesNotSpecifyBaseUrl() 14 | { 15 | 16 | var _ContextOptions = new DbContextOptionsBuilder() 17 | .UseInMemoryDatabase("TestDb") 18 | .Options; 19 | _Builder = new(new(_ContextOptions)); 20 | 21 | } 22 | 23 | [Fact] 24 | public void ShouldSpecifyDefaultUrl() 25 | { 26 | 27 | // arrange 28 | 29 | // act 30 | _Builder.IncludeTable(db => db.Contacts); 31 | var config = _Builder.Build(); 32 | 33 | // assert 34 | Assert.Single(config.Tables); 35 | Assert.Equal(new Uri("/api/Contacts", uriKind: UriKind.Relative), config.Tables.First().BaseUrl); 36 | 37 | } 38 | 39 | 40 | } 41 | 42 | 43 | -------------------------------------------------------------------------------- /Test/Configuration/WhenIncludeSpecifiesBaseUrl.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.EntityFrameworkCore; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace Test.Configuration; 12 | 13 | 14 | public class WhenIncludeSpecifiesBaseUrl : BaseFixture 15 | { 16 | 17 | InstantAPIsConfigBuilder _Builder; 18 | 19 | public WhenIncludeSpecifiesBaseUrl() 20 | { 21 | 22 | var _ContextOptions = new DbContextOptionsBuilder() 23 | .UseInMemoryDatabase("TestDb") 24 | .Options; 25 | _Builder = new(new(_ContextOptions)); 26 | 27 | } 28 | 29 | [Fact] 30 | public void ShouldSpecifyThatUrl() 31 | { 32 | 33 | // arrange 34 | 35 | // act 36 | var BaseUrl = new Uri("/testapi", UriKind.Relative); 37 | _Builder.IncludeTable(db => db.Contacts, baseUrl: BaseUrl.ToString()); 38 | var config = _Builder.Build(); 39 | 40 | // assert 41 | Assert.Single(config.Tables); 42 | Assert.Equal(BaseUrl, config.Tables.First().BaseUrl); 43 | 44 | } 45 | 46 | 47 | } 48 | 49 | 50 | -------------------------------------------------------------------------------- /Test/Configuration/WithIncludesAndExcludes.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.EntityFrameworkCore; 4 | using Xunit; 5 | 6 | namespace Test.Configuration; 7 | 8 | public class WithIncludesAndExcludes : BaseFixture 9 | { 10 | 11 | InstantAPIsConfigBuilder _Builder; 12 | 13 | public WithIncludesAndExcludes() 14 | { 15 | 16 | var _ContextOptions = new DbContextOptionsBuilder() 17 | .UseInMemoryDatabase("TestDb") 18 | .Options; 19 | _Builder = new(new(_ContextOptions)); 20 | 21 | } 22 | 23 | [Fact] 24 | public void ShouldExcludePreviouslyIncludedTable() 25 | { 26 | 27 | // arrange 28 | 29 | // act 30 | _Builder.IncludeTable(db => db.Addresses) 31 | .IncludeTable(db => db.Contacts) 32 | .ExcludeTable(db => db.Addresses); 33 | var config = _Builder.Build(); 34 | 35 | // assert 36 | Assert.Single(config.Tables); 37 | Assert.Equal("Contacts", config.Tables.First().Name); 38 | 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /Test/Configuration/WithOnlyExcludes.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.EntityFrameworkCore; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | namespace Test.Configuration; 8 | 9 | public class WithOnlyExcludes : BaseFixture 10 | { 11 | 12 | InstantAPIsConfigBuilder _Builder; 13 | 14 | public WithOnlyExcludes() 15 | { 16 | 17 | var _ContextOptions = new DbContextOptionsBuilder() 18 | .UseInMemoryDatabase("TestDb") 19 | .Options; 20 | _Builder = new(new(_ContextOptions)); 21 | 22 | } 23 | 24 | [Fact] 25 | public void ShouldExcludeSpecifiedTable() 26 | { 27 | 28 | // arrange 29 | 30 | // act 31 | _Builder.ExcludeTable(db => db.Addresses); 32 | var config = _Builder.Build(); 33 | 34 | // assert 35 | Assert.Single(config.Tables); 36 | Assert.Equal("Contacts", config.Tables.First().Name); 37 | 38 | } 39 | 40 | [Fact] 41 | public void ShouldThrowAnErrorIfAllTablesExcluded() 42 | { 43 | 44 | // arrange 45 | 46 | // act 47 | _Builder.ExcludeTable(db => db.Addresses) 48 | .ExcludeTable(db => db.Contacts); 49 | 50 | Assert.Throws(() => _Builder.Build()); 51 | 52 | } 53 | 54 | } 55 | 56 | -------------------------------------------------------------------------------- /Test/Configuration/WithOnlyIncludes.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.EntityFrameworkCore; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using Xunit; 10 | 11 | namespace Test.Configuration; 12 | 13 | public class WithOnlyIncludes : BaseFixture 14 | { 15 | 16 | InstantAPIsConfigBuilder _Builder; 17 | 18 | public WithOnlyIncludes() 19 | { 20 | 21 | var _ContextOptions = new DbContextOptionsBuilder() 22 | .UseInMemoryDatabase("TestDb") 23 | .Options; 24 | _Builder = new(new(_ContextOptions)); 25 | 26 | } 27 | 28 | [Fact] 29 | public void ShouldNotIncludeAllTables() 30 | { 31 | 32 | // arrange 33 | 34 | // act 35 | _Builder.IncludeTable(db => db.Contacts); 36 | var config = _Builder.Build(); 37 | 38 | // assert 39 | Assert.Single(config.Tables); 40 | Assert.Equal("Contacts", config.Tables.First().Name); 41 | 42 | } 43 | 44 | [Theory] 45 | [InlineData(ApiMethodsToGenerate.GetById | ApiMethodsToGenerate.Get)] 46 | [InlineData(ApiMethodsToGenerate.GetById | ApiMethodsToGenerate.Insert)] 47 | [InlineData(ApiMethodsToGenerate.GetById | ApiMethodsToGenerate.Insert | ApiMethodsToGenerate.Update)] 48 | public void ShouldIncludeAndSetAPIMethodsToInclude(ApiMethodsToGenerate methodsToGenerate) 49 | { 50 | 51 | // arrange 52 | 53 | // act 54 | _Builder.IncludeTable(db => db.Contacts, methodsToGenerate); 55 | var config = _Builder.Build(); 56 | 57 | // assert 58 | Assert.Equal(methodsToGenerate, config.Tables.First().ApiMethodsToGenerate); 59 | 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /Test/Configuration/WithoutIncludes.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.EntityFrameworkCore; 4 | using System.Linq; 5 | using Xunit; 6 | 7 | namespace Test.Configuration; 8 | 9 | public class WithoutIncludes : BaseFixture 10 | { 11 | 12 | InstantAPIsConfigBuilder _Builder; 13 | 14 | public WithoutIncludes() 15 | { 16 | 17 | var _ContextOptions = new DbContextOptionsBuilder() 18 | .UseInMemoryDatabase("TestDb") 19 | .Options; 20 | _Builder = new(new(_ContextOptions)); 21 | 22 | } 23 | 24 | 25 | [Fact] 26 | public void ShouldIncludeAllTables() 27 | { 28 | 29 | // arrange 30 | 31 | // act 32 | var config = _Builder.Build(); 33 | 34 | // assert 35 | Assert.Equal(2, config.Tables.Count); 36 | Assert.Equal(ApiMethodsToGenerate.All, config.Tables.First().ApiMethodsToGenerate); 37 | Assert.Equal(ApiMethodsToGenerate.All, config.Tables.Skip(1).First().ApiMethodsToGenerate); 38 | 39 | } 40 | 41 | } 42 | 43 | -------------------------------------------------------------------------------- /Test/GlobalSurpressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. 5 | 6 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Readability", "RCS1018:Add default access modifier.", Justification = "")] 7 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Maintainability", "RCS1141:Add parameter to documentation comment.", Justification = "")] 8 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Redundancy", "RCS1163:Unused parameter.", Justification = "")] 9 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "RCS1090:Call 'ConfigureAwait(false)'.", Justification = "No SynchronizationContext in AspNet core")] 10 | [assembly: System.Diagnostics.CodeAnalysis.SuppressMessage("Style", "IDE0007:Use implicit type", Justification = "Dont want to use var for bool, int ...")] 11 | -------------------------------------------------------------------------------- /Test/InstantAPIs/WebApplicationExtensions.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs; 2 | using Microsoft.AspNetCore.Builder; 3 | using Microsoft.AspNetCore.Routing; 4 | using System.Collections.Generic; 5 | using Xunit; 6 | 7 | namespace Test.InstantAPIs; 8 | 9 | public class WebApplicationExtensions : BaseFixture 10 | { 11 | 12 | [Fact] 13 | public void WhenMapInstantAPIsExpectedDefaultBehaviour() 14 | { 15 | 16 | // arrange 17 | var app = Mockery.Create(); 18 | var dataSources = new List(); 19 | app.Setup(x => x.DataSources).Returns(dataSources); 20 | 21 | // act 22 | app.Object.MapInstantAPIs(); 23 | 24 | // assert 25 | Assert.NotEmpty(dataSources); 26 | Assert.Equal(10, dataSources[0].Endpoints.Count); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /Test/StubData/Address.cs: -------------------------------------------------------------------------------- 1 | namespace Test.StubData 2 | { 3 | public class Address 4 | { 5 | 6 | public int Id { get; set; } 7 | 8 | public string? AddressLine1 { get; set; } 9 | 10 | public string AddressLine2 { get; set; } = string.Empty; 11 | 12 | public string? City { get; set; } 13 | 14 | public string? Region { get; set; } 15 | 16 | public string? PostalCode { get; set; } 17 | 18 | public string? Country { get; set; } 19 | 20 | } 21 | } -------------------------------------------------------------------------------- /Test/StubData/Contact.cs: -------------------------------------------------------------------------------- 1 | namespace Test.StubData; 2 | 3 | internal class Contact 4 | { 5 | 6 | public int Id { get; set; } 7 | public string? Name { get; set; } 8 | public string? Email { get; set; } 9 | 10 | } -------------------------------------------------------------------------------- /Test/StubData/MyContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace Test.StubData; 4 | 5 | internal class MyContext : DbContext 6 | { 7 | 8 | public MyContext(DbContextOptions options) : base(options) { } 9 | 10 | public DbSet Contacts => Set(); 11 | 12 | public DbSet
Addresses => Set
(); 13 | 14 | } 15 | -------------------------------------------------------------------------------- /Test/Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | all 19 | 20 | 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | all 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /Test/XunitLogger.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using System; 3 | using Xunit.Abstractions; 4 | 5 | namespace Test; 6 | 7 | public class XunitLogger : ILogger, IDisposable 8 | { 9 | private ITestOutputHelper _output; 10 | 11 | public XunitLogger(ITestOutputHelper output) 12 | { 13 | _output = output; 14 | } 15 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 16 | { 17 | _output.WriteLine(state.ToString()); 18 | } 19 | 20 | public bool IsEnabled(LogLevel logLevel) 21 | { 22 | return true; 23 | } 24 | 25 | public IDisposable BeginScope(TState state) 26 | { 27 | return this; 28 | } 29 | 30 | public void Dispose() 31 | { 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /TestJson/Program.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs; 2 | 3 | var builder = WebApplication.CreateBuilder(args); 4 | var app = builder.Build(); 5 | 6 | app.MapGet("/", () => "Hello World!"); 7 | app.UseJsonRoutes(); 8 | app.Run(); 9 | -------------------------------------------------------------------------------- /TestJson/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:3611", 7 | "sslPort": 44394 8 | } 9 | }, 10 | "profiles": { 11 | "TestJson": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "applicationUrl": "https://localhost:7045;http://localhost:5263", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "IIS Express": { 21 | "commandName": "IISExpress", 22 | "launchBrowser": true, 23 | "environmentVariables": { 24 | "ASPNETCORE_ENVIRONMENT": "Development" 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /TestJson/TestJson.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | net6.0 9 | enable 10 | enable 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /TestJson/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /TestJson/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /TestJson/mock.json: -------------------------------------------------------------------------------- 1 | { 2 | "products" : [{ 3 | "id": 1, 4 | "name": "pizza" 5 | }] 6 | } -------------------------------------------------------------------------------- /WorkingApi.Generators/MyContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace WorkingApi; 4 | 5 | public sealed class MyContext : DbContext 6 | { 7 | public MyContext() { } 8 | 9 | public MyContext(DbContextOptions options) : base(options) {} 10 | 11 | public DbSet Contacts => Set(); 12 | 13 | public DbSet Orders => Set(); 14 | } 15 | 16 | public sealed class Contact 17 | { 18 | public int Id { get; set; } 19 | public string? Name { get; set; } 20 | public string? Email { get; set; } 21 | } 22 | 23 | public sealed class Order 24 | { 25 | public int Id { get; set; } 26 | public string? Name { get; set; } 27 | } 28 | -------------------------------------------------------------------------------- /WorkingApi.Generators/Program.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs.Generators.Helpers; 2 | using Microsoft.EntityFrameworkCore; 3 | using WorkingApi; 4 | 5 | [assembly: InstantAPIsForDbContext(typeof(MyContext))] 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | builder.Services.AddDbContext( 10 | options => options.UseInMemoryDatabase("Test")); 11 | builder.Services.AddEndpointsApiExplorer(); 12 | builder.Services.AddSwaggerGen(); 13 | 14 | var app = builder.Build(); 15 | 16 | using (var scope = app.Services.CreateScope()) 17 | { 18 | await SetupMyContextAsync(scope.ServiceProvider.GetService()!); 19 | } 20 | 21 | // This is the configured version. 22 | /* 23 | app.MapMyContextToAPIs(options => 24 | options.Include(MyContextTables.Contacts, "Contacts", ApisToGenerate.Get) 25 | .Exclude(MyContextTables.Orders)); 26 | */ 27 | 28 | // This is the simple "configure everything" version. 29 | app.MapMyContextToAPIs(); 30 | 31 | app.UseSwagger(); 32 | app.UseSwaggerUI(); 33 | 34 | app.Run(); 35 | 36 | static async Task SetupMyContextAsync(MyContext context) 37 | { 38 | await context.Contacts.AddAsync(new Contact 39 | { 40 | Id = 1, 41 | Name = "Jason", 42 | Email = "jason@bock.com" 43 | }); 44 | 45 | await context.Contacts.AddAsync(new Contact 46 | { 47 | Id = 2, 48 | Name = "Jeff", 49 | Email = "jeff@fritz.com" 50 | }); 51 | 52 | await context.SaveChangesAsync(); 53 | } -------------------------------------------------------------------------------- /WorkingApi.Generators/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:47282", 7 | "sslPort": 0 8 | } 9 | }, 10 | "profiles": { 11 | "WorkingApi.Generators": { 12 | "commandName": "Project", 13 | "dotnetRunMessages": true, 14 | "launchBrowser": true, 15 | "launchUrl": "swagger/index.html", 16 | "applicationUrl": "http://localhost:5215", 17 | "environmentVariables": { 18 | "ASPNETCORE_ENVIRONMENT": "Development" 19 | } 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /WorkingApi.Generators/WorkingApi.Generators.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0 4 | enable 5 | enable 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /WorkingApi.Generators/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /WorkingApi.Generators/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /WorkingApi/Migrations/20220217005021_Init.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Migrations; 5 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 6 | using WorkingApi; 7 | 8 | #nullable disable 9 | 10 | namespace WorkingApi.Migrations 11 | { 12 | [DbContext(typeof(MyContext))] 13 | [Migration("20220217005021_Init")] 14 | partial class Init 15 | { 16 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 17 | { 18 | #pragma warning disable 612, 618 19 | modelBuilder.HasAnnotation("ProductVersion", "6.0.2"); 20 | 21 | modelBuilder.Entity("WorkingApi.Contact", b => 22 | { 23 | b.Property("Id") 24 | .ValueGeneratedOnAdd() 25 | .HasColumnType("INTEGER"); 26 | 27 | b.Property("Email") 28 | .HasColumnType("TEXT"); 29 | 30 | b.Property("Name") 31 | .HasColumnType("TEXT"); 32 | 33 | b.HasKey("Id"); 34 | 35 | b.ToTable("Contacts"); 36 | }); 37 | #pragma warning restore 612, 618 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /WorkingApi/Migrations/20220217005021_Init.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | #nullable disable 4 | 5 | namespace WorkingApi.Migrations 6 | { 7 | public partial class Init : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Contacts", 13 | columns: table => new 14 | { 15 | Id = table.Column(type: "INTEGER", nullable: false) 16 | .Annotation("Sqlite:Autoincrement", true), 17 | Name = table.Column(type: "TEXT", nullable: true), 18 | Email = table.Column(type: "TEXT", nullable: true) 19 | }, 20 | constraints: table => 21 | { 22 | table.PrimaryKey("PK_Contacts", x => x.Id); 23 | }); 24 | } 25 | 26 | protected override void Down(MigrationBuilder migrationBuilder) 27 | { 28 | migrationBuilder.DropTable( 29 | name: "Contacts"); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /WorkingApi/Migrations/MyContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 5 | using WorkingApi; 6 | 7 | #nullable disable 8 | 9 | namespace WorkingApi.Migrations 10 | { 11 | [DbContext(typeof(MyContext))] 12 | partial class MyContextModelSnapshot : ModelSnapshot 13 | { 14 | protected override void BuildModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder.HasAnnotation("ProductVersion", "6.0.2"); 18 | 19 | modelBuilder.Entity("WorkingApi.Contact", b => 20 | { 21 | b.Property("Id") 22 | .ValueGeneratedOnAdd() 23 | .HasColumnType("INTEGER"); 24 | 25 | b.Property("Email") 26 | .HasColumnType("TEXT"); 27 | 28 | b.Property("Name") 29 | .HasColumnType("TEXT"); 30 | 31 | b.HasKey("Id"); 32 | 33 | b.ToTable("Contacts"); 34 | }); 35 | #pragma warning restore 612, 618 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /WorkingApi/MyContext.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | 3 | namespace WorkingApi; 4 | 5 | public class MyContext : DbContext 6 | { 7 | public MyContext(DbContextOptions options) : base(options) {} 8 | 9 | public DbSet Contacts => Set(); 10 | 11 | } 12 | 13 | public class Contact 14 | { 15 | 16 | public int Id { get; set; } 17 | public string? Name { get; set; } 18 | public string? Email { get; set; } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /WorkingApi/Program.cs: -------------------------------------------------------------------------------- 1 | using InstantAPIs; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.OpenApi.Models; 4 | using System.Diagnostics; 5 | using WorkingApi; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | builder.Services.AddSqlite("Data Source=contacts.db"); 10 | builder.Services.AddInstantAPIs(); 11 | 12 | var app = builder.Build(); 13 | 14 | var sw = Stopwatch.StartNew(); 15 | 16 | app.MapInstantAPIs(config => 17 | { 18 | config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.All, "addressBook"); 19 | }); 20 | 21 | app.Run(); 22 | -------------------------------------------------------------------------------- /WorkingApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:57487", 7 | "sslPort": 44379 8 | } 9 | }, 10 | "profiles": { 11 | "WorkingApi": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "launchUrl": "swagger/index.html", 15 | "environmentVariables": { 16 | "ASPNETCORE_ENVIRONMENT": "Development" 17 | }, 18 | "applicationUrl": "https://localhost:7192;http://localhost:5193", 19 | "dotnetRunMessages": true 20 | }, 21 | "IIS Express": { 22 | "commandName": "IISExpress", 23 | "launchBrowser": true, 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | } 27 | }, 28 | "Docker": { 29 | "commandName": "Docker", 30 | "launchBrowser": true, 31 | "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", 32 | "publishAllPorts": true, 33 | "useSSL": true 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /WorkingApi/WorkingApi.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 91f999d8-46c2-4342-97b0-4753f599211c 8 | 9 | 10 | 11 | 12 | 13 | runtime; build; native; contentfiles; analyzers; buildtransitive 14 | all 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /WorkingApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /WorkingApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /WorkingApi/contacts.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/csharpfritz/InstantAPIs/a8f3187177708b37caeeb5ae9d814c1529165bec/WorkingApi/contacts.db -------------------------------------------------------------------------------- /docs/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM squidfunk/mkdocs-material:5.5.0 2 | 3 | RUN \ 4 | apk add --no-cache \ 5 | git \ 6 | git-fast-import \ 7 | openssh \ 8 | && apk add --no-cache --virtual .build gcc musl-dev \ 9 | && pip install --no-cache-dir \ 10 | 'markdown-include' \ 11 | 'mike' \ 12 | 'mkdocs-exclude' \ 13 | 'mkdocs-macros-plugin' \ 14 | && apk del .build gcc musl-dev \ 15 | && rm -rf /tmp/* 16 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | This article contains two different ways to get an instant API: 2 | 3 | - An API based on a `DbContext`, it will generate the routes it needs given a database class. There are two implementations of this: Reflection and a source generator. 4 | - An API based on a JSON file. 5 | 6 | ## DbContext based API 7 | 8 | A proof-of-concept library that generates Minimal API endpoints for an Entity Framework context. Right now, there are two implementations of this: one that uses Reflection (this is the one currently in the NuGet package), the other uses a source generator. Let's see how both of them work. 9 | 10 | For a given Entity Framework context, `MyContext` 11 | 12 | ```csharp 13 | public class MyContext : DbContext 14 | { 15 | public MyContext(DbContextOptions options) : base(options) {} 16 | 17 | public DbSet Contacts => Set(); 18 | public DbSet
Addresses => Set
(); 19 | 20 | } 21 | ``` 22 | 23 | We can generate all of the standard CRUD API endpoints with the Reflection approach using this syntax in `Program.cs` 24 | 25 | ```csharp 26 | var builder = WebApplication.CreateBuilder(args); 27 | 28 | builder.Services.AddSqlite("Data Source=contacts.db"); 29 | 30 | var app = builder.Build(); 31 | 32 | app.MapInstantAPIs(); 33 | 34 | app.Run(); 35 | ``` 36 | 37 | Now we can navigate to `/api/Contacts` and see all of the Contacts in the database. We can filter for a specific Contact by navigating to `/api/Contacts/1` to get just the first contact returned. We can also post to `/api/Contacts` and add a new Contact to the database. Since there are multiple `DbSet`, you can make the same calls to `/api/Addresses`. 38 | 39 | You can also customize the APIs if you want 40 | ```csharp 41 | app.MapInstantAPIs(config => 42 | { 43 | config.IncludeTable(db => db.Contacts, ApiMethodsToGenerate.All, "addressBook"); 44 | }); 45 | ``` 46 | 47 | This specifies that the all of the CRUD methods should be created for the `Contacts` table, and it prepends the routes with `addressBook`. 48 | 49 | The source generator approach has an example in the `WorkingApi.Generators` project (at the moment a NuGet package hasn't been created for this implementation). You specify which `DbContext` classes you want to map with the `InstantAPIsForDbContextAttribute`, For each context, an extension method named `Map{DbContextName}ToAPIs` is created. The end result is similar to the Reflection approach: 50 | 51 | ```csharp 52 | [assembly: InstantAPIsForDbContext(typeof(MyContext))] 53 | 54 | var builder = WebApplication.CreateBuilder(args); 55 | 56 | builder.Services.AddSqlite("Data Source=contacts.db"); 57 | 58 | var app = builder.Build(); 59 | 60 | app.MapMyContextToAPIs(); 61 | 62 | app.Run(); 63 | ``` 64 | 65 | You can also do customization as well 66 | ```csharp 67 | app.MapMyContextToAPIs(options => 68 | options.Include(MyContext.Contacts, "addressBook", ApisToGenerate.Get)); 69 | ``` 70 | 71 | Feel free to try both approaches and let Fritz know if you have any issues with either one of them. The intent is to keep feature parity between the two for the forseable future. 72 | 73 | ### Demo 74 | 75 | Check out Fritz giving a demo, showing the advantage of InstantAPIs on YouTube: https://youtu.be/vCSWXAOEpBo 76 | 77 | 78 | 79 | ## A JSON based API 80 | 81 | An API will be generated based on JSON file, for now it needs to be named *mock.json*. 82 | 83 | A typical content in *mock.json* looks like so: 84 | 85 | ```json 86 | { 87 | "products" : [{ 88 | "id": 1, 89 | "name": "pizza" 90 | }, { 91 | "id": 2, 92 | "name": "pineapple pizza" 93 | }, { 94 | "id": 3, 95 | "name": "meat pizza" 96 | }], 97 | "customers" : [{ 98 | "id": 1, 99 | "name": "customer1" 100 | }] 101 | 102 | } 103 | ``` 104 | 105 | The above JSON will create the following routes: 106 | 107 | |HTTP Verb |Endpoint | 108 | |---------|---------| 109 | | GET | /products | 110 | | GET | /products/{id} | 111 | | POST | /products | 112 | | DELETE | /products/{id} | 113 | | GET | /customers | 114 | | GET | /customers/{id} | 115 | | POST | /customers | 116 | | DELETE | /customers/{id} | 117 | 118 | ### Demo 119 | 120 | To use this, do the following: 121 | 122 | 1. Create a new minimal API, if you don't already have one: 123 | 124 | ```bash 125 | dotnet new web -o DemoApi -f net6.0 126 | cd DemoApi 127 | ``` 128 | 129 | 1. Add the NuGet package for [InstantAPIs](https://www.nuget.org/packages/InstantAPIs/): 130 | 131 | ```bash 132 | dotnet add package InstantAPIs --prerelease 133 | ``` 134 | 135 | 1. In *Program.cs*, add the following namespace: 136 | 137 | ```csharp 138 | using Mock; 139 | ``` 140 | 141 | 1. Create a file *mock.json* and give it for example the following content: 142 | 143 | ```json 144 | { 145 | "products" : [{ 146 | "id": 1, 147 | "name": "pizza" 148 | }] 149 | } 150 | ``` 151 | 152 | 1. Now add the following code for the routes to be created: 153 | 154 | ```csharp 155 | app.UseJsonRoutes(); 156 | ``` 157 | 158 | Here's an example program: 159 | 160 | ```csharp 161 | using Mock; 162 | 163 | var builder = WebApplication.CreateBuilder(args); 164 | var app = builder.Build(); 165 | 166 | app.MapGet("/", () => "Hello World!"); 167 | app.UseJsonRoutes(); 168 | app.Run(); 169 | ``` 170 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: InstantAPIs 2 | site_description: A library to generate RESTful APIs with 1 line of code 3 | site_author: Jeffrey T. Fritz 4 | site_url: https://csharpfritz.github.io/InstantAPIs/ 5 | edit_uri: edit/dev/docs/ 6 | docs_dir: docs 7 | 8 | repo_name: csharpfritz/InstantAPIs 9 | repo_url: https://github.com/csharpfritz/InstantAPIs 10 | 11 | extra: 12 | social: 13 | - icon: fontawesome/brands/github-alt 14 | link: https://github.com/csharpfritz/InstantAPIs 15 | 16 | theme: 17 | name: material 18 | language: en 19 | palette: 20 | scheme: default 21 | primary: indigo 22 | accent: indigo 23 | font: 24 | text: Roboto 25 | code: Roboto Mono 26 | logo: assets/logo.png 27 | favicon: assets/favicon.ico 28 | include_search_page: false 29 | search_index_only: true 30 | 31 | extra_css: 32 | - assets/stylesheets/extra.css 33 | 34 | markdown_extensions: 35 | - admonition 36 | - codehilite 37 | - footnotes 38 | - markdown_include.include: 39 | base_path: docs 40 | - meta 41 | - pymdownx.details 42 | - pymdownx.tabbed 43 | - pymdownx.superfences 44 | - pymdownx.emoji: 45 | emoji_index: !!python/name:materialx.emoji.twemoji 46 | emoji_generator: !!python/name:materialx.emoji.to_svg 47 | - toc: 48 | permalink: true 49 | 50 | plugins: 51 | - exclude: 52 | glob: 53 | - "_overrides/*" 54 | - "Dockerfile" 55 | - git-revision-date-localized: 56 | type: iso_datetime 57 | - macros 58 | - search: 59 | prebuild_index: python 60 | lang: 61 | - en 62 | 63 | nav: 64 | - Home: README.md 65 | --------------------------------------------------------------------------------