├── .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 | [](https://www.nuget.org/packages/InstantAPIs/)
4 | [](https://csharpfritz.github.io/InstantAPIs)
5 | 
6 | 
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 |
--------------------------------------------------------------------------------