├── .editorconfig ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── config.yml │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── dotnetcore.yml │ ├── publish-analyzer.yml │ ├── publish-nswag.yml │ ├── publish-swashbuckle.yml │ ├── publish-template.yml │ └── publish.yml ├── .gitignore ├── Ardalis.ApiEndpoints.sln ├── CONTRIBUTING.md ├── Directory.Build.props ├── LICENSE.txt ├── README.md ├── docs ├── CNAME ├── _config.yml ├── extensions │ ├── extend-define-evaluators.md │ ├── extend-specification-builder.md │ ├── extensions-for-specifications.md │ └── index.md ├── favicon.ico ├── features │ ├── asnotracking.md │ ├── asnotrackingwithidentityresolution.md │ ├── assplitquery.md │ ├── base-features.md │ ├── caching.md │ ├── evaluate.md │ ├── ignorequeryfilters.md │ ├── include.md │ ├── index.md │ ├── orderby.md │ ├── orm-features.md │ ├── paging.md │ ├── postprocessingaction.md │ ├── search.md │ ├── select.md │ ├── skip.md │ ├── take.md │ ├── then-include.md │ └── where.md ├── getting-started │ ├── faq.md │ ├── index.md │ ├── patterns-used.md │ └── quick-start-guide.md ├── index.md ├── projects-using-specification │ └── index.md ├── related-resources │ ├── articles.md │ ├── index.md │ ├── podcasts.md │ ├── training.md │ └── videos.md └── usage │ ├── create-specifications.md │ ├── index.md │ ├── use-built-in-abstract-repository.md │ ├── use-specification-dbcontext.md │ ├── use-specification-inmemory-collection.md │ └── use-specification-repository-pattern.md ├── sample ├── Sample.FunctionalTests │ ├── AuthorEndpoints │ │ ├── CreateEndpoint.cs │ │ ├── DeleteEndpoint.cs │ │ ├── GetEndpoint.cs │ │ ├── ListEndpoint.cs │ │ ├── ListPaginatedEndpoint.cs │ │ ├── StreamEndpoint.cs │ │ └── UpdateEndpoint.cs │ ├── CustomWebApplicationFactory.cs │ ├── Models │ │ └── Routes.cs │ └── Sample.FunctionalTests.csproj ├── Sample.WeatherForecast │ ├── .template.config │ │ └── template.json │ ├── Endpoints │ │ └── Get.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Sample.WeatherForecast.csproj │ ├── WeatherForecast.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ └── templatepack.csproj ├── SampleEndpointApp │ ├── AutoMapping.cs │ ├── DataAccess │ │ ├── AppDbContext.cs │ │ ├── Config │ │ │ └── AuthorConfig.cs │ │ ├── EfRepository.cs │ │ ├── Migrations │ │ │ ├── 20200209060455_Initial.Designer.cs │ │ │ ├── 20200209060455_Initial.cs │ │ │ └── AppDbContextModelSnapshot.cs │ │ └── SeedData.cs │ ├── DomainModel │ │ ├── Author.cs │ │ ├── BaseEntity.cs │ │ └── IAsyncRepository.cs │ ├── Endpoints │ │ └── Authors │ │ │ ├── Create.CreateAuthorCommand.cs │ │ │ ├── Create.CreateAuthorResult.cs │ │ │ ├── Create.cs │ │ │ ├── Delete.DeleteAuthorRequest.cs │ │ │ ├── Delete.cs │ │ │ ├── Get.AuthorResult.cs │ │ │ ├── Get.cs │ │ │ ├── List.AuthorListRequest.cs │ │ │ ├── List.AuthorListResult.cs │ │ │ ├── List.cs │ │ │ ├── ListJsonFile.cs │ │ │ ├── Stream.cs │ │ │ ├── Update.UpdateAuthorCommand.cs │ │ │ ├── Update.UpdatedAuthorResult.cs │ │ │ ├── Update.cs │ │ │ ├── UpdateById.UpdateAuthorCommand.cs │ │ │ ├── UpdateById.UpdatedAuthorByIdResult.cs │ │ │ └── UpdateById.cs │ ├── FromMultiSourceAttribute.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── README.md │ ├── SampleEndpointApp.csproj │ ├── Startup.cs │ ├── appsettings.Development.json │ ├── appsettings.json │ └── database.sqlite └── httpCommands.rest ├── src ├── Ardalis.ApiEndpoints.CodeAnalyzers │ ├── Ardalis.ApiEndpoints.CodeAnalyzers.csproj │ ├── EndpointHasExtraPublicMethodAnalyzer.cs │ ├── EndpointHasExtraPublicMethodCodeFixProvider.cs │ ├── Extensions │ │ └── RoslynExtensions.cs │ └── tools │ │ ├── install.ps1 │ │ └── uninstall.ps1 ├── Ardalis.ApiEndpoints.NSwag │ ├── Ardalis.ApiEndpoints.NSwag.csproj │ └── Extensions │ │ └── NSwagSwaggerGeneratorSettingsExtensions.cs ├── Ardalis.ApiEndpoints.Swashbuckle │ ├── Ardalis.ApiEndpoints.Swashbuckle.csproj │ └── Extensions │ │ └── SwaggerGenOptionsExtensions.cs ├── Ardalis.ApiEndpoints │ ├── Ardalis.ApiEndpoints.csproj │ ├── EndpointBase.cs │ ├── Extensions │ │ ├── MvcOptionsExtensions.cs │ │ └── TypeExtensions.cs │ ├── FluentGenerics │ │ ├── EndpointBaseAsync.cs │ │ └── EndpointBaseSync.cs │ ├── LICENSE.txt │ └── Properties │ │ └── AssemblyInfo.cs └── Directory.Build.props └── tests └── Ardalis.ApiEndpoints.CodeAnalyzers.Test ├── Ardalis.ApiEndpoints.CodeAnalyzers.Test.csproj ├── EndpointHasPublicActionMethodTests.cs ├── Helpers ├── CodeFixVerifier.Helper.cs ├── DiagnosticResult.cs └── DiagnosticVerifier.Helper.cs └── Verifiers ├── CodeFixVerifier.cs └── DiagnosticVerifier.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | # To learn more about .editorconfig see https://aka.ms/editorconfigdocs 2 | ############################### 3 | # Core EditorConfig Options # 4 | ############################### 5 | # All files 6 | [*] 7 | indent_style = space 8 | # Code files 9 | [*.{cs,csx,vb,vbx}] 10 | indent_size = 2 11 | insert_final_newline = true 12 | charset = utf-8-bom 13 | ############################### 14 | # .NET Coding Conventions # 15 | ############################### 16 | [*.{cs,vb}] 17 | # Organize usings 18 | dotnet_sort_system_directives_first = true 19 | # this. preferences 20 | dotnet_style_qualification_for_field = false:silent 21 | dotnet_style_qualification_for_property = false:silent 22 | dotnet_style_qualification_for_method = false:silent 23 | dotnet_style_qualification_for_event = false:silent 24 | # Language keywords vs BCL types preferences 25 | dotnet_style_predefined_type_for_locals_parameters_members = true:silent 26 | dotnet_style_predefined_type_for_member_access = true:silent 27 | # Parentheses preferences 28 | dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent 29 | dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent 30 | dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent 31 | dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent 32 | # Modifier preferences 33 | dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent 34 | dotnet_style_readonly_field = true:suggestion 35 | # Expression-level preferences 36 | dotnet_style_object_initializer = true:suggestion 37 | dotnet_style_collection_initializer = true:suggestion 38 | dotnet_style_explicit_tuple_names = true:suggestion 39 | dotnet_style_null_propagation = true:suggestion 40 | dotnet_style_coalesce_expression = true:suggestion 41 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent 42 | dotnet_style_prefer_inferred_tuple_names = true:suggestion 43 | dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion 44 | dotnet_style_prefer_auto_properties = true:silent 45 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent 46 | dotnet_style_prefer_conditional_expression_over_return = true:silent 47 | # Namespace preferences 48 | csharp_style_namespace_declarations = file_scoped:warning 49 | ############################### 50 | # Naming Conventions # 51 | ############################### 52 | # Style Definitions 53 | dotnet_naming_style.pascal_case_style.capitalization = pascal_case 54 | # Use PascalCase for constant fields 55 | dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion 56 | dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields 57 | dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style 58 | dotnet_naming_symbols.constant_fields.applicable_kinds = field 59 | dotnet_naming_symbols.constant_fields.applicable_accessibilities = * 60 | dotnet_naming_symbols.constant_fields.required_modifiers = const 61 | tab_width= 2 62 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion 63 | dotnet_style_prefer_compound_assignment = true:suggestion 64 | dotnet_style_prefer_simplified_interpolation = true:suggestion 65 | dotnet_style_namespace_match_folder = true:suggestion 66 | dotnet_style_operator_placement_when_wrapping = beginning_of_line 67 | end_of_line = crlf 68 | ############################### 69 | # C# Coding Conventions # 70 | ############################### 71 | [*.cs] 72 | # var preferences 73 | csharp_style_var_for_built_in_types = true:silent 74 | csharp_style_var_when_type_is_apparent = true:silent 75 | csharp_style_var_elsewhere = true:silent 76 | # Expression-bodied members 77 | csharp_style_expression_bodied_methods = false:silent 78 | csharp_style_expression_bodied_constructors = false:silent 79 | csharp_style_expression_bodied_operators = false:silent 80 | csharp_style_expression_bodied_properties = true:silent 81 | csharp_style_expression_bodied_indexers = true:silent 82 | csharp_style_expression_bodied_accessors = true:silent 83 | # Pattern matching preferences 84 | csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion 85 | csharp_style_pattern_matching_over_as_with_null_check = true:suggestion 86 | # Null-checking preferences 87 | csharp_style_throw_expression = true:suggestion 88 | csharp_style_conditional_delegate_call = true:suggestion 89 | # Modifier preferences 90 | csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion 91 | # Expression-level preferences 92 | csharp_prefer_braces = true:silent 93 | csharp_style_deconstructed_variable_declaration = true:suggestion 94 | csharp_prefer_simple_default_expression = true:suggestion 95 | csharp_style_pattern_local_over_anonymous_function = true:suggestion 96 | csharp_style_inlined_variable_declaration = true:suggestion 97 | ############################### 98 | # C# Formatting Rules # 99 | ############################### 100 | # New line preferences 101 | csharp_new_line_before_open_brace = all 102 | csharp_new_line_before_else = true 103 | csharp_new_line_before_catch = true 104 | csharp_new_line_before_finally = true 105 | csharp_new_line_before_members_in_object_initializers = true 106 | csharp_new_line_before_members_in_anonymous_types = true 107 | csharp_new_line_between_query_expression_clauses = true 108 | # Indentation preferences 109 | csharp_indent_case_contents = true 110 | csharp_indent_switch_labels = true 111 | csharp_indent_labels = flush_left 112 | # Space preferences 113 | csharp_space_after_cast = false 114 | csharp_space_after_keywords_in_control_flow_statements = true 115 | csharp_space_between_method_call_parameter_list_parentheses = false 116 | csharp_space_between_method_declaration_parameter_list_parentheses = false 117 | csharp_space_between_parentheses = false 118 | csharp_space_before_colon_in_inheritance_clause = true 119 | csharp_space_after_colon_in_inheritance_clause = true 120 | csharp_space_around_binary_operators = before_and_after 121 | csharp_space_between_method_declaration_empty_parameter_list_parentheses = false 122 | csharp_space_between_method_call_name_and_opening_parenthesis = false 123 | csharp_space_between_method_call_empty_parameter_list_parentheses = false 124 | # Wrapping preferences 125 | csharp_preserve_single_line_statements = true 126 | csharp_preserve_single_line_blocks = true 127 | csharp_using_directive_placement = outside_namespace:silent 128 | csharp_prefer_simple_using_statement = true:suggestion 129 | csharp_style_namespace_declarations = file_scoped:warning 130 | csharp_style_prefer_method_group_conversion = true:silent 131 | csharp_style_prefer_top_level_statements = true:silent 132 | csharp_style_expression_bodied_lambdas = true:silent 133 | csharp_style_expression_bodied_local_functions = false:silent 134 | csharp_style_prefer_null_check_over_type_check = true:suggestion 135 | csharp_style_prefer_local_over_anonymous_function = true:suggestion 136 | csharp_style_prefer_index_operator = true:suggestion 137 | csharp_style_prefer_range_operator = true:suggestion 138 | csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion 139 | csharp_style_prefer_tuple_swap = true:suggestion 140 | csharp_style_prefer_utf8_string_literals = true:suggestion 141 | csharp_style_unused_value_assignment_preference = discard_variable:suggestion 142 | ############################### 143 | # VB Coding Conventions # 144 | ############################### 145 | [*.vb] 146 | # Modifier preferences 147 | visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion 148 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ardalis 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | --- 5 | 6 | 7 | - NuGet Package Version: 8 | - .NET SDK Version: 9 | 10 | Steps to Reproduce: 11 | 12 | 1. 13 | 2. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Question 4 | url: https://stackoverflow.com/questions/tagged/ardalis-api-endpoints 5 | about: Please ask and answer questions on Stack Overflow. 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | open-pull-requests-limit: 0 8 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL Analysis 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 8 1 * *' 8 | 9 | jobs: 10 | analyze: 11 | name: CodeQL Analysis 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout repository 15 | id: checkout_repo 16 | uses: actions/checkout@v2 17 | 18 | - name: Initialize CodeQL 19 | id: init_codeql 20 | uses: github/codeql-action/init@v1 21 | with: 22 | queries: security-and-quality 23 | 24 | - name: Autobuild 25 | id: auto_build 26 | uses: github/codeql-action/autobuild@v1 27 | 28 | - name: Perform CodeQL Analysis 29 | id: analyze_codeql 30 | uses: github/codeql-action/analyze@v1 31 | 32 | # Built with ❤ by [Pipeline Foundation](https://pipeline.foundation) 33 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: dotnet core - build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-dotnet@v1 11 | with: 12 | dotnet-version: '6.0.x' 13 | - name: Build with dotnet 14 | run: dotnet build --configuration Release 15 | - name: Run Tests 16 | run: dotnet test 17 | -------------------------------------------------------------------------------- /.github/workflows/publish-analyzer.yml: -------------------------------------------------------------------------------- 1 | name: publish analyzer to nuget 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main # Your default release branch 7 | paths: 8 | - 'src/Ardalis.ApiEndpoints.CodeAnalyzers/**' 9 | jobs: 10 | publish: 11 | name: list on nuget 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: '6.0.x' 18 | - name: publish on version change 19 | uses: brandedoutcast/publish-nuget@v2 20 | with: 21 | PROJECT_FILE_PATH: src/Ardalis.ApiEndpoints.CodeAnalyzers/Ardalis.ApiEndpoints.CodeAnalyzers.csproj # Relative to repository root 22 | TAG_FORMAT: Analyzer_v* # Format of the git tag, [*] gets replaced with version 23 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} # nuget.org API key 24 | -------------------------------------------------------------------------------- /.github/workflows/publish-nswag.yml: -------------------------------------------------------------------------------- 1 | name: publish NSwag package to nuget 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main # Your default release branch 7 | paths: 8 | - 'src/ApiEndpoints.NSwag/**' 9 | jobs: 10 | publish: 11 | name: list on nuget 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: '6.0.x' 18 | - name: publish on version change 19 | uses: brandedoutcast/publish-nuget@v2 20 | with: 21 | PROJECT_FILE_PATH: src/Ardalis.ApiEndpoints.NSwag/Ardalis.ApiEndpoints.NSwag.csproj # Relative to repository root 22 | TAG_FORMAT: NSwag_v* # Format of the git tag, [*] gets replaced with version 23 | INCLUDE_SYMBOLS: true # Pushing symbols along with nuget package to the server 24 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} # nuget.org API key 25 | -------------------------------------------------------------------------------- /.github/workflows/publish-swashbuckle.yml: -------------------------------------------------------------------------------- 1 | name: publish Swashbuckle package to nuget 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main # Your default release branch 7 | paths: 8 | - 'src/Ardalis.ApiEndpoints.Swashbuckle/**' 9 | jobs: 10 | publish: 11 | name: list on nuget 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: '6.0.x' 18 | - uses: brandedoutcast/publish-nuget@v2 19 | with: 20 | PROJECT_FILE_PATH: src/Ardalis.ApiEndpoints.Swashbuckle/Ardalis.ApiEndpoints.Swashbuckle.csproj # Relative to repository root 21 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 22 | INCLUDE_SYMBOLS: true 23 | TAG_FORMAT: Swashbuckle_v* # Format of the git tag, [*] gets replaced with version 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/publish-template.yml: -------------------------------------------------------------------------------- 1 | name: publish template package to nuget 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - main # Your default release branch 8 | jobs: 9 | build-test-prep-release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - uses: actions/setup-dotnet@v1 14 | with: 15 | dotnet-version: '6.0.x' 16 | env: 17 | DOTNET_INSTALL_DIR: /usr/share/dotnet 18 | - name: build and test 19 | run: | 20 | dotnet restore 21 | dotnet build -c Release --no-restore 22 | dotnet test -c Release --no-build 23 | - name: Create the package 24 | run: dotnet pack sample/Sample.WeatherForecast/templatepack.csproj -c Release --output nupkgs 25 | - name: Publish the package to NuGet.org 26 | env: 27 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} 28 | run: dotnet nuget push nupkgs/*.nupkg -k $NUGET_KEY -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: publish ApiEndpoints to nuget 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main # Your default release branch 7 | paths: 8 | - 'src/Ardalis.ApiEndpoints/**' 9 | jobs: 10 | publish: 11 | name: list on nuget 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | - uses: actions/setup-dotnet@v1 16 | with: 17 | dotnet-version: '6.0.x' 18 | - name: Build with dotnet 19 | run: dotnet build --configuration Release 20 | - name: Run Tests 21 | run: dotnet test 22 | - name: publish on version change 23 | uses: brandedoutcast/publish-nuget@v2 24 | with: 25 | PROJECT_FILE_PATH: src/Ardalis.ApiEndpoints/Ardalis.ApiEndpoints.csproj # Relative to repository root 26 | VERSION_REGEX: (.*)<\/Version> # Regex pattern to extract version info in a capturing group 27 | TAG_COMMIT: true # Flag to enable / disable git tagging 28 | TAG_FORMAT: v* # Format of the git tag, [*] gets replaced with version 29 | NUGET_KEY: ${{secrets.NUGET_API_KEY}} # nuget.org API key 30 | -------------------------------------------------------------------------------- /.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 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | sample/SampleEndpointApp/database.sqlite 352 | .vscode/settings.json 353 | sample/SampleEndpointApp/SampleEndpointApp.xml 354 | -------------------------------------------------------------------------------- /Ardalis.ApiEndpoints.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29709.97 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{CE4A1E99-D876-4D8F-B56E-9BF453456BAB}" 7 | ProjectSection(SolutionItems) = preProject 8 | src\Directory.Build.props = src\Directory.Build.props 9 | EndProjectSection 10 | EndProject 11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{38434103-76E0-4820-B4AF-F5EA5D08A7BD}" 12 | EndProject 13 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ardalis.ApiEndpoints", "src\Ardalis.ApiEndpoints\Ardalis.ApiEndpoints.csproj", "{6C623E88-4737-417C-B835-FA1202F98866}" 14 | EndProject 15 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample.FunctionalTests", "sample\Sample.FunctionalTests\Sample.FunctionalTests.csproj", "{851683FE-B589-43DB-93B5-F38C7AAEBEE2}" 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SampleEndpointApp", "sample\SampleEndpointApp\SampleEndpointApp.csproj", "{B7EB2D30-B907-4B61-96F6-091F90590438}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{1241985D-5277-49C6-9570-6E1FA1851CBA}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ardalis.ApiEndpoints.CodeAnalyzers.Test", "tests\Ardalis.ApiEndpoints.CodeAnalyzers.Test\Ardalis.ApiEndpoints.CodeAnalyzers.Test.csproj", "{EF801BF2-AA72-4FCF-AA67-09B5E3ED4283}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ardalis.ApiEndpoints.CodeAnalyzers", "src\Ardalis.ApiEndpoints.CodeAnalyzers\Ardalis.ApiEndpoints.CodeAnalyzers.csproj", "{7D8E74B0-4620-492A-940A-FCEEF9CF92F9}" 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{256C2CE7-00B3-47FF-BC7E-1BAF20A86E08}" 26 | ProjectSection(SolutionItems) = preProject 27 | .editorconfig = .editorconfig 28 | Directory.Build.props = Directory.Build.props 29 | LICENSE.txt = LICENSE.txt 30 | EndProjectSection 31 | EndProject 32 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CodeAnalyzers", "CodeAnalyzers", "{EF881EEC-9B50-4AE5-AA21-A42B9253122A}" 33 | EndProject 34 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenApi", "OpenApi", "{7BBDA78D-3207-479B-9D80-9CADD46C31F1}" 35 | EndProject 36 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ardalis.ApiEndpoints.NSwag", "src\Ardalis.ApiEndpoints.NSwag\Ardalis.ApiEndpoints.NSwag.csproj", "{B11A92CB-3AE3-495E-99A2-F06EE62DCDF6}" 37 | EndProject 38 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Ardalis.ApiEndpoints.Swashbuckle", "src\Ardalis.ApiEndpoints.Swashbuckle\Ardalis.ApiEndpoints.Swashbuckle.csproj", "{0A625FF1-6E6E-445F-B40D-50AE6B5FFCA0}" 39 | EndProject 40 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample.WeatherForecast", "sample\Sample.WeatherForecast\Sample.WeatherForecast.csproj", "{B37C448C-7DCE-48E2-8CA7-0910CB40393C}" 41 | EndProject 42 | Global 43 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 44 | Debug|Any CPU = Debug|Any CPU 45 | Release|Any CPU = Release|Any CPU 46 | EndGlobalSection 47 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 48 | {6C623E88-4737-417C-B835-FA1202F98866}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {6C623E88-4737-417C-B835-FA1202F98866}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {6C623E88-4737-417C-B835-FA1202F98866}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {6C623E88-4737-417C-B835-FA1202F98866}.Release|Any CPU.Build.0 = Release|Any CPU 52 | {851683FE-B589-43DB-93B5-F38C7AAEBEE2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 53 | {851683FE-B589-43DB-93B5-F38C7AAEBEE2}.Debug|Any CPU.Build.0 = Debug|Any CPU 54 | {851683FE-B589-43DB-93B5-F38C7AAEBEE2}.Release|Any CPU.ActiveCfg = Release|Any CPU 55 | {851683FE-B589-43DB-93B5-F38C7AAEBEE2}.Release|Any CPU.Build.0 = Release|Any CPU 56 | {B7EB2D30-B907-4B61-96F6-091F90590438}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 57 | {B7EB2D30-B907-4B61-96F6-091F90590438}.Debug|Any CPU.Build.0 = Debug|Any CPU 58 | {B7EB2D30-B907-4B61-96F6-091F90590438}.Release|Any CPU.ActiveCfg = Release|Any CPU 59 | {B7EB2D30-B907-4B61-96F6-091F90590438}.Release|Any CPU.Build.0 = Release|Any CPU 60 | {EF801BF2-AA72-4FCF-AA67-09B5E3ED4283}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 61 | {EF801BF2-AA72-4FCF-AA67-09B5E3ED4283}.Debug|Any CPU.Build.0 = Debug|Any CPU 62 | {EF801BF2-AA72-4FCF-AA67-09B5E3ED4283}.Release|Any CPU.ActiveCfg = Release|Any CPU 63 | {EF801BF2-AA72-4FCF-AA67-09B5E3ED4283}.Release|Any CPU.Build.0 = Release|Any CPU 64 | {7D8E74B0-4620-492A-940A-FCEEF9CF92F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 65 | {7D8E74B0-4620-492A-940A-FCEEF9CF92F9}.Debug|Any CPU.Build.0 = Debug|Any CPU 66 | {7D8E74B0-4620-492A-940A-FCEEF9CF92F9}.Release|Any CPU.ActiveCfg = Release|Any CPU 67 | {7D8E74B0-4620-492A-940A-FCEEF9CF92F9}.Release|Any CPU.Build.0 = Release|Any CPU 68 | {B11A92CB-3AE3-495E-99A2-F06EE62DCDF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 69 | {B11A92CB-3AE3-495E-99A2-F06EE62DCDF6}.Debug|Any CPU.Build.0 = Debug|Any CPU 70 | {B11A92CB-3AE3-495E-99A2-F06EE62DCDF6}.Release|Any CPU.ActiveCfg = Release|Any CPU 71 | {B11A92CB-3AE3-495E-99A2-F06EE62DCDF6}.Release|Any CPU.Build.0 = Release|Any CPU 72 | {0A625FF1-6E6E-445F-B40D-50AE6B5FFCA0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 73 | {0A625FF1-6E6E-445F-B40D-50AE6B5FFCA0}.Debug|Any CPU.Build.0 = Debug|Any CPU 74 | {0A625FF1-6E6E-445F-B40D-50AE6B5FFCA0}.Release|Any CPU.ActiveCfg = Release|Any CPU 75 | {0A625FF1-6E6E-445F-B40D-50AE6B5FFCA0}.Release|Any CPU.Build.0 = Release|Any CPU 76 | {B37C448C-7DCE-48E2-8CA7-0910CB40393C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 77 | {B37C448C-7DCE-48E2-8CA7-0910CB40393C}.Debug|Any CPU.Build.0 = Debug|Any CPU 78 | {B37C448C-7DCE-48E2-8CA7-0910CB40393C}.Release|Any CPU.ActiveCfg = Release|Any CPU 79 | {B37C448C-7DCE-48E2-8CA7-0910CB40393C}.Release|Any CPU.Build.0 = Release|Any CPU 80 | EndGlobalSection 81 | GlobalSection(SolutionProperties) = preSolution 82 | HideSolutionNode = FALSE 83 | EndGlobalSection 84 | GlobalSection(NestedProjects) = preSolution 85 | {6C623E88-4737-417C-B835-FA1202F98866} = {CE4A1E99-D876-4D8F-B56E-9BF453456BAB} 86 | {851683FE-B589-43DB-93B5-F38C7AAEBEE2} = {38434103-76E0-4820-B4AF-F5EA5D08A7BD} 87 | {B7EB2D30-B907-4B61-96F6-091F90590438} = {38434103-76E0-4820-B4AF-F5EA5D08A7BD} 88 | {EF801BF2-AA72-4FCF-AA67-09B5E3ED4283} = {1241985D-5277-49C6-9570-6E1FA1851CBA} 89 | {7D8E74B0-4620-492A-940A-FCEEF9CF92F9} = {EF881EEC-9B50-4AE5-AA21-A42B9253122A} 90 | {EF881EEC-9B50-4AE5-AA21-A42B9253122A} = {CE4A1E99-D876-4D8F-B56E-9BF453456BAB} 91 | {7BBDA78D-3207-479B-9D80-9CADD46C31F1} = {CE4A1E99-D876-4D8F-B56E-9BF453456BAB} 92 | {B11A92CB-3AE3-495E-99A2-F06EE62DCDF6} = {7BBDA78D-3207-479B-9D80-9CADD46C31F1} 93 | {0A625FF1-6E6E-445F-B40D-50AE6B5FFCA0} = {7BBDA78D-3207-479B-9D80-9CADD46C31F1} 94 | {B37C448C-7DCE-48E2-8CA7-0910CB40393C} = {38434103-76E0-4820-B4AF-F5EA5D08A7BD} 95 | EndGlobalSection 96 | GlobalSection(ExtensibilityGlobals) = postSolution 97 | SolutionGuid = {932AEB26-7B36-4EFB-B4D1-41F13EDAE5D2} 98 | EndGlobalSection 99 | EndGlobal 100 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Ardalis.GuardClauses 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | 10 | ## We Develop with GitHub 11 | 12 | Obviously... 13 | 14 | ## We Use Pull Requests 15 | 16 | Mostly. But pretty much exclusively for non-maintainers. You'll need to fork the repo in order to submit a pull request. Here are the basic steps: 17 | 18 | 1. Fork the repo and create your branch from `main`. 19 | 2. If you've added code that should be tested, add tests. 20 | 3. If you've changed APIs, update the documentation. 21 | 4. Ensure the test suite passes. 22 | 5. Make sure your code lints. 23 | 6. Issue that pull request! 24 | 25 | - [Pull Request Check List](https://ardalis.com/github-pull-request-checklist/) 26 | - [Resync your fork with this upstream repo](https://ardalis.com/syncing-a-fork-of-a-github-repository-with-upstream/) 27 | 28 | ## Ask before adding a pull request 29 | 30 | You can just add a pull request out of the blue if you want, but it's much better etitquette (and more likely to be accepted) if you open a new issue or comment in an existing issue stating you'd like to make a pull request. 31 | 32 | ## Getting Started 33 | 34 | Look for [issues marked with 'help wanted'](https://github.com/ardalis/guardclauses/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) to find good places to start contributing. 35 | 36 | ## Any contributions you make will be under the MIT Software License 37 | 38 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers this project. 39 | 40 | ## Report bugs using Github's [issues](https://github.com/ardalis/guardclauses/issues) 41 | 42 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/ardalis/GuardClauses/issues/new/choose); it's that easy! 43 | 44 | ## Sponsor us 45 | 46 | If you don't have the time or expertise to contribute code, you can still support us by [sponsoring](https://github.com/sponsors/ardalis). 47 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp3.1 5 | latest 6 | enable 7 | true 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Steve Smith 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 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | apiendpoints.ardalis.com -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pmarsceill/just-the-docs 2 | 3 | # Aux links for the upper right navigation 4 | aux_links: 5 | "Ardalis.ApiEndpoints on GitHub": 6 | - "//github.com/ardalis/apiendpoints" 7 | -------------------------------------------------------------------------------- /docs/extensions/extend-define-evaluators.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: How to extend or define your own evaluators 4 | parent: Extensions 5 | nav_order: 3 6 | --- 7 | 8 | How to extend or define your own evaluators 9 | -------------------------------------------------------------------------------- /docs/extensions/extend-specification-builder.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: How to create your own specification builder 4 | parent: Extensions 5 | nav_order: 2 6 | --- 7 | 8 | 9 | # How to add extensions to the specification builder 10 | 11 | The specification builder from `Ardalis.Specification` is extensible by design. In fact, the methods you can use out of the box are implemented as extension methods themselves (check out the [source code](https://github.com/ardalis/Specification/blob/main/Specification/src/Ardalis.Specification/Builder/SpecificationBuilderExtensions.cs)). Your project might have requirements that cannot be satisfied by the existing toolset of course, or you might want to simplify repetitive code in several specification constructors. Whatever your case, enhancing the default builder is easy by creating your own extension methods. 12 | 13 | So where do you start? A good practice is to write the thing you think you need. Say you'd like to use a builder method `WithCustomerIdAndName` that takes the `Id` and `Name` of a customer as parameters. Then just write it like so: 14 | 15 | ````csharp 16 | Query.AsNoTracking() 17 | .WithCustomerIdAndName(1337, "John Doe"); 18 | ```` 19 | 20 | From here you can inspect the return type of the builder method you chained it to (`AsNoTracking`), and create an extension method on that interface (it doesn't need to be chained of course -- working on `Query` itself is also valid). This will most likely be `ISpecificationBuilder`, but in some cases it's an inherited inteface. The example below illustrates how extension methods on inherited interfaces allow the builder to offer specific methods in specific contexts. 21 | 22 | 23 | ## Example: Configure caching behaviour through specification builder extension method 24 | 25 | In order to achieve this (note the `.WithTimeToLive` method): 26 | 27 | ````csharp 28 | public class CustomerByNameWithStores : Specification 29 | { 30 | public CustomerByNameWithStores(string name) 31 | { 32 | Query.Where(x => x.Name == name) 33 | .EnableCache(nameof(CustomerByNameWithStoresSpec), name) 34 | // Can only be called after .EnableCache() 35 | .WithTimeToLive(TimeSpan.FromHours(1)) 36 | .Include(x => x.Stores); 37 | } 38 | } 39 | ```` 40 | 41 | We can create a simple extension method on the specification builder: 42 | 43 | ````csharp 44 | public static class SpecificationBuilderExtensions 45 | { 46 | public static ISpecificationBuilder WithTimeToLive(this ICacheSpecificationBuilder @this, TimeSpan ttl) 47 | where T : class 48 | { 49 | // The .SetCacheTTL method is an extension method which is discussed below 50 | @this.Specification.SetCacheTTL(ttl); 51 | return @this; 52 | } 53 | } 54 | ```` 55 | 56 | This extension method can only be called when chained after `EnableCache`. This is because `EnableCache` returns `ICacheSpecificationBuilder` which inherits from `ISpecificationBuilder`. Which is nice because it helps the IDE to give the right suggestions in the right place, and because it avoids confusing code as the `.WithTimeToLive` cannot be used without its *parent* `EnableCache` method. 57 | 58 | The next thing we need to is use the TTL information in a repository. For example: 59 | 60 | ```csharp 61 | public class Repository 62 | { 63 | private DbContext _ctx; 64 | private MemoryCache _cache; 65 | 66 | public List List(ISpecification spec) 67 | { 68 | var specificationResult = SpecificationEvaluator.Default.GetQuery(_ctx.Set().AsQueryable(), spec); 69 | 70 | if (spec.CacheEnabled) 71 | { 72 | // The .GetCacheTTL method is an extension method which is discussed below 73 | var ttl = spec.GetCacheTTL(); 74 | 75 | // Uses Microsoft's MemoryCache to cache the result 76 | _cache.GetOrCreate(spec.CacheKey, ce => 77 | { 78 | ce.AbsoluteExpiration = DateTime.Now.Add(ttl); 79 | return specificationResult.ToList(); 80 | }); 81 | } 82 | else 83 | { 84 | return specificationResult.ToList(); 85 | } 86 | } 87 | } 88 | ``` 89 | 90 | Finally, we need to take care of some plumbing to implement the `.GetCacheTTL` and `.SetCacheTTL` methods that we've used in the example repository and builder extension. 91 | 92 | ````csharp 93 | public static class SpecificationExtensions 94 | { 95 | public static void SetCacheTTL(this ISpecification spec, TimeSpan timeToLive) 96 | { 97 | spec.Items["CacheTTL"] = timeToLive; 98 | } 99 | public static TimeSpan GetCacheTTL(this ISpecification spec) 100 | { 101 | spec.Items.TryGetValue("CacheTTL", out var ttl); 102 | return (ttl as TimeSpan?) ?? TimeSpan.MaxValue; 103 | } 104 | } 105 | ```` -------------------------------------------------------------------------------- /docs/extensions/extensions-for-specifications.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: How to write extensions to specifications 4 | parent: Extensions 5 | nav_order: 1 6 | --- 7 | 8 | How to write extensions to specifications 9 | -------------------------------------------------------------------------------- /docs/extensions/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Extensions 4 | nav_order: 5 5 | has_children: true 6 | --- 7 | 8 | How to extend the package's base functionality using extensions, builders, and evaluators. 9 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardalis/ApiEndpoints/3d19ba1537e4c477ac74304a5befe0ad2a2b4980/docs/favicon.ico -------------------------------------------------------------------------------- /docs/features/asnotracking.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: AsNoTracking 4 | nav_order: 1 5 | has_children: false 6 | parent: ORM-Specific Features 7 | grand_parent: Features 8 | --- 9 | 10 | # AsNoTracking 11 | 12 | Compatible with: 13 | 14 | - [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/) 15 | - [EF6](https://www.nuget.org/packages/Ardalis.Specification.EntityFramework6/) 16 | 17 | The `AsNoTracking` feature applies this method to the resulting query executed by [EF6](https://docs.microsoft.com/en-us/dotnet/api/system.data.entity.dbextensions.asnotracking) or [EF Core](https://docs.microsoft.com/en-us/ef/core/querying/tracking#no-tracking-queries). 18 | 19 | > No tracking queries are useful when the results are used in a read-only scenario. They're quicker to execute because there's no need to set up the change tracking information. If you don't need to update the entities retrieved from the database, then a no-tracking query should be used. 20 | 21 | ## Example 22 | 23 | The following example shows how to add `AsNoTracking` to a specification: 24 | 25 | ```csharp 26 | public class CustomerByNameReadOnlySpec : Specification 27 | { 28 | public CustomerByNameReadOnlySpec(string name) 29 | { 30 | Query.Where(x => x.Name == name) 31 | .AsNoTracking() 32 | .OrderBy(x => x.Name) 33 | .ThenByDescending(x => x.Address); 34 | } 35 | } 36 | ``` 37 | 38 | **Note:** It's a good idea to note when specifications use `AsNoTracking` so that consumers of the specification will not attempt to modify and save entities returned by queries using the specification. The above specification adds `ReadOnly` to its name for this purpose. 39 | -------------------------------------------------------------------------------- /docs/features/asnotrackingwithidentityresolution.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: AsNoTrackingWithIdentityResolution 4 | nav_order: 2 5 | has_children: false 6 | parent: ORM-Specific Features 7 | grand_parent: Features 8 | --- 9 | 10 | # AsNoTrackingWithIdentityResolution 11 | 12 | Compatible with: 13 | 14 | - [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/) 15 | 16 | The `AsNoTrackingWithIdentityResolution` feature applies this method to the resulting query executed by[EF Core](https://docs.microsoft.com/en-us/ef/core/change-tracking/identity-resolution#identity-resolution-and-queries). It is not supported by EF 6. 17 | 18 | > No-tracking queries can be forced to perform identity resolution by using `AsNoTrackingWithIdentityResolution(IQueryable)`. The query will then keep track of returned instances (without tracking them in the normal way) and ensure no duplicates are created in the query results. 19 | 20 | ## Example 21 | 22 | The following example shows how to add `AsNoTrackingWithIdentityResolution` to a specification: 23 | 24 | ```csharp 25 | public class CustomerByNameReadOnlySpec : Specification 26 | { 27 | public CustomerByNameReadOnlySpec(string name) 28 | { 29 | Query.Where(x => x.Name == name) 30 | .AsNoTrackingWithIdentityResolution() 31 | .OrderBy(x => x.Name) 32 | .ThenByDescending(x => x.Address); 33 | } 34 | } 35 | ``` 36 | 37 | **Note:** It's a good idea to note when specifications use `AsNoTracking` (or `AsNoTrackingWithIdentityResolution`) so that consumers of the specification will not attempt to modify and save entities returned by queries using the specification. The above specification adds `ReadOnly` to its name for this purpose. 38 | -------------------------------------------------------------------------------- /docs/features/assplitquery.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: AsSplitQuery 4 | nav_order: 3 5 | has_children: false 6 | parent: ORM-Specific Features 7 | grand_parent: Features 8 | --- 9 | 10 | # AsSplitQuery 11 | 12 | Compatible with: 13 | 14 | - [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/) 15 | 16 | [EF Core 5 introduced support for split queries](https://docs.microsoft.com/ef/core/querying/single-split-queries#split-queries-1) which will perform separate queries rather than complex joins when returning data from multiple tables. A single query result with data from many tables may result in a "cartesian explosion" of duplicate data across many columns and rows. 17 | 18 | > EF allows you to specify that a given LINQ query should be split into multiple SQL queries. Instead of JOINs, split queries generate an additional SQL query for each included collection navigation. 19 | 20 | ## Example 21 | 22 | Below is a specification that uses `AsSplitQuery` in order to generate several separate queries rather than a large join across the Company, Store, and Product tables: 23 | 24 | ```csharp 25 | public class CompanyByIdAsSplitQuery : Specification, ISingleResultSpecification 26 | { 27 | public CompanyByIdAsSplitQuery(int id) 28 | { 29 | Query.Where(company => company.Id == id) 30 | .Include(x => x.Stores) 31 | .ThenInclude(x => x.Products) 32 | .AsSplitQuery(); 33 | } 34 | } 35 | ``` 36 | -------------------------------------------------------------------------------- /docs/features/base-features.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Base Features 4 | nav_order: 1 5 | has_children: true 6 | parent: Features 7 | --- 8 | 9 | # Base Features 10 | 11 | The features described in the docs below all work as they do in Linq. For explanations beyond those provided below, you may find the Methods section of the [Linq docs](https://docs.microsoft.com/en-us/dotnet/api/system.linq.enumerable?view=net-5.0) helpful. 12 | -------------------------------------------------------------------------------- /docs/features/caching.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Caching 4 | nav_order: 6 5 | has_children: false 6 | parent: Base Features 7 | grand_parent: Features 8 | --- 9 | 10 | # Caching 11 | 12 | To implement caching using Specification, you will need to enable caching on your specification when it is defined: 13 | 14 | ```csharp 15 | public class CustomerByNameWithStoresSpec : Specification, ISingleResultSpecification 16 | { 17 | public CustomerByNameWithStoresSpec(string name) 18 | { 19 | Query.Where(x => x.Name == name) 20 | .Include(x => x.Stores) 21 | .EnableCache(nameof(CustomerByNameWithStoresSpec), name); 22 | } 23 | } 24 | ``` 25 | 26 | The `.EnableCache` method takes in two parameters: the name of the specification and the parameters of the specification. It does not include any parameters to control how the cache should behave (e.g. absolute expiration date, expiration tokens, ...). However, one could create an extension method to the specification builder in order to add this information ([example](../extensions/extend-specification-builder.md)). 27 | 28 | Implementing caching will also require infrastructure such as a CachedRepository, an example of which is given in [the sample](https://github.com/ardalis/Specification/blob/2605202df4d8e40fe388732db6d8f7a3754fcc2b/sample/Ardalis.SampleApp.Infrastructure/Data/CachedCustomerRepository.cs#L13) on GitHub. The `EnableCache` method is used to inform the cache implementation that caching should be used, and to configure the `CacheKey` based on the arguments supplied. 29 | -------------------------------------------------------------------------------- /docs/features/evaluate.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Evaluate 4 | nav_order: 8 5 | has_children: false 6 | parent: Base Features 7 | grand_parent: Features 8 | --- 9 | 10 | # Evaluate 11 | 12 | Apply a specification to an in memory collection. 13 | 14 | ## Example 15 | 16 | First, a Specification can be defined to filter a given type. In this case, a specification that filters strings using a Contains clause. 17 | 18 | ```csharp 19 | public class StringsWhereValueContainsSpec : Specification 20 | { 21 | public StringsWhereValueContainsSpec(string filter) 22 | { 23 | Query.Where(s => s.Contains(filter)); 24 | } 25 | } 26 | ``` 27 | 28 | You can apply the Specification above to an in memory collection using the `Evaluate` method. This method takes an `IEnumerable` as a parameter representing the collection to apply the specification. A brief example is demonstrated below. 29 | 30 | ```csharp 31 | var trainingResources = new[] 32 | { 33 | "Articles", 34 | "Blogs", 35 | "Documentation", 36 | "Pluralsight", 37 | }; 38 | 39 | var specification = new StringsWhereValueContainsSpec("ti"); 40 | 41 | var results = specification.Evaluate(trainingResources); 42 | ``` 43 | 44 | The result of `Evaluate` should be a collection of strings containing "Articles" and "Documentation". For additional information on `Evaluate` refer to the [Specifications with In Memory Collections](../usage/use-specification-inmemory-collection.md) guide. 45 | -------------------------------------------------------------------------------- /docs/features/ignorequeryfilters.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: IgnoreQueryFilters 4 | nav_order: 4 5 | has_children: false 6 | parent: ORM-Specific Features 7 | grand_parent: Features 8 | --- 9 | 10 | # IgnoreQueryFilters 11 | 12 | Compatible with: 13 | 14 | - [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/) 15 | 16 | The `IgnoreQueryFilters` feature is used to indicate to EF Core (it is not supported by EF 6) that it should ignore global query filters for this query. It simply passes along this call to the underlying [EF Core feature for disabling global filters](https://docs.microsoft.com/ef/core/querying/filters#disabling-filters). 17 | 18 | ## Example 19 | 20 | The following specification implements the `IgnoreQueryFilters()` expression: 21 | 22 | ```csharp 23 | public class CompanyByIdIgnoreQueryFilters : Specification, ISingleResultSpecification 24 | { 25 | public CompanyByIdIgnoreQueryFilters(int id) 26 | { 27 | Query 28 | .Where(company => company.Id == id) 29 | .IgnoreQueryFilters(); 30 | } 31 | } 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/features/include.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Include 4 | nav_order: 5 5 | has_children: false 6 | parent: ORM-Specific Features 7 | grand_parent: Features 8 | --- 9 | 10 | # Include 11 | 12 | Compatible with: 13 | 14 | - [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/) 15 | - [EF6](https://www.nuget.org/packages/Ardalis.Specification.EntityFramework6/) 16 | 17 | The `Include` feature is used to indicate to the ORM that a related navigation property should be returned along with the base record being queried. It is used to expand the amount of related data being returned with an entity, providing [eager loading of related data](https://docs.microsoft.com/en-us/ef/core/querying/related-data/eager). 18 | 19 | **Note**: [*Lazy-loading* is not recommended in web-based .NET applications](https://ardalis.com/avoid-lazy-loading-entities-in-asp-net-applications/). 20 | 21 | ## Example 22 | 23 | Below is a specification that loads a Company entity along with its collection of Stores. 24 | 25 | ```csharp 26 | public class CompanyByIdWithStores : Specification, ISingleResultSpecification 27 | { 28 | public CompanyByIdWithStores(int id) 29 | { 30 | Query.Where(company => company.Id == id) 31 | .Include(x => x.Stores) 32 | } 33 | } 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/features/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Features 4 | nav_order: 4 5 | has_children: true 6 | --- 7 | 8 | # Ardalis.Specification Features 9 | 10 | Detailed review of supported features. 11 | -------------------------------------------------------------------------------- /docs/features/orderby.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: OrderBy 4 | nav_order: 2 5 | has_children: false 6 | parent: Base Features 7 | grand_parent: Features 8 | --- 9 | 10 | # OrderBy 11 | 12 | The `OrderBy` feature defined in the Specification behaves the same as `OrderBy` in Linq, and it accepts `Expression>` expression as a parameter. The same is true for the related features `OrderByDescending`, `ThenBy`, and `ThenByDescending` described below. 13 | 14 | `OrderBy`, as one might expect, is used to order the results of a query based on a key, defined by the lambda expression passed into `OrderBy`. 15 | 16 | For example: 17 | 18 | ```csharp 19 | Query.OrderBy(x => x.Name); 20 | ``` 21 | 22 | On the other hand, in order to order the results in the opposite order, one could instead use `OrderByDescending`, which works in the same manner as `OrderBy`: 23 | 24 | ```csharp 25 | Query.OrderByDescending(x => x.Name); 26 | ``` 27 | 28 | Finally, `ThenBy` and `ThenByDescending` are also supported in the Specification and can be used to further refine the order of the results: 29 | 30 | ```csharp 31 | Query.OrderByDescending(x => x.Name) 32 | .ThenByDescending(x => x.Id) 33 | .ThenBy(x => x.DateCreated); 34 | ``` 35 | -------------------------------------------------------------------------------- /docs/features/orm-features.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: ORM-Specific Features 4 | nav_order: 2 5 | has_children: true 6 | parent: Features 7 | --- 8 | 9 | # ORM-Specific Features 10 | -------------------------------------------------------------------------------- /docs/features/paging.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Paging 4 | nav_order: 5 5 | has_children: false 6 | parent: Base Features 7 | grand_parent: Features 8 | --- 9 | 10 | # Paging 11 | 12 | You can use [Skip](skip.md) and [Take](take.md) to implement paging with Specification. 13 | 14 | ## Example 15 | 16 | A simple Specification with paging might look something like this: 17 | 18 | ```csharp 19 | public class StoresByCompanyPaginatedSpec : Specification 20 | { 21 | public StoresByCompanyPaginatedSpec(int companyId, int skip, int take) 22 | { 23 | Query.Where(x => x.CompanyId == companyId) 24 | .Skip(skip) 25 | .Take(take); 26 | } 27 | } 28 | ``` 29 | 30 | *Find the most recent version of this Specification [here](https://github.com/ardalis/Specification/blob/master/ArdalisSpecification/tests/Ardalis.Specification.UnitTests/Fixture/Specs/StoresByCompanyPaginatedSpec.cs).* 31 | 32 | ## How paging should work 33 | 34 | To implement paging, you should `Skip` `i * n` entries, where `i` is the index of the page you're on (starting from zero), and `n` is the number of entries per page. Then you should `Take` `n` entries. When paging through a set of data, each request must include the appropriate `Skip` and `Take` values for the page being requested. 35 | -------------------------------------------------------------------------------- /docs/features/postprocessingaction.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: PostProcessingAction 4 | nav_order: 9 5 | has_children: false 6 | parent: Base Features 7 | grand_parent: Features 8 | --- 9 | 10 | # PostProcessingAction 11 | 12 | See [this issue for details](https://github.com/ardalis/Specification/pull/56). 13 | 14 | ## Example 15 | 16 | TODO 17 | -------------------------------------------------------------------------------- /docs/features/search.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Search 4 | nav_order: 7 5 | has_children: false 6 | parent: ORM-Specific Features 7 | grand_parent: Features 8 | --- 9 | 10 | # Search 11 | 12 | Compatible with: 13 | 14 | - [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/) 15 | - [EF6](https://www.nuget.org/packages/Ardalis.Specification.EntityFramework6/) 16 | 17 | The `Search` extension filters the query source by applying an 'SQL LIKE' operation to it. The parameters for `Search` include the *Selector*, which is the property/column the LIKE should be applied against, and the *SearchTerm*, the value to use with the LIKE. Any wildcards (`%`) must be included in the SearchTerm. 18 | 19 | ## Example 20 | 21 | The following example demonstrates how to use the `Search` feature: 22 | 23 | ```csharp 24 | public class CustomerSpec : Specification 25 | { 26 | public CustomerSpec(CustomerFilter filter) 27 | { 28 | // other criteria omitted 29 | 30 | if (!string.IsNullOrEmpty(filter.Address)) 31 | { 32 | Query 33 | .Search(x => x.Address, "%" + filter.Address + "%"); 34 | } 35 | } 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/features/select.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Select 4 | nav_order: 7 5 | has_children: false 6 | parent: Base Features 7 | grand_parent: Features 8 | --- 9 | 10 | # Select 11 | 12 | The `Select` feature defined in Specification behaves the same as `Select` in Linq, and it takes in `IEnumerable` and `Func` as its parameters. 13 | 14 | `Select` is used to transform elements in a sequence into a new form. In Specification, `Select` is most commonly used to select a single property of each object in a list being queried. For example, the below expression could be used to retrieve only the name of each object: 15 | 16 | ```csharp 17 | Query.Select(x => x.Name); 18 | ``` 19 | 20 | Since this query is now returning a different type, the type of `Name`, rather than of `x`, the base class of the Specification will need to reflect this. Instead of being a `Specification`, the Specification will need to be a `Specification`: 21 | 22 | ```csharp 23 | public class StoreNamesSpec : Specification 24 | { 25 | public StoreNamesSpec() 26 | { 27 | Query.Select(x => x.Name); 28 | } 29 | } 30 | ``` 31 | -------------------------------------------------------------------------------- /docs/features/skip.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Skip 4 | nav_order: 3 5 | has_children: false 6 | parent: Base Features 7 | grand_parent: Features 8 | --- 9 | 10 | # Skip 11 | 12 | The `Skip` feature defined in the Specification behaves the same as `Skip` in Linq, and it accepts an `int count` as a parameter. 13 | 14 | `Skip` is used to skip a certain number of the results in a query, starting from the beginning. For example: 15 | 16 | ```csharp 17 | int[] numbers = { 1, 3, 2, 5, 7, 4 }; 18 | 19 | IEnumerable subsetOfNumbers = numbers.Skip(2); 20 | ``` 21 | 22 | Here, `subsetOfNumbers` would contain `{ 2, 5, 7, 4 }`. 23 | 24 | Alternatively: 25 | 26 | ```csharp 27 | int[] numbers = { 1, 3, 2, 5, 7, 4 }; 28 | 29 | IEnumerable subsetOfNumbers = numbers.OrderBy(n => n).Skip(2); 30 | ``` 31 | 32 | Here, `subsetOfNumbers` would contain `{ 3, 4, 5, 7 }`. 33 | 34 | `Skip` is commonly used in combination with [Take](take.md) to implement [Paging](paging.md), but as the above demonstrates, `Skip` can also be used on its own. 35 | -------------------------------------------------------------------------------- /docs/features/take.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Take 4 | nav_order: 4 5 | has_children: false 6 | parent: Base Features 7 | grand_parent: Features 8 | --- 9 | 10 | # Take 11 | 12 | The `Take` feature defined in the Specification behaves the same as `Take` in Linq, and it accepts an `int count` as a parameter. 13 | 14 | `Take` is used to select a certain number of the results in a query, starting from the beginning. For example: 15 | 16 | ```csharp 17 | int[] numbers = { 1, 3, 2, 5, 7, 4 }; 18 | 19 | IEnumerable subsetOfNumbers = numbers.Take(3); 20 | ``` 21 | 22 | Here, `subsetOfNumbers` would contain `{ 1, 3, 2 }`. 23 | 24 | Alternatively: 25 | 26 | ```csharp 27 | int[] numbers = { 1, 3, 2, 5, 7, 4 }; 28 | 29 | IEnumerable subsetOfNumbers = numbers.OrderBy(n => n).Take(3); 30 | ``` 31 | 32 | Here, `subsetOfNumbers` would contain `{ 1, 2, 3 }`. 33 | 34 | `Take` is commonly used in combination with [Skip](skip.md) to implement [Paging](paging.md), but as the above demonstrates, `Take` can also be used on its own. 35 | -------------------------------------------------------------------------------- /docs/features/then-include.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: ThenInclude 4 | nav_order: 6 5 | has_children: false 6 | parent: ORM-Specific Features 7 | grand_parent: Features 8 | --- 9 | 10 | # ThenInclude 11 | 12 | Compatible with: 13 | 14 | - [EF Core](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/) 15 | - [EF6](https://www.nuget.org/packages/Ardalis.Specification.EntityFramework6/) 16 | 17 | The `ThenInclude` feature is used to indicate to the ORM that a related property of a previously `Include`d property should be returned with a query result. 18 | 19 | ## Example 20 | 21 | Below is a specification that loads a Company entity along with its collection of Stores, *then* each Store's collection of Products. 22 | 23 | ```csharp 24 | public class CompanyByIdWithStoresAndProducts : Specification, ISingleResultSpecification 25 | { 26 | public CompanyByIdWithStoresAndProducts(int id) 27 | { 28 | Query.Where(company => company.Id == id) 29 | .Include(x => x.Stores) 30 | .ThenInclude(x => x.Products) 31 | } 32 | } 33 | ``` 34 | -------------------------------------------------------------------------------- /docs/features/where.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Where 4 | nav_order: 1 5 | has_children: false 6 | parent: Base Features 7 | grand_parent: Features 8 | --- 9 | 10 | # Where 11 | 12 | The `Where` feature defined in the Specification behaves the same as `Where` in Linq, and it accepts `Expression>` expression as a parameter. 13 | 14 | `Where` is used to select objects meeting a certain criteria, as defined by a lambda expression. For example: 15 | 16 | ```csharp 17 | Query.Where(x => x.Id == Id); 18 | ``` 19 | 20 | This query will select an object `x` if `x.Id` is equal to `Id`. Note that while this particular query likely selects a single object (since Ids should generally be unique), the `Where` operator will select *all* objects matching the specified criteria. 21 | -------------------------------------------------------------------------------- /docs/getting-started/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Frequently Asked Questions (FAQ) 4 | parent: Getting Started 5 | nav_order: 4 6 | --- 7 | 8 | ## Does the use of filters break the Open-Closed Principle? 9 | 10 | Filters are an optional approach to use with specifications. You'll find samples of them [here](https://github.com/ardalis/Specification/tree/master/sample/Ardalis.SampleApp.Core/Specifications/Filters). 11 | 12 | This is a totally valid question. If the intention by using the specifications is to conform to this principle, then by adding the concept of filters, aren't we doing the opposite? We go back and update the specification by adding additional conditions. 13 | As a brief recap, the OCP predicates that we should have constructs that are open to extension and closed to changes. This means, if we need to add a "behavior" to a class, we should be able to do that without changing the class itself. Even more simplified, if you have switch statements and too much conditional logic, it might be a sign that the behavior is too hardcoded, and might be refactored in a better way. 14 | 15 | In our case, indeed we have too many conditions within the specification, so this concern is partially true. The catch here is that we have to do with single and atomic business requirement. The user has demanded from us that we change the behavior and add an additional condition by which the customers can be queried. The requirement is that the filters on the customer's UI page be extended. Whenever you have business requirement changes, undoubtedly you will have code changes in the domain model as well. That particular specification will change only when this exact business requirement changes, and never otherwise; it's wired up to this functionality only. Even though not the perfect structure, I might say it's an acceptable solution. 16 | 17 | This is quite different from the case when you have to change/add/delete behavior in a "classic" repository, in which case in order to update one business requirement, you're forced to update a construct that holds a collection of business requirements. That clearly violates SRP and OCP. 18 | 19 | ## Why using `ThenInclude` in one instance broke the application? What is the proper usage? 20 | 21 | In one instance, while describing different usages we updated the specification by adding `ThenInclude` (as shown below), which in turn resulted in a runtime error. 22 | 23 | ```csharp 24 | Query.Include(x => x.Stores).ThenInclude(x => x.Address); 25 | ``` 26 | 27 | The error here has to do with the fact that the `Address` property is not a navigation property, but a string property. Obviously, you should not include simple properties. And, this is the same behavior that you will have by using EF directly. Including simple properties won't result in a compile-time error but a runtime error. It's up to you to be careful, make proper usage of it, and thoroughly test your queries. 28 | 29 | We can constrain this usage and throw an exception, but we don't want to alter the behavior that much. What if the EF in some further version makes use of this usage? So, the ultimate usage constraints should be the responsibility of the ORM you're using. 30 | 31 | ## How many Include statements are OK to have? 32 | 33 | While creating JOINs in SQL, the real issue is not about how many tables, but the cardinality. If the dependent tables are configured as one-to-one relationships, that's quite OK. But if you're including dependent tables, where there are many rows for each principal row, then it can have quite an impact on the performance. On top of that, you should be careful what SQL queries are being produced by EF as well. The EF Core 1&2 uses split queries, while EF Core 3 uses a single query. If you have a lot of 1:n relationships and use a single query, then you might end with a "cartesian explosion" (consider split queries in these cases). 34 | 35 | During the [stream](https://www.youtube.com/watch?v=BgWWbBUWyig&t=315s&ab_channel=Ardalis), I showcased the following specification, and the question was if this is OK? 36 | 37 | ```csharp 38 | public class AwbForInvoiceSpec : Specification 39 | { 40 | public AwbForInvoiceSpec(int ID) 41 | { 42 | Query.Where(x => x.ID == ID); 43 | Query.Include(x => x.Packages); 44 | Query.Include(x => x.AwbCargoServices); 45 | Query.Include(x => x.AwbPurchase); 46 | Query.Include(x => x.CargoManifest); 47 | Query.Include(x => x.Customer).ThenInclude(x => x.Addresses); 48 | } 49 | } 50 | ``` 51 | 52 | In the context of that particular application, the `Awb` has quite significant importance in the overall business workflow, and it might be a bit more complicated than it should be. First of all, the `AwbPurchase` and `CargoManifest` represent 1:1 relationships. So, we end up with two 1:n navigations. This is relatively OK if you're retrieving one Awb record (as in this case). On the other hand, if you're trying to get a list of records, then you should reconsider if you need the child collections or not. Try to measure the performance, consider the usage of the application, number of users, peak usages, etc, and then you can decide if that meets your criteria or not. 53 | 54 | One key benefit of using the specification pattern is that you can easily have different specifiations that include just the related data necessary for a given operation or context. 55 | 56 | ## Anti-pattern usage 57 | 58 | In the above example, the actual anti-pattern is not the usage of several Include statements, but including the `Customer`. That implies that Awb aggregate has direct navigation to the `Customer` aggregate. If you follow DDD, you should strive to have as independent aggregates as possible. If required, one particular aggregate should hold only the identifier of some other aggregate root and not have a direct reference. The app can then load the other aggregate from its id as necessary. 59 | 60 | In our case, it was a deliberate design decision to break this rule (for `Customer` aggregate), in order to improve the performance in particular cases and to reduce roundtrips to DB. But, it's not something I would advise you to do. Anyhow, it's up to you to weigh the pros/cons and make your own elaborate decisions for your applications. 61 | 62 | ## Do I need private constructors for the entities (e.g. for the EF code-first approach) 63 | 64 | I got this question related to one particular scenario which happened during the stream. Once we added the `DateTime birthdate` parameter in the constructor, we were forced to add an additional parameterless constructor so the EF could work properly. 65 | 66 | EF requires a parameterless constructor, so it can create the instance of the entity and then populate the properties. So, it might be wise always to add one private parameterless constructor just to be sure EF can instantiate the entity. 67 | 68 | EF is smart enough to utilize the constructor and will try to pass the values as ctor arguments. That's how EF handles the immutability (if your props have only getters). But, the ctor arguments should be named the same as the properties. The first character can be lowercase, and that's ok, EF will map to it correctly. But, the case of the rest of the characters should be exactly the same. So, in our case, if we have named the argument `birthDate` instead of `birthdate`, would have worked with no issues. 69 | 70 | ```csharp 71 | public Customer(string name, string email, string address, DateTime birthdate) 72 | { 73 | Guard.Against.NullOrEmpty(name, nameof(name)); 74 | Guard.Against.NullOrEmpty(email, nameof(email)); 75 | 76 | this.BirthDate = birthdate; 77 | this.Name = name; 78 | this.Email = email; 79 | this.Address = address; 80 | } 81 | ``` 82 | 83 | ## How can I use the `Select` operator in Specification? 84 | 85 | Use the following syntax to transform elements in a sequence based on a lambda expression: 86 | 87 | ```csharp 88 | Query.Select(x => x.Name); 89 | ``` 90 | 91 | In this case, each element x is being "transformed" into its `Name` property. 92 | 93 | Also make sure that the base class of your Specification using `Select` is a `Specification`. 94 | 95 | See the [doc page](../features/select.md) on `Select` for a more detailed explanation. 96 | -------------------------------------------------------------------------------- /docs/getting-started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Getting Started 4 | nav_order: 2 5 | has_children: true 6 | --- 7 | 8 | # Getting Started 9 | 10 | This section includes a quick start guide to help you get started using the package and pattern quickly, as well as some additional information you may find useful as you first use the package and its classes. 11 | -------------------------------------------------------------------------------- /docs/getting-started/patterns-used.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Patterns Used 4 | parent: Getting Started 5 | nav_order: 3 6 | --- 7 | 8 | # Design Patterns Used 9 | 10 | ## Specification Pattern 11 | 12 | In the [Specification Pattern](https://deviq.com/design-patterns/specification-pattern), specifications are used to define a query. Using a specification eliminates the need for scattering LINQ logic throughout the codebase, as the LINQ expressions can instead be encapsulated in the specification object. Additionally, using a specification to define the exact data required in a given query increases performance by ensuring only one query needs to be made at a time (as opposed to lazily loading each piece of data as it is required). As used in the [Ardalis.Specification package](https://www.nuget.org/packages/Ardalis.Specification), this pattern is used in conjunction with the [Repository Pattern](https://deviq.com/design-patterns/repository-pattern). 13 | 14 | When used to define an object's state, the Specification Pattern can be used with the [Rules Engine Pattern](https://www.pluralsight.com/courses/c-sharp-design-patterns-rules-pattern) or the Factory Pattern. They can also be used to perform validation by specifying criteria that represent a valid instance of an object. 15 | 16 | ## Repository 17 | 18 | By adding specifications to a repository implementation, a number of [SOLID principles](https://deviq.com/principles/solid) are better applied to the resulting solution. These typically only apply to repositories in larger and more complex applications, but if you're seeing your number or size of repositories grow in your applicaion, that's a code smell that indicates you may benefit from using specifications. 19 | 20 | A common issue with repository is that custom queries require additional methods on repository interfaces and implementations. As new requirements arrive, more and more methods are appended to once-simple interfaces. 21 | 22 | Opening and modifying the same types again and again is obviously breaking the [Open/Closed Principle](https://deviq.com/principles/open-closed-principle). 23 | 24 | Adding more and more responsibilities (persistence, but also querying) to your repository types violates the [Single Responsibility Principle](https://deviq.com/principles/single-responsibility-principle). 25 | 26 | Creating larger and larger repository interfaces violates the [Interface Segregation Principle](https://deviq.com/principles/interface-segregation). 27 | 28 | Using a smaller, simpler repository interface that works with specifications solves all of these problems. 29 | -------------------------------------------------------------------------------- /docs/getting-started/quick-start-guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Quick Start Guide 4 | parent: Getting Started 5 | nav_order: 2 6 | --- 7 | 8 | # Ardalis.Specification Quick Start Guide 9 | 10 | 1. Install Nuget-Package(s) 11 | 12 | a. Always required: [Ardalis.Specification](https://www.nuget.org/packages/Ardalis.Specification/) 13 | 14 | b. If you want to use it with EF Core also install the package [Ardalis.Specification.EntityFrameworkCore](https://www.nuget.org/packages/Ardalis.Specification.EntityFrameworkCore/) 15 | 16 | c. Alternatively, if you want to use it with EF6 also install the package [Ardalis.Specification.EntityFramework6](https://www.nuget.org/packages/Ardalis.Specification.EntityFramework6/) 17 | 18 | 2. Derive a Repository from `RepositoryBase` in your Infrastructure project or layer where `YourDbContext` is defined. 19 | 20 | ```csharp 21 | public class YourRepository : RepositoryBase where T : class 22 | { 23 | private readonly YourDbContext _dbContext; 24 | 25 | public YourRepository(YourDbContext dbContext) : base(dbContext) 26 | { 27 | _dbContext = dbContext; 28 | } 29 | } 30 | ``` 31 | 32 | 3. Create a first specification. It is good practice to define Specifications in the same layer as your domain entities. 33 | 34 | ```csharp 35 | public class CustomerByLastnameSpec : Specification 36 | { 37 | public CustomerByLastnameSpec(string lastname) 38 | { 39 | Query.Where(c => c.Lastname == lastname); 40 | } 41 | } 42 | ``` 43 | 44 | 4. Register your Repository as a service to the dependency injection provider of your choice. 45 | 46 | ```csharp 47 | services.AddScoped(typeof(YourRepository<>)); 48 | ``` 49 | 50 | 5. Bind it all together: 51 | 52 | ```csharp 53 | public class CustomerService { 54 | private readonly YourRepository customerRepository; 55 | 56 | public CustomerService (YourRepository customerRepository) { 57 | this.customerRepository = customerRepository; 58 | } 59 | 60 | public Task> GetCustomersByLastname(string lastname) { 61 | return customerRepository.ListAsync(new CustomerByLastnameSpec(lastname)); 62 | } 63 | } 64 | ``` 65 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Overview 4 | nav_order: 1 5 | has_children: false 6 | --- 7 | # Overview 8 | 9 | To do: Change this to cover API Endpoints. 10 | 11 | ASP.NET Core API Endpoints are essentially Razor Pages for APIs. They break apart bloated controllers and group the API models used by individual endpoints with the endpoint logic itself. They provide a simple way to have a single file for the logic and linked files for the model types. 12 | 13 | When working with ASP.NET Core API Endpoints your project won't need any Controller classes. You can organize the Endpoints however you want. By feature. In a giant Endpoints folder. It doesn't matter - they'll work regardless of where you put them. 14 | 15 | Most REST APIs have groups of endpoints for a given resource. In Controller-based projects you would have a controller per resource. When using API Endpoints you can simply create a folder per resource, just as you would use folders to group related pages in Razor Pages. 16 | 17 | Instead of Model-View-Controller (MVC) the pattern becomes Request-EndPoint-Response(REPR). The REPR (reaper) pattern is much simpler and groups everything that has to do with a particular API endpoint together. It follows SOLID principles, in particular [SRP](https://en.wikipedia.org/wiki/Single-responsibility_principle) and [OCP](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle). It also has all the benefits of feature folders and better follows the Common Closure Principle by grouping together things that change together. 18 | 19 | ## Installing Ardalis.ApiEndpoints 20 | 21 | Install Ardalis.Specification from NuGet. The latest version is available here: 22 | 23 | [https://www.nuget.org/packages/Ardalis.ApiEndpoints/](https://www.nuget.org/packages/Ardalis.ApiEndpoints/) 24 | 25 | Alternately, add it to a project using this CLI command: 26 | 27 | ```powershell 28 | dotnet add package Ardalis.ApiEndpoints 29 | ``` 30 | 31 | ## Docs theme notes 32 | 33 | This docs site is using the [Just the Docs theme](https://pmarsceill.github.io/just-the-docs/docs/navigation-structure/). Details on how to configure its metadata and navigation can be found [here](https://pmarsceill.github.io/just-the-docs/docs/navigation-structure/). 34 | -------------------------------------------------------------------------------- /docs/projects-using-specification/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Projects Using Specification 4 | nav_order: 7 5 | has_children: true 6 | --- 7 | 8 | # Projects using Ardalis.Specification 9 | 10 | ## Contents (to do) 11 | 12 | - [eShopOnWeb Reference App](https://github.com/dotnet-architecture/eShopOnWeb) 13 | - [Pluralsight DDD Fundamentals Course sample](https://github.com/ardalis/pluralsight-ddd-fundamentals) 14 | - [CleanArchitecture Solution Template](https://github.com/ardalis/CleanArchitecture) 15 | - [fullstackhero Web API Boilerplate](https://github.com/fullstackhero/dotnet-webapi-boilerplate) 16 | - (add your own project here via [pull request](https://github.com/ardalis/Specification/pulls)) 17 | -------------------------------------------------------------------------------- /docs/related-resources/articles.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Articles 4 | parent: Related Resources 5 | nav_order: 3 6 | --- 7 | 8 | # Specification Articles 9 | 10 | Below are some articles related to this project and/or the specification design pattern. 11 | 12 | - [Specification Pattern](https://deviq.com/design-patterns/specification-pattern) 13 | - [Martin Fowler PDF](https://www.martinfowler.com/apsupp/spec.pdf) 14 | -------------------------------------------------------------------------------- /docs/related-resources/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Related Resources 4 | nav_order: 6 5 | has_children: true 6 | --- 7 | 8 | # Related Resources 9 | 10 | Additional resources to learn more about the Specification pattern and related topics like Repository and Domain-Driven Design. 11 | -------------------------------------------------------------------------------- /docs/related-resources/podcasts.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Podcasts 4 | parent: Related Resources 5 | nav_order: 4 6 | --- 7 | 8 | # Specification Podcasts 9 | 10 | Below are some podcast episodes related to this project and/or the specification design pattern. 11 | 12 | - [Layering Patterns on Repositories](https://www.weeklydevtips.com/episodes/026) 13 | -------------------------------------------------------------------------------- /docs/related-resources/training.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Training 4 | parent: Related Resources 5 | nav_order: 1 6 | --- 7 | 8 | # Specification (and DDD) Training 9 | 10 | Below are some resources related to training with the Specification design pattern and Domain-Driven Design. 11 | 12 | - Pluralsight: [DDD Fundamentals](https://www.pluralsight.com/courses/fundamentals-domain-driven-design) (2021) 13 | - Pluralsight: [DDD Fundamentals](https://www.pluralsight.com/courses/domain-driven-design-fundamentals) (2014) 14 | - Pluralsight: [Design Patterns Library](https://www.pluralsight.com/courses/patterns-library) (scroll to bottom) 15 | - [NimblePros Private Workshop](https://nimblepros.com/what-we-do) (North America) 16 | - [Pozitron Consulting](https://pozitrongroup.com/) (Europe) 17 | -------------------------------------------------------------------------------- /docs/related-resources/videos.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Videos 4 | parent: Related Resources 5 | nav_order: 2 6 | --- 7 | 8 | # Specification Videos 9 | 10 | Below are some videos related to this project. 11 | 12 | - 5 March 2021: [What's new in v5 of Ardalis.Specification](https://www.youtube.com/watch?v=gT72mWdD4Qo&ab_channel=Ardalis) 13 | - 6 November 2020: [Overiew - Working with Ardalis.Specification](https://www.youtube.com/watch?v=BgWWbBUWyig&t=1545s&ab_channel=Ardalis) 14 | - [Additional streams and videos](https://www.youtube.com/c/Ardalis/search?query=specification) 15 | -------------------------------------------------------------------------------- /docs/usage/create-specifications.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: How to Create Specifications 4 | parent: Usage 5 | nav_order: 1 6 | --- 7 | 8 | # How to Create Specifications 9 | 10 | ## Basic Specification 11 | 12 | A Specification class should inherit from `Specification`, where `T` is the type being retrieved in the query: 13 | 14 | ```csharp 15 | public class ItemByIdSpec : Specification 16 | ``` 17 | 18 | A Specification can take parameters in its constructor and use these parameters to make the appropriate query. Since the above class's name indicates that it will retrieve an `Item` *by id*, its constructor should take in an `id` parameter: 19 | 20 | ```csharp 21 | public ItemByIdSpec(int Id) 22 | ``` 23 | 24 | In its constructor, the Specification should define a `Query` expression, using its parameter to retrieve the desired object: 25 | 26 | ```csharp 27 | Query.Where(x => x.Id == Id); 28 | ``` 29 | 30 | Based on the above, the most basic specification should look something like this: 31 | 32 | ```csharp 33 | public class ItemByIdSpec : Specification 34 | { 35 | public ItemByIdSpec(int Id) 36 | { 37 | Query.Where(x => x.Id == Id); 38 | } 39 | } 40 | ``` 41 | 42 | Finally: the Specification above should also implement the marker interface `ISingleResultSpecification`, which makes clear that this Specification will return only one result. Any "ById" Specification, and any other Specification intended to return only one result, should implement this interface to make clear that it returns a single result. 43 | 44 | ```csharp 45 | public class ItemByIdSpec : Specification, ISingleResultSpecification 46 | { 47 | public ItemByIdSpec(int Id) 48 | { 49 | Query.Where(x => x.Id == Id); 50 | } 51 | } 52 | ``` 53 | 54 | ## Advanced Specification 55 | 56 | From here, additional operators can be used to further refine the Specification. These operators follow LINQ syntax and are described in more detail in the [Features](../features/index.md) section. 57 | -------------------------------------------------------------------------------- /docs/usage/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Usage 4 | nav_order: 3 5 | has_children: true 6 | --- 7 | 8 | # Usage 9 | 10 | This section includes specific ways to use the patterns supported by the Ardalis.Specification package. 11 | -------------------------------------------------------------------------------- /docs/usage/use-built-in-abstract-repository.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: How to use the Built In Abstract Repository 4 | parent: Usage 5 | nav_order: 5 6 | --- 7 | 8 | # How to use the Built In Abstract Repository 9 | 10 | ## Introduction 11 | 12 | Specifications shine when combined with the [Repository Pattern](https://deviq.com/design-patterns/repository-pattern). Get started using the one included in this package by following these steps. This example builds off the steps described in the [Quick Start Guide](../getting-started/quick-start-guide.md). 13 | 14 | To use the abstract generic repository provided in this library, first define a repository class that inherits from `RepositoryBase` in the Infrastructure or data access layer of your project. An example of this is provided in the sample web application in the [Specification repo](https://github.com/ardalis/Specification/blob/main/sample/Ardalis.SampleApp.Infrastructure/Data/MyRepository.cs). By inheriting from this base class, the generic repository class can now be used with any Entity supported by the provided DbContext. It also inherits many useful methods typically defined on a Repository without having to define them for each Entity type. This allows access to typical CRUD actions like `Add`, `Get`, `Update`, `Delete`, and `List` with minimal configuration and less duplicate code to maintain. 15 | 16 | ```csharp 17 | public class YourRepository : RepositoryBase where T : class 18 | { 19 | private readonly YourDbContext dbContext; 20 | 21 | public YourRepository(YourDbContext dbContext) : base(dbContext) 22 | { 23 | this.dbContext = dbContext; 24 | } 25 | 26 | // Not required to implement anything. Add additional functionalities if required. 27 | } 28 | ``` 29 | 30 | It is important to remember to register this generic repository as a service with the application's Dependency Injection provider. 31 | 32 | ```csharp 33 | services.AddScoped(typeof(YourRepository<>)); 34 | ``` 35 | 36 | In the example below, two different services inject the same `YourRepository` class but with different type arguments to perform similar actions. This allows for the creation of different services that can apply Specifications to collections of Entities without having to develop and maintain Repositories for each type. 37 | 38 | ```csharp 39 | public class HeroByNameAndSuperPowerContainsFilterSpec : Specification 40 | { 41 | public HeroByNameAndSuperPowerContainsFilterSpec(string name, string superPower) 42 | { 43 | if (!string.IsNullOrEmpty(name)) 44 | { 45 | Query.Where(h => h.Name.Contains(name)); 46 | } 47 | 48 | if (!string.IsNullOrEmpty(superPower)) 49 | { 50 | Query.Where(h => h.SuperPower.Contains(superPower)); 51 | } 52 | } 53 | } 54 | 55 | public class HeroService 56 | { 57 | private readonly YourRepository _heroRepository; 58 | 59 | public HeroService(YourRepository heroRepository) 60 | { 61 | _heroRepository = heroRepository; 62 | } 63 | 64 | public async Task> GetHeroesFilteredByNameAndSuperPower(string name, string superPower) 65 | { 66 | var spec = new HeroByNameAndSuperPowerContainsFilterSpec(name, superPower); 67 | 68 | return await _heroRepository.ListAsync(spec); 69 | } 70 | } 71 | 72 | public class CustomerByNameContainsFilterSpec : Specification 73 | { 74 | public CustomerByNameContainsFilterSpec(string name) 75 | { 76 | if (!string.IsNullOrEmpty(name)) 77 | { 78 | Query.Where(c => c.Name.Contains(name)); 79 | } 80 | } 81 | } 82 | 83 | public class CustomerService 84 | { 85 | private readonly YourRepository _customerRepository; 86 | 87 | public CustomerService(YourRepository customerRepository) 88 | { 89 | _customerRepository = customerRepository; 90 | } 91 | 92 | public async Task> GetCustomersFilteredByName(string name) 93 | { 94 | var spec = new CustomerByNameContainsFilterSpec(name); 95 | 96 | return await _customerRepository.ListAsync(spec); 97 | } 98 | } 99 | ``` 100 | 101 | ## Features of `RepositoryBase` 102 | 103 | The section above introduced using `RepositoryBase` to provide similar functionality across two entities and their services. This section aims to go into more detail about the methods made available by `RepositoryBase` and provide some examples of their usages. Continuing with the HeroService example that contains a `private readonly YourRepository _heroRepository` field, it is possible to create heroes as follows using the `AddAsync` method. The `SaveChangesAsync` method exposes the underlying DbContext method of the same name to persist changes to the database. 104 | 105 | ```csharp 106 | public async Task Create(string name, string superPower, bool isAlive, bool isAvenger) 107 | { 108 | var hero = new Hero(name, superPower, isAlive, isAvenger); 109 | 110 | await _heroRepository.AddAsync(hero); 111 | 112 | await _heroRepository.SaveChangesAsync(); 113 | 114 | return hero; 115 | } 116 | ``` 117 | 118 | Now that a Hero has been created, it's possible to retrieve that Hero using either the Hero's Id or by using a Specification. Note that since the `HeroByNameSpec` returns a single Hero entity, the Specification inherits the interface `ISingleResultSpecification` which `GetBySpecAsync` uses to constrain the return type to a single Entity. In this case if more than one Hero was to exist for a given name, `GetBySpecAsync` performs a `FirstOrDefaultAsync` to return the first Hero of the result set. 119 | 120 | ```csharp 121 | public class HeroByNameSpec : Specification, ISingleResultSpecification 122 | { 123 | public HeroByNameSpec(string name) 124 | { 125 | if (!string.IsNullOrEmpty(name)) 126 | { 127 | Query.Where(h => h.Name == name); 128 | } 129 | } 130 | } 131 | 132 | public async Task GetById(int id) 133 | { 134 | return await _heroRepository.GetByIdAsync(id); 135 | } 136 | 137 | public async Task GetByName(string name) 138 | { 139 | var spec = new HeroByNameSpec(name); 140 | 141 | return await _heroRepository.GetBySpecAsync(spec); 142 | } 143 | ``` 144 | 145 | Next, a Hero can be updated using `UpdateAsync`. `HeroService` defines a method `SetIsAlive` that takes an existing Hero and updates the IsAlive property. 146 | 147 | ```csharp 148 | public async Task SetIsAlive(int id, bool isAlive) 149 | { 150 | var hero = await _heroRepository.GetByIdAsync(id); 151 | 152 | hero.IsAlive = isAlive; 153 | 154 | await _heroRepository.UpdateAsync(hero); 155 | 156 | await _heroRepository.SaveChangesAsync(); 157 | 158 | return hero; 159 | } 160 | ``` 161 | 162 | Removing Heroes can be done either by Hero using `DeleteAsync` or by collection using `DeleteRangeAsync`. 163 | 164 | ```csharp 165 | public async Task Delete(Hero hero) 166 | { 167 | await _heroRepository.DeleteAsync(hero); 168 | 169 | await _heroRepository.SaveChangesAsync(); 170 | } 171 | 172 | public async Task DeleteRange(Hero[] heroes) 173 | { 174 | await _heroRepository.DeleteRangeAsync(heroes); 175 | 176 | await _heroRepository.SaveChangesAsync(); 177 | } 178 | ``` 179 | 180 | The `RepositoryBase` also provides two common Linq operations `CountAsync` and `AnyAsync`. 181 | 182 | ```csharp 183 | public async Task SeedData(Hero[] heroes) 184 | { 185 | // only seed if no Heroes exist 186 | if (await _heroRepository.AnyAsync()) 187 | { 188 | return; 189 | } 190 | 191 | // alternatively 192 | if (await _heroRepository.CountAsync() > 0) 193 | { 194 | return; 195 | } 196 | 197 | foreach (var hero in heroes) 198 | { 199 | await _heroRepository.AddAsync(hero); 200 | } 201 | 202 | await _heroRepository.SaveChangesAsync(); 203 | } 204 | ``` 205 | 206 | The full HeroService implementation is shown below. 207 | 208 | ```csharp 209 | public class HeroService 210 | { 211 | private readonly YourRepository _heroRepository; 212 | 213 | public HeroService(YourRepository heroRepository) 214 | { 215 | _heroRepository = heroRepository; 216 | } 217 | 218 | public async Task Create(string name, string superPower, bool isAlive, bool isAvenger) 219 | { 220 | var hero = new Hero(name, superPower, isAlive, isAvenger); 221 | 222 | await _heroRepository.AddAsync(hero); 223 | 224 | await _heroRepository.SaveChangesAsync(); 225 | 226 | return hero; 227 | } 228 | 229 | public async Task Delete(Hero hero) 230 | { 231 | await _heroRepository.DeleteAsync(hero); 232 | 233 | await _heroRepository.SaveChangesAsync(); 234 | } 235 | 236 | public async Task DeleteRange(List heroes) 237 | { 238 | await _heroRepository.DeleteRangeAsync(heroes); 239 | 240 | await _heroRepository.SaveChangesAsync(); 241 | } 242 | 243 | public async Task GetById(int id) 244 | { 245 | return await _heroRepository.GetByIdAsync(id); 246 | } 247 | 248 | public async Task GetByName(string name) 249 | { 250 | var spec = new HeroByNameSpec(name); 251 | 252 | return await _heroRepository.GetBySpecAsync(spec); 253 | } 254 | 255 | public async Task> GetHeroesFilteredByNameAndSuperPower(string name, string superPower) 256 | { 257 | var spec = new HeroByNameAndSuperPowerFilterSpec(name, superPower); 258 | 259 | return await _heroRepository.ListAsync(spec); 260 | } 261 | 262 | public async Task SetIsAlive(int id, bool isAlive) 263 | { 264 | var hero = await _heroRepository.GetByIdAsync(id); 265 | 266 | hero.IsAlive = isAlive; 267 | 268 | await _heroRepository.UpdateAsync(hero); 269 | 270 | await _heroRepository.SaveChangesAsync(); 271 | 272 | return hero; 273 | } 274 | 275 | public async Task SeedData(Hero[] heroes) 276 | { 277 | // only seed if no Heroes exist 278 | if (!await _heroRepository.AnyAsync()) 279 | { 280 | return; 281 | } 282 | 283 | // alternatively 284 | if (await _heroRepository.CountAsync() > 0) 285 | { 286 | return; 287 | } 288 | 289 | foreach (var hero in heroes) 290 | { 291 | await _heroRepository.AddAsync(hero); 292 | } 293 | 294 | await _heroRepository.SaveChangesAsync(); 295 | } 296 | } 297 | ``` 298 | 299 | ## Putting it all together 300 | 301 | The following sample program puts the methods described above together. Note the handling of dependencies is excluded for brevity. 302 | 303 | ```csharp 304 | public async Task Run() 305 | { 306 | var seedData = new[] 307 | { 308 | new Hero( 309 | name: "Batman", 310 | superPower: "Intelligence", 311 | isAlive: true, 312 | isAvenger: false), 313 | new Hero( 314 | name: "Iron Man", 315 | superPower: "Intelligence", 316 | isAlive: true, 317 | isAvenger: true), 318 | new Hero( 319 | name: "Spiderman", 320 | superPower: "Spidey Sense", 321 | isAlive: true, 322 | isAvenger: true), 323 | }; 324 | 325 | await heroService.SeedData(seedData); 326 | 327 | var captainAmerica = await heroService.Create("Captain America", "Shield", true, true); 328 | 329 | var ironMan = await heroService.GetByName("Iron Man"); 330 | 331 | var alsoIronMan = await heroService.GetById(ironMan.Id); 332 | 333 | await heroService.SetIsAlive(ironMan.Id, false); 334 | 335 | var shouldOnlyContainBatman = await heroService.GetHeroesFilteredByNameAndSuperPower("Bat", "Intel"); 336 | 337 | await heroService.Delete(captainAmerica); 338 | 339 | var allRemainingHeroes = await heroService.GetHeroesFilteredByNameAndSuperPower("", ""); 340 | 341 | await heroService.DeleteRange(allRemainingHeroes); 342 | } 343 | ``` 344 | 345 | ## Resources 346 | 347 | An in-depth demo of a similar implementation of the Repository Pattern and `RepositoryBase` can be found in the Repositories section of this [Pluralsight course](https://www.pluralsight.com/courses/domain-driven-design-fundamentals). 348 | -------------------------------------------------------------------------------- /docs/usage/use-specification-dbcontext.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: How to use Specifications with a DbContext 4 | parent: Usage 5 | nav_order: 3 6 | --- 7 | 8 | # How to use Specifications with a DbContext 9 | 10 | You can use Specifications to define queries that are executed directly using an EF6 or EF Core `DbContext`. 11 | The following snippet defines a `Customer` entity and a sample `DbContext` which defines a `DbSet` of Customers. 12 | 13 | ```csharp 14 | public class Customer 15 | { 16 | public int Id { get; set; } 17 | public string Name { get; set; } 18 | } 19 | 20 | public class SampleDbContext : DbContext 21 | { 22 | public DbSet Customers { get; set; } 23 | 24 | public SampleDbContext(DbContextOptions options) 25 | : base(options) 26 | { 27 | } 28 | 29 | // additional overrides intentionally left out 30 | } 31 | ``` 32 | 33 | A specification can be applied directly to this `DbSet` using the `WithSpecification` extension method defined in `Ardalis.Specification.EntityFrameworkCore` package. Assuming a Specification is defined similar to the `ItemByIdSpec` described in [How to Create Specifications](./creating-specifications.md), the following code demonstrates putting these pieces together. 34 | 35 | ```csharp 36 | // handling of IDisposable DbContext intentionally left out 37 | 38 | int id = 1; 39 | 40 | var specification = new CustomerByIdSpec(id); 41 | 42 | var customer = dbContext.Customers 43 | .WithSpecification(specification) 44 | .FirstOrDefault(); 45 | ``` 46 | 47 | Note that the `WithSpecification` extension method exposes an `IQueryable` so additional extension methods maybe be applied after the specification. Some examples can be found below. 48 | 49 | ```csharp 50 | bool isFound = dbContext.Customers.WithSpecification(specification).Any(); 51 | 52 | int customerCount = dbContext.Customers.WithSpecification(specification).Count(); 53 | 54 | var customers = dbContext.Customers.WithSpecification(specification).ToList(); 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/usage/use-specification-inmemory-collection.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: How to use Specifications with In Memory Collections 4 | parent: Usage 5 | nav_order: 4 6 | --- 7 | 8 | # How to use Specifications with In Memory Collections 9 | 10 | You can use Specifications on collections of objects in memory. This approach can be convenient when retrieving data doesn't require querying a remote or out of process data store like a database. If the process does require querying external persistence, it is better to refer to the practices for using a Specification with a [Repository Pattern](./use-specification-repository-pattern.md) or [DbContext](./use-specification-dbcontext.md). 11 | 12 | A Specification can be applied to an in memory collection using the `Evaluate` method. This method takes a single parameter of type `IEnumerable` representing the collection on which the underlying query expression will be appled to. In this example, the `GetEnvironment` method of the `Example` class handles creating a Specification and applying it to the collect static collection of Environment entities. 13 | 14 | ```csharp 15 | public class Environment 16 | { 17 | public string Name { get; set; } 18 | public string Description { get; set; } 19 | } 20 | 21 | public class EnvironmentByNameSpec : Specification 22 | { 23 | public EnvironmentByNameSpec(string name) 24 | { 25 | Query.Where(h => h.Name == name); 26 | } 27 | } 28 | 29 | public static class Example 30 | { 31 | private static readonly Environment[] Environments = new Environment[] 32 | { 33 | new() 34 | { 35 | Name = "DEV", 36 | Description = "this application's development environment" 37 | }, 38 | new() 39 | { 40 | Name = "QA", 41 | Description = "this application's QA environment" 42 | }, 43 | new() 44 | { 45 | Name = "PROD", 46 | Description = "this application's production environment" 47 | } 48 | }; 49 | 50 | public static Environment GetEnvironment(string name) 51 | { 52 | var specification = new EnvironmentByNameSpec(name); 53 | 54 | var environment = specification.Evaluate(Environments) 55 | .Single(); 56 | 57 | return environment; 58 | } 59 | } 60 | ``` 61 | -------------------------------------------------------------------------------- /docs/usage/use-specification-repository-pattern.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: How to use Specifications with the Repository Pattern 4 | parent: Usage 5 | nav_order: 2 6 | --- 7 | 8 | # How to use Specifications with the Repository Pattern 9 | 10 | Specifications shine when combined with the [Repository Pattern](https://deviq.com/design-patterns/repository-pattern), a sample generic implementation of which is included in this NuGet package. For the purpose of this walkthrough, the repository can be thought of as a simple data access abstraction over a collection of entities. In this example, the entity for a `Hero`, the repository implementation, and its interface are defined below. 11 | 12 | ```csharp 13 | public class Hero 14 | { 15 | public string Name { get; set; } 16 | public string SuperPower { get; set; } 17 | public bool IsAlive { get; set; } 18 | public bool IsAvenger { get; set; } 19 | } 20 | 21 | public interface IHeroRepository 22 | { 23 | List GetAllHeroes(); 24 | } 25 | 26 | public class HeroRepository : IHeroRepository 27 | { 28 | private readonly HeroDbContext _dbContext; 29 | 30 | public HeroRepository(HeroDbContext dbContext) 31 | { 32 | _dbContext = dbContext; 33 | } 34 | 35 | public List GetAllHeroes() 36 | { 37 | return _dbContext.Heroes.ToList(); 38 | } 39 | } 40 | ``` 41 | 42 | It's possible to extend this existing repository to support Specifications by adding a parameter for the specification to the `GetAllHeroes` method and then modifying the repository to apply the query of the Specification to the underlying data store. A basic implementation of this using the default value for `SpecificationEvaluator` and no `PostProcessingAction` is as follows. For a deeper dive, it is worth looking into the internals of [this abstract class](https://github.com/ardalis/Specification/blob/main/Specification.EntityFrameworkCore/src/Ardalis.Specification.EntityFrameworkCore/RepositoryBaseOfT.cs). This example also depends on DbContext provided by Entity Framework, although any IQueryable should work in place of `_dbContext.Heroes`. 43 | 44 | ```csharp 45 | public interface IHeroRepository 46 | { 47 | List GetAllHeroes(Specifcation specification); 48 | } 49 | 50 | public class HeroRepository : IHeroRepository 51 | { 52 | private readonly HeroDbContext _dbContext; 53 | 54 | public HeroRepository(HeroDbContext dbContext) 55 | { 56 | _dbContext = dbContext; 57 | } 58 | 59 | public List GetAllHeroes(ISpecification specification) 60 | { 61 | var queryResult = SpecificationEvaluator.Default.GetQuery( 62 | query: _dbContext.Heroes.AsQueryable(), 63 | specification: specification); 64 | 65 | return queryResult.ToList(); 66 | } 67 | } 68 | ``` 69 | 70 | Now that the Hero repository supports Specifications, a Specification can be defined that filters by whether the Hero is alive and is an Avenger. Note any fields that are needed to filter the Heroes are passed to the Specification's constructor where the query logic should be implemented. 71 | 72 | ```csharp 73 | public class HeroByIsAliveAndIsAvengerSpec : Specification 74 | { 75 | public HeroByIsAliveAndIsAvengerSpec(bool isAlive, bool isAvenger) 76 | { 77 | Query.Where(h => h.IsAlive == isAlive && h.IsAvenger == isAvenger); 78 | } 79 | } 80 | ``` 81 | 82 | With the Specification and Repository defined, it is now possible to define a `GetHeroes` method that can take a `HeroRepository` as a parameter along with the filtering conditions and produce a filtered collection of heroes. Applying the Repository to the Specification is done using the `Evaluate` method on the Specification class which takes a `IEnumerable` as a parameter. This should mirror the kind of methods typically found on Controllers or [Api Endpoints](https://github.com/ardalis/ApiEndpoints) where the IHeroRepository might be supplied via Dependency Injection to the class's constructor rather than passed as a parameter. 83 | 84 | ```csharp 85 | public List GetHeroes(IHeroRepository repository, bool isAlive, bool isAvenger) 86 | { 87 | var specification = new HeroByIsAliveAndIsAvengerSpec(isAlive, isAvenger); 88 | 89 | return repository.GetAllHeroes(specification); 90 | } 91 | ``` 92 | 93 | Suppose the data store behind the IHeroRepository has the following state and client code calls the `GetHeroes` as below. The result should be a collection containing only the Spider Man hero. 94 | 95 |
96 | 97 | | Name | SuperPower | IsAlive | IsAvenger | 98 | | :--------- | :----------- | :------ | :-------- | 99 | | Batman | Intelligence | true | false | 100 | | Iron Man | Intelligence | false | true | 101 | | Spider Man | Spidey Sense | true | true | 102 | 103 |
104 | 105 | ```csharp 106 | var result = GetHeroes(repository: repository, isAlive: true, isAvenger: true); 107 | ``` 108 | 109 | ## Further Reading 110 | 111 | For more information on the Repository Pattern and the sample generic implementation included in this package, see the [How to use the Built In Abstract Repository](./use-built-in-abstract-repository.md) tutorial. 112 | -------------------------------------------------------------------------------- /sample/Sample.FunctionalTests/AuthorEndpoints/CreateEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Newtonsoft.Json; 3 | using Sample.FunctionalTests.Models; 4 | using SampleEndpointApp; 5 | using SampleEndpointApp.DataAccess; 6 | using SampleEndpointApp.Endpoints.Authors; 7 | using Xunit; 8 | 9 | namespace Sample.FunctionalTests.AuthorEndpoints; 10 | 11 | public class CreateEndpoint : IClassFixture> 12 | { 13 | private readonly HttpClient _client; 14 | 15 | public CreateEndpoint(CustomWebApplicationFactory factory) 16 | { 17 | _client = factory.CreateClient(); 18 | } 19 | 20 | [Fact] 21 | public async Task CreatesANewAuthor() 22 | { 23 | var newAuthor = new CreateAuthorCommand() 24 | { 25 | Name = "James Eastham", 26 | PluralsightUrl = "https://app.pluralsight.com", 27 | TwitterAlias = "jeasthamdev", 28 | }; 29 | 30 | var lastAuthor = SeedData.Authors().Last(); 31 | 32 | var response = await _client.PostAsync(Routes.Authors.Create, new StringContent(JsonConvert.SerializeObject(newAuthor), Encoding.UTF8, "application/json")); 33 | 34 | response.EnsureSuccessStatusCode(); 35 | var stringResponse = await response.Content.ReadAsStringAsync(); 36 | var result = JsonConvert.DeserializeObject(stringResponse); 37 | 38 | Assert.NotNull(result); 39 | Assert.Equal(result.Id, lastAuthor.Id + 1); 40 | Assert.Equal(result.Name, newAuthor.Name); 41 | Assert.Equal(result.PluralsightUrl, newAuthor.PluralsightUrl); 42 | Assert.Equal(result.TwitterAlias, newAuthor.TwitterAlias); 43 | } 44 | 45 | [Fact] 46 | public async Task GivenLongRunningCreateRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated() 47 | { 48 | // Arrange, generate a token source that times out instantly 49 | var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(0)); 50 | var lastAuthor = SeedData.Authors().Last(); 51 | var newAuthor = new CreateAuthorCommand() 52 | { 53 | Name = "James Eastham", 54 | PluralsightUrl = "https://app.pluralsight.com", 55 | TwitterAlias = "jeasthamdev", 56 | }; 57 | 58 | // Act 59 | var request = _client.PostAsync(Routes.Authors.Create, new StringContent(JsonConvert.SerializeObject(newAuthor), Encoding.UTF8, "application/json"), tokenSource.Token); 60 | 61 | // Assert 62 | await Assert.ThrowsAsync(async () => await request); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /sample/Sample.FunctionalTests/AuthorEndpoints/DeleteEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.HttpClientTestExtensions; 2 | using Newtonsoft.Json; 3 | using Sample.FunctionalTests.Models; 4 | using SampleEndpointApp; 5 | using SampleEndpointApp.DomainModel; 6 | using Xunit; 7 | 8 | namespace Sample.FunctionalTests.AuthorEndpoints; 9 | 10 | public class DeleteEndpoint : IClassFixture> 11 | { 12 | private readonly HttpClient _client; 13 | 14 | public DeleteEndpoint(CustomWebApplicationFactory factory) 15 | { 16 | _client = factory.CreateClient(); 17 | } 18 | 19 | [Fact] 20 | public async Task DeleteAnExistingAuthor() 21 | { 22 | int existingAuthorId = 2; 23 | string route = Routes.Authors.Delete(existingAuthorId); 24 | 25 | var response = await _client.DeleteAsync(route); 26 | response.EnsureSuccessStatusCode(); 27 | 28 | var listResponse = await _client.GetAsync(Routes.Authors.List()); 29 | listResponse.EnsureSuccessStatusCode(); 30 | var stringListResponse = await listResponse.Content.ReadAsStringAsync(); 31 | var listResult = JsonConvert.DeserializeObject>(stringListResponse); 32 | 33 | Assert.True(listResult.Count() <= 2); 34 | } 35 | 36 | [Fact] 37 | public async Task ReturnsNotFoundGivenNonexistingAuthor() 38 | { 39 | int nonexistingAuthorId = 2222; 40 | string route = Routes.Authors.Delete(nonexistingAuthorId); 41 | 42 | await _client.DeleteAndEnsureNotFound(route); 43 | } 44 | 45 | [Fact] 46 | public Task GivenLongRunningDeleteRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated() 47 | { 48 | // Arrange, generate a token source that times out instantly 49 | var tokenSource = new CancellationTokenSource(TimeSpan.Zero); 50 | 51 | // Act 52 | int existingAuthorId = 2; 53 | string route = Routes.Authors.Delete(existingAuthorId); 54 | var request = _client.DeleteAsync(route, tokenSource.Token); 55 | 56 | // Assert 57 | return Assert.ThrowsAsync(async () => await request); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /sample/Sample.FunctionalTests/AuthorEndpoints/GetEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.HttpClientTestExtensions; 2 | using Newtonsoft.Json; 3 | using Sample.FunctionalTests.Models; 4 | using SampleEndpointApp; 5 | using SampleEndpointApp.DataAccess; 6 | using SampleEndpointApp.DomainModel; 7 | using Xunit; 8 | 9 | namespace Sample.FunctionalTests.AuthorEndpoints; 10 | 11 | public class GetEndpoint : IClassFixture> 12 | { 13 | private readonly HttpClient _client; 14 | 15 | public GetEndpoint(CustomWebApplicationFactory factory) 16 | { 17 | _client = factory.CreateClient(); 18 | } 19 | 20 | [Fact] 21 | public async Task ReturnsAuthorById() 22 | { 23 | var firstAuthor = SeedData.Authors().First(); 24 | 25 | var response = await _client.GetAsync(Routes.Authors.Get(firstAuthor.Id)); 26 | response.EnsureSuccessStatusCode(); 27 | var stringResponse = await response.Content.ReadAsStringAsync(); 28 | var result = JsonConvert.DeserializeObject(stringResponse); 29 | 30 | Assert.NotNull(result); 31 | Assert.Equal(firstAuthor.Id, result.Id); 32 | Assert.Equal(firstAuthor.Name, result.Name); 33 | Assert.Equal(firstAuthor.PluralsightUrl, result.PluralsightUrl); 34 | Assert.Equal(firstAuthor.TwitterAlias, result.TwitterAlias); 35 | } 36 | 37 | [Fact] 38 | public async Task ReturnsNotFoundGivenInvalidAuthorId() 39 | { 40 | int invalidId = 9999; 41 | 42 | var response = await _client.GetAsync(Routes.Authors.Get(invalidId)); 43 | 44 | response.EnsureNotFound(); 45 | } 46 | 47 | [Fact] 48 | public async Task GivenLongRunningGetRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated() 49 | { 50 | // Arrange, generate a token source that times out instantly 51 | var tokenSource = new CancellationTokenSource(TimeSpan.Zero); 52 | var firstAuthor = SeedData.Authors().First(); 53 | 54 | // Act 55 | var request = _client.GetAsync(Routes.Authors.Get(firstAuthor.Id), tokenSource.Token); 56 | 57 | // Assert 58 | await Assert.ThrowsAsync(async () => await request); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /sample/Sample.FunctionalTests/AuthorEndpoints/ListEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.HttpClientTestExtensions; 2 | using Sample.FunctionalTests.Models; 3 | using SampleEndpointApp; 4 | using SampleEndpointApp.DataAccess; 5 | using SampleEndpointApp.DomainModel; 6 | using Xunit; 7 | 8 | namespace Sample.FunctionalTests.AuthorEndpoints; 9 | 10 | public class ListEndpoint : IClassFixture> 11 | { 12 | private readonly HttpClient _client; 13 | 14 | public ListEndpoint(CustomWebApplicationFactory factory) 15 | { 16 | _client = factory.CreateClient(); 17 | } 18 | 19 | [Fact] 20 | public async Task ReturnsTwoGivenTwoAuthors() 21 | { 22 | var result = await _client.GetAndDeserialize>(Routes.Authors.List()); 23 | 24 | Assert.NotNull(result); 25 | Assert.Equal(SeedData.Authors().Count, result.Count()); 26 | } 27 | 28 | [Fact] 29 | public async Task GivenLongRunningListRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated() 30 | { 31 | // Arrange, generate a token source that times out instantly 32 | var tokenSource = new CancellationTokenSource(TimeSpan.Zero); 33 | 34 | // Act 35 | var request = _client.GetAsync(Routes.Authors.List(), tokenSource.Token); 36 | 37 | // Assert 38 | var response = await Assert.ThrowsAsync(async () => await request); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sample/Sample.FunctionalTests/AuthorEndpoints/ListPaginatedEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.HttpClientTestExtensions; 2 | using Sample.FunctionalTests.Models; 3 | using SampleEndpointApp; 4 | using SampleEndpointApp.DomainModel; 5 | using Xunit; 6 | 7 | namespace Sample.FunctionalTests.AuthorEndpoints; 8 | 9 | public class ListPaginatedEndpoint : IClassFixture> 10 | { 11 | private readonly HttpClient _client; 12 | 13 | public ListPaginatedEndpoint(CustomWebApplicationFactory factory) 14 | { 15 | _client = factory.CreateClient(); 16 | } 17 | 18 | [Fact] 19 | public async Task Page1PerPage1_ShouldReturnFirstAuthor() 20 | { 21 | var result = await _client.GetAndDeserialize>(Routes.Authors.List(1, 1)); 22 | 23 | Assert.NotNull(result); 24 | Assert.Single(result); 25 | } 26 | 27 | [Fact] 28 | public async Task GivenLongRunningPaginatedListRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated() 29 | { 30 | // Arrange, generate a token source that times out instantly 31 | var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(0)); 32 | 33 | // Act 34 | var request = _client.GetAsync(Routes.Authors.List(1, 1), tokenSource.Token); 35 | 36 | // Assert 37 | var response = await Assert.ThrowsAsync(async () => await request); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sample/Sample.FunctionalTests/AuthorEndpoints/StreamEndpoint.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.HttpClientTestExtensions; 2 | using Sample.FunctionalTests.Models; 3 | using SampleEndpointApp; 4 | using SampleEndpointApp.DataAccess; 5 | using SampleEndpointApp.DomainModel; 6 | using Xunit; 7 | 8 | namespace Sample.FunctionalTests.AuthorEndpoints; 9 | 10 | public class StreamEndpoint : IClassFixture> 11 | { 12 | private readonly HttpClient _client; 13 | 14 | public StreamEndpoint(CustomWebApplicationFactory factory) 15 | { 16 | _client = factory.CreateClient(); 17 | } 18 | 19 | [Fact] 20 | public async Task ReturnsTwoGivenTwoAuthors() 21 | { 22 | var result = await _client.GetAndDeserialize>(Routes.Authors.Stream()); 23 | 24 | Assert.NotNull(result); 25 | Assert.Equal(SeedData.Authors().Count, result.Count()); 26 | } 27 | 28 | [Fact] 29 | public async Task GivenLongRunningListRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated() 30 | { 31 | // Arrange, generate a token source that times out instantly 32 | var tokenSource = new CancellationTokenSource(TimeSpan.Zero); 33 | 34 | // Act 35 | var request = _client.GetAsync(Routes.Authors.Stream(), tokenSource.Token); 36 | 37 | // Assert 38 | var response = await Assert.ThrowsAsync(async () => await request); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sample/Sample.FunctionalTests/AuthorEndpoints/UpdateEndpoint.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Ardalis.HttpClientTestExtensions; 3 | using Newtonsoft.Json; 4 | using Sample.FunctionalTests.Models; 5 | using SampleEndpointApp; 6 | using SampleEndpointApp.DataAccess; 7 | using SampleEndpointApp.Endpoints.Authors; 8 | using Xunit; 9 | 10 | namespace Sample.FunctionalTests.AuthorEndpoints; 11 | 12 | public class UpdateEndpoint : IClassFixture> 13 | { 14 | private readonly HttpClient _client; 15 | 16 | public UpdateEndpoint(CustomWebApplicationFactory factory) 17 | { 18 | _client = factory.CreateClient(); 19 | } 20 | 21 | [Fact] 22 | public async Task UpdatesAnExistingAuthor() 23 | { 24 | var updatedAuthor = new UpdateAuthorCommand() 25 | { 26 | Id = 2, 27 | Name = "James Eastham", 28 | }; 29 | 30 | var authorPreUpdate = SeedData.Authors().First(p => p.Id == 2); 31 | 32 | var response = await _client.PutAsync(Routes.Authors.Update, new StringContent(JsonConvert.SerializeObject(updatedAuthor), Encoding.UTF8, "application/json")); 33 | 34 | response.EnsureSuccessStatusCode(); 35 | var stringResponse = await response.Content.ReadAsStringAsync(); 36 | var result = JsonConvert.DeserializeObject(stringResponse); 37 | 38 | Assert.NotNull(result); 39 | Assert.Equal(result.Id, updatedAuthor.Id.ToString()); 40 | Assert.NotEqual(result.Name, authorPreUpdate.Name); 41 | Assert.Equal("James Eastham", result.Name); 42 | Assert.Equal(result.PluralsightUrl, authorPreUpdate.PluralsightUrl); 43 | Assert.Equal(result.TwitterAlias, authorPreUpdate.TwitterAlias); 44 | } 45 | 46 | [Fact] 47 | public async Task ReturnsNotFoundGivenNonexistingAuthor() 48 | { 49 | var updatedAuthor = new UpdateAuthorCommand() 50 | { 51 | Id = 2222, // invalid author 52 | Name = "Doesn't Matter", 53 | }; 54 | 55 | await _client.PutAndEnsureNotFound(Routes.Authors.Update, new StringContent(JsonConvert.SerializeObject(updatedAuthor), Encoding.UTF8, "application/json")); 56 | } 57 | 58 | [Fact] 59 | public async Task GivenLongRunningUpdateRequest_WhenTokenSourceCallsForCancellation_RequestIsTerminated() 60 | { 61 | // Arrange, generate a token source that times out instantly 62 | var tokenSource = new CancellationTokenSource(TimeSpan.FromMilliseconds(0)); 63 | var authorPreUpdate = SeedData.Authors().FirstOrDefault(p => p.Id == 2); 64 | var updatedAuthor = new UpdateAuthorCommand() 65 | { 66 | Id = 2, 67 | Name = "James Eastham", 68 | }; 69 | 70 | // Act 71 | var request = _client.PutAsync(Routes.Authors.Update, new StringContent(JsonConvert.SerializeObject(updatedAuthor), Encoding.UTF8, "application/json"), tokenSource.Token); 72 | 73 | // Assert 74 | await Assert.ThrowsAsync(async () => await request); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /sample/Sample.FunctionalTests/CustomWebApplicationFactory.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.Mvc.Testing; 3 | using Microsoft.AspNetCore.TestHost; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Logging; 7 | using SampleEndpointApp; 8 | using SampleEndpointApp.DataAccess; 9 | 10 | namespace Sample.FunctionalTests; 11 | 12 | public class CustomWebApplicationFactory : WebApplicationFactory 13 | { 14 | protected override void ConfigureWebHost(IWebHostBuilder builder) 15 | { 16 | builder 17 | .UseSolutionRelativeContentRoot("sample/SampleEndpointApp") 18 | .ConfigureServices(services => 19 | { 20 | var descriptor = services.SingleOrDefault( 21 | d => d.ServiceType == 22 | typeof(DbContextOptions)); 23 | 24 | if (descriptor != null) 25 | { 26 | // remove default (real) implementation 27 | services.Remove(descriptor); 28 | } 29 | 30 | // Create a new service provider. 31 | var serviceProvider = new ServiceCollection() 32 | .AddEntityFrameworkInMemoryDatabase() 33 | .BuildServiceProvider(); 34 | 35 | // Add a database context (AppDbContext) using an in-memory 36 | // database for testing. 37 | services.AddDbContext(options => 38 | { 39 | options.UseInMemoryDatabase($"InMemoryDbForTesting"); 40 | options.UseInternalServiceProvider(serviceProvider); 41 | }); 42 | 43 | // Build the service provider. 44 | var sp = services.BuildServiceProvider(); 45 | 46 | // Create a scope to obtain a reference to the database 47 | // context (AppDbContext). 48 | using var scope = sp.CreateScope(); 49 | var scopedServices = scope.ServiceProvider; 50 | var db = scopedServices.GetRequiredService(); 51 | 52 | var logger = scopedServices 53 | .GetRequiredService>>(); 54 | 55 | // Ensure the database is created. 56 | db.Database.EnsureCreated(); 57 | 58 | try 59 | { 60 | // Seed the database with test data. 61 | SeedData.PopulateTestData(db); 62 | } 63 | catch (Exception ex) 64 | { 65 | logger.LogError(ex, "An error occurred seeding the " + 66 | $"database with test messages. Error: {ex.Message}"); 67 | } 68 | }); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /sample/Sample.FunctionalTests/Models/Routes.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.FunctionalTests.Models; 2 | 3 | public static class Routes 4 | { 5 | private const string BaseRoute = "api"; 6 | 7 | public static class Authors 8 | { 9 | private const string BaseRoute = Routes.BaseRoute + "/authors"; 10 | 11 | public const string Create = BaseRoute; 12 | 13 | public const string Update = BaseRoute; 14 | 15 | public static string List() => BaseRoute; 16 | 17 | public static string List(int perPage, int page) => $"{BaseRoute}?perPage={perPage}&page={page}"; 18 | 19 | public static string Stream() => $"{BaseRoute}/stream"; 20 | 21 | public static string Get(int id) => $"{BaseRoute}/{id}"; 22 | 23 | public static string Delete(int id) => $"{BaseRoute}/{id}"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /sample/Sample.FunctionalTests/Sample.FunctionalTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | disable 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /sample/Sample.WeatherForecast/.template.config/template.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/template", 3 | "author": "Steve Smith @ardalis, Ali zaferany", 4 | 5 | "name": "Ardalis.ApiEndpoint Template", 6 | "description": "A project template for creating an ASP.NET Core application with an example ApiEndpoints for a RESTful HTTP service.", 7 | "identity": "Ardalis.ApiEndpoints.", 8 | "shortName": "apiendpoints", 9 | "sourceName": "Sample.WeatherForecast", 10 | "defaultName": "WebApplication", 11 | "preferNameDirectory": true, 12 | "classifications": [ 13 | "Web", 14 | "WebAPI", 15 | "C#" 16 | ], 17 | "tags": { 18 | "language": "C#", 19 | "type": "project" 20 | }, 21 | "sources": [ 22 | { 23 | "source": "./", 24 | "target": "./", 25 | "exclude": [ 26 | "README.md", 27 | "**/[Bb]in/**", 28 | "**/[Oo]bj/**", 29 | ".template.config/**/*", 30 | ".vs/**/*", 31 | "**/*.filelist", 32 | "**/*.user", 33 | "**/*.lock.json", 34 | "**/.git/**", 35 | "*.nuspec", 36 | "**/node_modules/**", 37 | "riderModule.iml" , 38 | "/_ReSharper.Caches/" , 39 | ".idea/" , 40 | "/.idea/" , 41 | ".git/" , 42 | "/.git/" , 43 | "logs/", 44 | ".releaserc", 45 | "templatepack.csproj"] 46 | } 47 | ] 48 | } -------------------------------------------------------------------------------- /sample/Sample.WeatherForecast/Endpoints/Get.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.ApiEndpoints; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Swashbuckle.AspNetCore.Annotations; 4 | 5 | namespace Sample.WeatherForecast.Endpoints; 6 | 7 | public class Get : EndpointBaseSync 8 | .WithoutRequest 9 | .WithActionResult> 10 | { 11 | private static readonly string[] Summaries = new[] 12 | { 13 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 14 | }; 15 | 16 | private readonly ILogger _logger; 17 | 18 | public Get(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | [HttpGet("/WeatherForecast")] 24 | [SwaggerOperation( 25 | Summary = "Get weather forecast", 26 | OperationId = "WeatherForecast_Get", 27 | Tags = new[] { "WeatherForecast" }) 28 | ] 29 | public override ActionResult> Handle() 30 | { 31 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 32 | { 33 | Date = DateTime.Now.AddDays(index), 34 | TemperatureC = Random.Shared.Next(-20, 55), 35 | Summary = Summaries[Random.Shared.Next(Summaries.Length)] 36 | }) 37 | .ToArray(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /sample/Sample.WeatherForecast/Program.cs: -------------------------------------------------------------------------------- 1 | var builder = WebApplication.CreateBuilder(args); 2 | 3 | // Add services to the container. 4 | 5 | builder.Services.AddControllers(); 6 | 7 | // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle 8 | builder.Services.AddEndpointsApiExplorer(); 9 | builder.Services.AddSwaggerGen(options => options.EnableAnnotations()); 10 | 11 | var app = builder.Build(); 12 | 13 | // Configure the HTTP request pipeline. 14 | if (app.Environment.IsDevelopment()) 15 | { 16 | app.UseSwagger(); 17 | app.UseSwaggerUI(); 18 | } 19 | 20 | app.UseRouting(); 21 | 22 | app.MapControllers(); 23 | 24 | app.Run(); 25 | -------------------------------------------------------------------------------- /sample/Sample.WeatherForecast/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:56251", 8 | "sslPort": 44385 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5218", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7277;http://localhost:5218", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sample/Sample.WeatherForecast/Sample.WeatherForecast.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample/Sample.WeatherForecast/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | namespace Sample.WeatherForecast; 2 | 3 | public class WeatherForecast 4 | { 5 | public DateTime Date { get; set; } 6 | 7 | public int TemperatureC { get; set; } 8 | 9 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 10 | 11 | public string? Summary { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /sample/Sample.WeatherForecast/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /sample/Sample.WeatherForecast/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*" 9 | } 10 | -------------------------------------------------------------------------------- /sample/Sample.WeatherForecast/templatepack.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Template 4 | 4.0.1 5 | Ardalis.ApiEndpoints.Templates 6 | Ardalis.ApiEndpoint 7 | Steve Smith @ardalis, Ali zaferany 8 | A project template for creating an ASP.NET Core application with an example ApiEndpoints for a RESTful HTTP service. 9 | dotnet-new;dotnet;templates;csharp;api;apiendpoints 10 | netstandard2.0 11 | true 12 | false 13 | content 14 | $(NoWarn);NU5128 15 | true 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/AutoMapping.cs: -------------------------------------------------------------------------------- 1 | using AutoMapper; 2 | using SampleEndpointApp.DomainModel; 3 | using SampleEndpointApp.Endpoints.Authors; 4 | 5 | namespace SampleEndpointApp; 6 | 7 | public class AutoMapping : Profile 8 | { 9 | public AutoMapping() 10 | { 11 | CreateMap(); 12 | CreateMap(); 13 | CreateMap(); 14 | 15 | CreateMap(); 16 | CreateMap(); 17 | CreateMap(); 18 | CreateMap(); 19 | CreateMap(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/DataAccess/AppDbContext.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.EFCore.Extensions; 2 | using Microsoft.EntityFrameworkCore; 3 | using SampleEndpointApp.DomainModel; 4 | 5 | namespace SampleEndpointApp.DataAccess; 6 | 7 | public class AppDbContext : DbContext 8 | { 9 | public AppDbContext(DbContextOptions options) : base(options) 10 | { 11 | } 12 | 13 | public DbSet Authors { get; set; } = null!; 14 | 15 | protected override void OnModelCreating(ModelBuilder modelBuilder) 16 | { 17 | base.OnModelCreating(modelBuilder); 18 | 19 | modelBuilder.ApplyAllConfigurationsFromCurrentAssembly(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/DataAccess/Config/AuthorConfig.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 3 | using SampleEndpointApp.DomainModel; 4 | 5 | namespace SampleEndpointApp.DataAccess.Config; 6 | 7 | public class AuthorConfig : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(e => e.Name) 12 | .IsRequired(); 13 | 14 | builder.Property(e => e.PluralsightUrl) 15 | .IsRequired(); 16 | 17 | builder.HasData(SeedData.Authors()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/DataAccess/EfRepository.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore; 2 | using SampleEndpointApp.DomainModel; 3 | 4 | namespace SampleEndpointApp.DataAccess; 5 | 6 | /// 7 | /// Source: My reference app https://github.com/dotnet-architecture/eShopOnWeb 8 | /// Check it out if you need filtering/paging/etc. 9 | /// Also consider Ardalis.Specification and its built-in generic repository 10 | /// 11 | public class EfRepository : IAsyncRepository where T : BaseEntity 12 | { 13 | protected readonly AppDbContext _dbContext; 14 | 15 | public EfRepository(AppDbContext dbContext) 16 | { 17 | _dbContext = dbContext; 18 | } 19 | 20 | public virtual async Task GetByIdAsync(int id, CancellationToken cancellationToken) 21 | { 22 | return await _dbContext.Set().FirstOrDefaultAsync(a => a.Id == id, cancellationToken); 23 | } 24 | 25 | public async Task> ListAllAsync(CancellationToken cancellationToken) 26 | { 27 | return await _dbContext.Set().ToListAsync(cancellationToken); 28 | } 29 | 30 | /// 31 | public async Task> ListAllAsync( 32 | int perPage, 33 | int page, 34 | CancellationToken cancellationToken) 35 | { 36 | return await _dbContext.Set().Skip(perPage * (page - 1)).Take(perPage).ToListAsync(cancellationToken); 37 | } 38 | 39 | public async Task AddAsync(T entity, CancellationToken cancellationToken) 40 | { 41 | await _dbContext.Set().AddAsync(entity, cancellationToken); 42 | await _dbContext.SaveChangesAsync(cancellationToken); 43 | 44 | return entity; 45 | } 46 | 47 | public async Task UpdateAsync(T entity, CancellationToken cancellationToken) 48 | { 49 | _dbContext.Entry(entity).State = EntityState.Modified; 50 | await _dbContext.SaveChangesAsync(cancellationToken); 51 | } 52 | 53 | public async Task DeleteAsync(T entity, CancellationToken cancellationToken) 54 | { 55 | _dbContext.Set().Remove(entity); 56 | await _dbContext.SaveChangesAsync(cancellationToken); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/DataAccess/Migrations/20200209060455_Initial.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 SampleEndpointApp.DataAccess; 7 | 8 | namespace SampleEndpointApp.DataAccess.Migrations 9 | { 10 | [DbContext(typeof(AppDbContext))] 11 | [Migration("20200209060455_Initial")] 12 | partial class Initial 13 | { 14 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 15 | { 16 | #pragma warning disable 612, 618 17 | modelBuilder 18 | .HasAnnotation("ProductVersion", "3.1.1"); 19 | 20 | modelBuilder.Entity("SampleEndpointApp.DomainModel.Author", b => 21 | { 22 | b.Property("Id") 23 | .ValueGeneratedOnAdd() 24 | .HasColumnType("INTEGER"); 25 | 26 | b.Property("Name") 27 | .IsRequired() 28 | .HasColumnType("TEXT"); 29 | 30 | b.Property("PluralsightUrl") 31 | .IsRequired() 32 | .HasColumnType("TEXT"); 33 | 34 | b.Property("TwitterAlias") 35 | .HasColumnType("TEXT"); 36 | 37 | b.HasKey("Id"); 38 | 39 | b.ToTable("Authors"); 40 | 41 | b.HasData( 42 | new 43 | { 44 | Id = 1, 45 | Name = "Steve Smith", 46 | PluralsightUrl = "", 47 | TwitterAlias = "ardalis" 48 | }, 49 | new 50 | { 51 | Id = 2, 52 | Name = "Julie Lerman", 53 | PluralsightUrl = "", 54 | TwitterAlias = "julialerman" 55 | }); 56 | }); 57 | #pragma warning restore 612, 618 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/DataAccess/Migrations/20200209060455_Initial.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | 3 | namespace SampleEndpointApp.DataAccess.Migrations; 4 | 5 | public partial class Initial : Migration 6 | { 7 | protected override void Up(MigrationBuilder migrationBuilder) 8 | { 9 | migrationBuilder.CreateTable( 10 | name: "Authors", 11 | columns: table => new 12 | { 13 | Id = table.Column(nullable: false) 14 | .Annotation("Sqlite:Autoincrement", true), 15 | Name = table.Column(nullable: false), 16 | PluralsightUrl = table.Column(nullable: false), 17 | TwitterAlias = table.Column(nullable: true) 18 | }, 19 | constraints: table => 20 | { 21 | table.PrimaryKey("PK_Authors", x => x.Id); 22 | }); 23 | 24 | migrationBuilder.InsertData( 25 | table: "Authors", 26 | columns: new[] { "Id", "Name", "PluralsightUrl", "TwitterAlias" }, 27 | values: new object[] { 1, "Steve Smith", "", "ardalis" }); 28 | 29 | migrationBuilder.InsertData( 30 | table: "Authors", 31 | columns: new[] { "Id", "Name", "PluralsightUrl", "TwitterAlias" }, 32 | values: new object[] { 2, "Julie Lerman", "", "julialerman" }); 33 | } 34 | 35 | protected override void Down(MigrationBuilder migrationBuilder) 36 | { 37 | migrationBuilder.DropTable( 38 | name: "Authors"); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/DataAccess/Migrations/AppDbContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Infrastructure; 4 | using Microsoft.EntityFrameworkCore.Storage.ValueConversion; 5 | using SampleEndpointApp.DataAccess; 6 | 7 | namespace SampleEndpointApp.DataAccess.Migrations 8 | { 9 | [DbContext(typeof(AppDbContext))] 10 | partial class AppDbContextModelSnapshot : ModelSnapshot 11 | { 12 | protected override void BuildModel(ModelBuilder modelBuilder) 13 | { 14 | #pragma warning disable 612, 618 15 | modelBuilder 16 | .HasAnnotation("ProductVersion", "3.1.1"); 17 | 18 | modelBuilder.Entity("SampleEndpointApp.DomainModel.Author", b => 19 | { 20 | b.Property("Id") 21 | .ValueGeneratedOnAdd() 22 | .HasColumnType("INTEGER"); 23 | 24 | b.Property("Name") 25 | .IsRequired() 26 | .HasColumnType("TEXT"); 27 | 28 | b.Property("PluralsightUrl") 29 | .IsRequired() 30 | .HasColumnType("TEXT"); 31 | 32 | b.Property("TwitterAlias") 33 | .HasColumnType("TEXT"); 34 | 35 | b.HasKey("Id"); 36 | 37 | b.ToTable("Authors"); 38 | 39 | b.HasData( 40 | new 41 | { 42 | Id = 1, 43 | Name = "Steve Smith", 44 | PluralsightUrl = "", 45 | TwitterAlias = "ardalis" 46 | }, 47 | new 48 | { 49 | Id = 2, 50 | Name = "Julie Lerman", 51 | PluralsightUrl = "", 52 | TwitterAlias = "julialerman" 53 | }); 54 | }); 55 | #pragma warning restore 612, 618 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/DataAccess/SeedData.cs: -------------------------------------------------------------------------------- 1 | using SampleEndpointApp.DomainModel; 2 | 3 | namespace SampleEndpointApp.DataAccess; 4 | 5 | public static class SeedData 6 | { 7 | public static List Authors() 8 | { 9 | int id = 1; 10 | 11 | var authors = new List() 12 | { 13 | new Author 14 | { 15 | Id = id++, 16 | Name="Steve Smith", 17 | PluralsightUrl="https://www.pluralsight.com/authors/steve-smith", 18 | TwitterAlias="ardalis" 19 | }, 20 | new Author 21 | { 22 | Id = id++, 23 | Name="Julie Lerman", 24 | PluralsightUrl="https://www.pluralsight.com/authors/julie-lerman", 25 | TwitterAlias="julialerman" 26 | } 27 | }; 28 | 29 | return authors; 30 | } 31 | 32 | public static void PopulateTestData(AppDbContext dbContext) 33 | { 34 | dbContext.Authors.AddRange(Authors()); 35 | 36 | dbContext.SaveChanges(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/DomainModel/Author.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp.DomainModel; 2 | 3 | public class Author : BaseEntity 4 | { 5 | public string Name { get; set; } = null!; 6 | public string PluralsightUrl { get; set; } = null!; 7 | public string? TwitterAlias { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/DomainModel/BaseEntity.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp.DomainModel; 2 | 3 | public abstract class BaseEntity 4 | { 5 | public int Id { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/DomainModel/IAsyncRepository.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp.DomainModel; 2 | 3 | /// 4 | /// Source: My reference app https://github.com/dotnet-architecture/eShopOnWeb 5 | /// 6 | /// 7 | public interface IAsyncRepository where T : BaseEntity 8 | { 9 | Task GetByIdAsync(int id, CancellationToken cancellationToken); 10 | 11 | Task> ListAllAsync(CancellationToken cancellationToken); 12 | 13 | Task> ListAllAsync(int perPage, int page, CancellationToken cancellationToken); 14 | 15 | //Task> ListAsync(ISpecification spec); 16 | 17 | Task AddAsync(T entity, CancellationToken cancellationToken); 18 | 19 | Task UpdateAsync(T entity, CancellationToken cancellationToken); 20 | 21 | Task DeleteAsync(T entity, CancellationToken cancellationToken); 22 | 23 | //Task CountAsync(ISpecification spec); 24 | } 25 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Create.CreateAuthorCommand.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace SampleEndpointApp.Endpoints.Authors; 4 | 5 | public class CreateAuthorCommand 6 | { 7 | [Required] 8 | public string Name { get; set; } = null!; 9 | [Required] 10 | public string PluralsightUrl { get; set; } = null!; 11 | public string? TwitterAlias { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Create.CreateAuthorResult.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp.Endpoints.Authors; 2 | 3 | public class CreateAuthorResult : CreateAuthorCommand 4 | { 5 | public int Id { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Create.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.ApiEndpoints; 2 | using AutoMapper; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SampleEndpointApp.DomainModel; 5 | 6 | namespace SampleEndpointApp.Endpoints.Authors; 7 | 8 | public class Create : EndpointBaseAsync 9 | .WithRequest 10 | .WithActionResult 11 | { 12 | private readonly IAsyncRepository _repository; 13 | private readonly IMapper _mapper; 14 | 15 | public Create(IAsyncRepository repository, 16 | IMapper mapper) 17 | { 18 | _repository = repository; 19 | _mapper = mapper; 20 | } 21 | 22 | /// 23 | /// Creates a new Author 24 | /// 25 | [HttpPost("api/[namespace]")] 26 | public override async Task HandleAsync([FromBody] CreateAuthorCommand request, CancellationToken cancellationToken) 27 | { 28 | var author = new Author(); 29 | _mapper.Map(request, author); 30 | await _repository.AddAsync(author, cancellationToken); 31 | 32 | var result = _mapper.Map(author); 33 | return CreatedAtRoute("Authors_Get", new { id = result.Id }, result); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Delete.DeleteAuthorRequest.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp.Endpoints.Authors; 2 | 3 | public class DeleteAuthorRequest 4 | { 5 | public int Id { get; set; } 6 | } 7 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Delete.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.ApiEndpoints; 2 | using Microsoft.AspNetCore.Mvc; 3 | using SampleEndpointApp.DomainModel; 4 | 5 | namespace SampleEndpointApp.Endpoints.Authors; 6 | 7 | public class Delete : EndpointBaseAsync 8 | .WithRequest 9 | .WithActionResult 10 | { 11 | private readonly IAsyncRepository _repository; 12 | 13 | public Delete(IAsyncRepository repository) 14 | { 15 | _repository = repository; 16 | } 17 | 18 | /// 19 | /// Deletes an Author 20 | /// 21 | [HttpDelete("api/[namespace]/{id}")] 22 | public override async Task HandleAsync([FromRoute] DeleteAuthorRequest request, CancellationToken cancellationToken) 23 | { 24 | var author = await _repository.GetByIdAsync(request.Id, cancellationToken); 25 | 26 | if (author is null) 27 | { 28 | return NotFound(request.Id); 29 | } 30 | 31 | await _repository.DeleteAsync(author, cancellationToken); 32 | 33 | // see https://restfulapi.net/http-methods/#delete 34 | return NoContent(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Get.AuthorResult.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp.Endpoints.Authors; 2 | 3 | public class AuthorResult 4 | { 5 | public string Id { get; set; } = null!; 6 | public string Name { get; set; } = null!; 7 | public string PluralsightUrl { get; set; } = null!; 8 | public string? TwitterAlias { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Get.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.ApiEndpoints; 2 | using AutoMapper; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SampleEndpointApp.DomainModel; 5 | 6 | namespace SampleEndpointApp.Endpoints.Authors; 7 | 8 | public class Get : EndpointBaseAsync 9 | .WithRequest 10 | .WithActionResult 11 | { 12 | private readonly IAsyncRepository _repository; 13 | private readonly IMapper _mapper; 14 | 15 | public Get(IAsyncRepository repository, 16 | IMapper mapper) 17 | { 18 | _repository = repository; 19 | _mapper = mapper; 20 | } 21 | 22 | /// 23 | /// Get a specific Author 24 | /// 25 | [HttpGet("api/[namespace]/{id}", Name = "[namespace]_[controller]")] 26 | public override async Task> HandleAsync(int id, CancellationToken cancellationToken) 27 | { 28 | var author = await _repository.GetByIdAsync(id, cancellationToken); 29 | 30 | if (author is null) return NotFound(); 31 | 32 | var result = _mapper.Map(author); 33 | 34 | return result; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/List.AuthorListRequest.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp.Endpoints.Authors; 2 | 3 | public class AuthorListRequest 4 | { 5 | public int Page { get; set; } 6 | public int PerPage { get; set; } 7 | } 8 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/List.AuthorListResult.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp.Endpoints.Authors; 2 | 3 | public class AuthorListResult 4 | { 5 | public int Id { get; set; } 6 | public string Name { get; set; } = null!; 7 | public string? TwitterAlias { get; set; } 8 | } 9 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/List.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.ApiEndpoints; 2 | using AutoMapper; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SampleEndpointApp.DomainModel; 5 | 6 | namespace SampleEndpointApp.Endpoints.Authors; 7 | 8 | public class List : EndpointBaseAsync 9 | .WithRequest 10 | .WithResult> 11 | { 12 | private readonly IAsyncRepository repository; 13 | private readonly IMapper mapper; 14 | 15 | public List( 16 | IAsyncRepository repository, 17 | IMapper mapper) 18 | { 19 | this.repository = repository; 20 | this.mapper = mapper; 21 | } 22 | 23 | /// 24 | /// List all Authors 25 | /// 26 | [HttpGet("api/[namespace]")] 27 | public override async Task> HandleAsync( 28 | [FromQuery] AuthorListRequest request, 29 | CancellationToken cancellationToken = default) 30 | { 31 | if (request.PerPage == 0) 32 | { 33 | request.PerPage = 10; 34 | } 35 | if (request.Page == 0) 36 | { 37 | request.Page = 1; 38 | } 39 | var result = (await repository.ListAllAsync(request.PerPage, request.Page, cancellationToken)) 40 | .Select(i => mapper.Map(i)); 41 | 42 | return result; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/ListJsonFile.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using Ardalis.ApiEndpoints; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SampleEndpointApp.DomainModel; 5 | 6 | namespace SampleEndpointApp.Endpoints.Authors; 7 | 8 | public class ListJsonFile : EndpointBaseAsync 9 | .WithoutRequest 10 | .WithActionResult 11 | { 12 | private readonly IAsyncRepository repository; 13 | 14 | public ListJsonFile(IAsyncRepository repository) 15 | { 16 | this.repository = repository; 17 | } 18 | 19 | /// 20 | /// List all Authors as a JSON file 21 | /// 22 | [HttpGet("api/[namespace]/Json")] 23 | public override async Task HandleAsync( 24 | CancellationToken cancellationToken = default) 25 | { 26 | var result = (await repository.ListAllAsync(cancellationToken)).ToList(); 27 | 28 | var streamData = JsonSerializer.SerializeToUtf8Bytes(result); 29 | return File(streamData, "text/json", "authors.json"); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Stream.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Ardalis.ApiEndpoints; 3 | using AutoMapper; 4 | using Microsoft.AspNetCore.Mvc; 5 | using SampleEndpointApp.DomainModel; 6 | 7 | namespace SampleEndpointApp.Endpoints.Authors; 8 | 9 | public class Stream : EndpointBaseAsync 10 | .WithoutRequest 11 | .WithAsyncEnumerableResult 12 | { 13 | private readonly IAsyncRepository repository; 14 | private readonly IMapper mapper; 15 | 16 | public Stream( 17 | IAsyncRepository repository, 18 | IMapper mapper) 19 | { 20 | this.repository = repository; 21 | this.mapper = mapper; 22 | } 23 | 24 | /// 25 | /// Stream all authors with a one second delay between entries 26 | /// 27 | [HttpGet("api/[namespace]/stream")] 28 | public override async IAsyncEnumerable HandleAsync([EnumeratorCancellation] CancellationToken cancellationToken) 29 | { 30 | var result = await repository.ListAllAsync(cancellationToken); 31 | foreach (var author in result) 32 | { 33 | yield return mapper.Map(author); 34 | await Task.Delay(1000, cancellationToken); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Update.UpdateAuthorCommand.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace SampleEndpointApp.Endpoints.Authors; 4 | 5 | public class UpdateAuthorCommand 6 | { 7 | [Required] 8 | public int Id { get; set; } 9 | [Required] 10 | public string Name { get; set; } = null!; 11 | } 12 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Update.UpdatedAuthorResult.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp.Endpoints.Authors; 2 | 3 | public class UpdatedAuthorResult 4 | { 5 | public string Id { get; set; } = null!; 6 | public string Name { get; set; } = null!; 7 | public string PluralsightUrl { get; set; } = null!; 8 | public string? TwitterAlias { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/Update.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.ApiEndpoints; 2 | using AutoMapper; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SampleEndpointApp.DomainModel; 5 | 6 | namespace SampleEndpointApp.Endpoints.Authors; 7 | 8 | public class Update : EndpointBaseAsync 9 | .WithRequest 10 | .WithActionResult 11 | { 12 | private readonly IAsyncRepository _repository; 13 | private readonly IMapper _mapper; 14 | 15 | public Update(IAsyncRepository repository, 16 | IMapper mapper) 17 | { 18 | _repository = repository; 19 | _mapper = mapper; 20 | } 21 | 22 | /// 23 | /// Updates an existing Author 24 | /// 25 | [HttpPut("api/[namespace]")] 26 | public override async Task> HandleAsync([FromBody] UpdateAuthorCommand request, CancellationToken cancellationToken) 27 | { 28 | var author = await _repository.GetByIdAsync(request.Id, cancellationToken); 29 | 30 | if (author is null) return NotFound(); 31 | 32 | _mapper.Map(request, author); 33 | await _repository.UpdateAsync(author, cancellationToken); 34 | 35 | var result = _mapper.Map(author); 36 | return result; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/UpdateById.UpdateAuthorCommand.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace SampleEndpointApp.Endpoints.Authors; 5 | 6 | public class UpdateAuthorCommandById 7 | { 8 | [Required] 9 | [FromRoute] 10 | public int Id { get; set; } 11 | 12 | [FromBody] 13 | public UpdateDetails Details { get; set; } = null!; 14 | 15 | public class UpdateDetails 16 | { 17 | [Required] 18 | public string Name { get; set; } = null!; 19 | [Required] 20 | public string TwitterAlias { get; set; } = null!; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/UpdateById.UpdatedAuthorByIdResult.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp.Endpoints.Authors; 2 | 3 | public class UpdatedAuthorByIdResult 4 | { 5 | public string Id { get; set; } = null!; 6 | public string Name { get; set; } = null!; 7 | public string PluralsightUrl { get; set; } = null!; 8 | public string? TwitterAlias { get; set; } 9 | } 10 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Endpoints/Authors/UpdateById.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.ApiEndpoints; 2 | using AutoMapper; 3 | using Microsoft.AspNetCore.Mvc; 4 | using SampleEndpointApp.DomainModel; 5 | 6 | namespace SampleEndpointApp.Endpoints.Authors; 7 | 8 | // Show how to use route and body parameters 9 | // See: https://github.com/ardalis/ApiEndpoints/issues/161 10 | public class UpdateById : EndpointBaseAsync 11 | .WithRequest 12 | .WithActionResult 13 | { 14 | private readonly IAsyncRepository _repository; 15 | private readonly IMapper _mapper; 16 | 17 | public UpdateById(IAsyncRepository repository, 18 | IMapper mapper) 19 | { 20 | _repository = repository; 21 | _mapper = mapper; 22 | } 23 | 24 | /// 25 | /// Updates an existing Author 26 | /// 27 | [HttpPut("api/[namespace]/{id}")] 28 | public override async Task> HandleAsync([FromMultiSource]UpdateAuthorCommandById request, 29 | CancellationToken cancellationToken) 30 | { 31 | var author = await _repository.GetByIdAsync(request.Id, cancellationToken); 32 | 33 | if (author is null) return NotFound(); 34 | 35 | author.Name = request.Details.Name; 36 | author.TwitterAlias = request.Details.TwitterAlias; 37 | 38 | await _repository.UpdateAsync(author, cancellationToken); 39 | 40 | var result = _mapper.Map(author); 41 | return result; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/FromMultiSourceAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ModelBinding; 2 | 3 | namespace SampleEndpointApp; 4 | 5 | public sealed class FromMultiSourceAttribute : Attribute, IBindingSourceMetadata 6 | { 7 | public BindingSource BindingSource { get; } = CompositeBindingSource.Create( 8 | new[] { BindingSource.Path, BindingSource.Query }, 9 | nameof(FromMultiSourceAttribute)); 10 | } 11 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Program.cs: -------------------------------------------------------------------------------- 1 | namespace SampleEndpointApp; 2 | 3 | public class Program 4 | { 5 | public static void Main(string[] args) 6 | { 7 | CreateHostBuilder(args).Build().Run(); 8 | } 9 | 10 | public static IHostBuilder CreateHostBuilder(string[] args) => 11 | Host.CreateDefaultBuilder(args) 12 | .ConfigureWebHostDefaults(webBuilder => 13 | { 14 | webBuilder.UseStartup(); 15 | }); 16 | } 17 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:22957", 7 | "sslPort": 44338 8 | } 9 | }, 10 | "$schema": "http://json.schemastore.org/launchsettings.json", 11 | "profiles": { 12 | "IIS Express": { 13 | "commandName": "IISExpress", 14 | "launchBrowser": true, 15 | "launchUrl": "swagger", 16 | "environmentVariables": { 17 | "ASPNETCORE_ENVIRONMENT": "Development" 18 | } 19 | }, 20 | "SampleEndpointApp": { 21 | "commandName": "Project", 22 | "launchBrowser": true, 23 | "launchUrl": "swagger", 24 | "environmentVariables": { 25 | "ASPNETCORE_ENVIRONMENT": "Development" 26 | }, 27 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /sample/SampleEndpointApp/README.md: -------------------------------------------------------------------------------- 1 | # READ ME 2 | 3 | ## EF Migration Scripts 4 | 5 | All of these commands should be run from the same folder as the .csproj file. 6 | 7 | Make sure you have the global tool installed. If not, run this command: 8 | 9 | ``` 10 | dotnet tool install --global dotnet-ef 11 | ``` 12 | 13 | Once you have it, if you need to add migrations run this: 14 | 15 | ``` 16 | dotnet ef migrations add "Name" -o DataAccess/Migrations 17 | ``` 18 | 19 | Then to update the database (and reseed it) run this: 20 | 21 | ``` 22 | dotnet ef database update 23 | ``` 24 | 25 | ## Duplicate Code in Endpoints 26 | 27 | If the duplicate dependency code in the endpoints bothers you, you can avoid it easily by creating your own `AppEndpointBaseSync` classes and your own set of extensions that mirror those in `EndpointBaseSync`. Your new base class expose as properties any common dependencies like loggers, mappers, or generic repositories, and then use an IOC container's property injection feature to ensure they're always populated when the endpoint is created. 28 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/SampleEndpointApp.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | 6 | 7 | 8 | .\SampleEndpointApp.xml 9 | CS1591 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.OpenApi.Models; 4 | using SampleEndpointApp.DataAccess; 5 | using SampleEndpointApp.DomainModel; 6 | 7 | namespace SampleEndpointApp; 8 | 9 | public class Startup 10 | { 11 | public Startup(IConfiguration configuration) 12 | { 13 | Configuration = configuration; 14 | } 15 | 16 | public IConfiguration Configuration { get; } 17 | 18 | public void ConfigureServices(IServiceCollection services) 19 | { 20 | services.AddDbContext(options => 21 | options.UseSqlite("Data Source=database.sqlite")); // will be created in web project root 22 | 23 | services.AddControllers(options => 24 | { 25 | options.UseNamespaceRouteToken(); 26 | }); 27 | 28 | services.Configure(options => 29 | { 30 | options.SuppressInferBindingSourcesForParameters = true; 31 | }); 32 | 33 | services.AddSwaggerGen(c => 34 | { 35 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "SampleEndpointApp", Version = "v1" }); 36 | c.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, "SampleEndpointApp.xml")); 37 | c.UseApiEndpoints(); 38 | }); 39 | 40 | services.AddAutoMapper(typeof(Startup)); 41 | 42 | services.AddScoped(typeof(IAsyncRepository<>), typeof(EfRepository<>)); 43 | } 44 | 45 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 46 | { 47 | if (env.IsDevelopment()) 48 | { 49 | app.UseDeveloperExceptionPage(); 50 | } 51 | 52 | app.UseHttpsRedirection(); 53 | 54 | app.UseRouting(); 55 | 56 | app.UseAuthorization(); 57 | 58 | // Enable middleware to serve generated Swagger as a JSON endpoint. 59 | app.UseSwagger(); 60 | 61 | // Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.), specifying the Swagger JSON endpoint. 62 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SampleEndpointApp V1")); 63 | 64 | app.UseEndpoints(endpoints => 65 | { 66 | endpoints.MapControllers(); 67 | }); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /sample/SampleEndpointApp/database.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ardalis/ApiEndpoints/3d19ba1537e4c477ac74304a5befe0ad2a2b4980/sample/SampleEndpointApp/database.sqlite -------------------------------------------------------------------------------- /sample/httpCommands.rest: -------------------------------------------------------------------------------- 1 | # https://marketplace.visualstudio.com/items?itemName=humao.rest-client 2 | 3 | # Copy a curl request from Chrome dev tools Network tab 4 | curl 'https://localhost:5001/authors' -H 'authority: localhost:5001' -H 'accept: text/plain' -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36' -H 'sec-fetch-site: same-origin' -H 'sec-fetch-mode: cors' -H 'referer: https://localhost:5001/swagger/index.html' -H 'accept-encoding: gzip, deflate, br' -H 'accept-language: en-US,en;q=0.9' -H 'cookie: language=en; continueCode=Emar5w1qXgvdzLt5U2H9TWF2iqqho9S7zuMyU3jFlQf96cLB07VR6ZWlzo4j; io=EgQuVp9O41q_g70OAAAp' --compressed 5 | 6 | ### 7 | 8 | # Or just write your own simple query 9 | GET https://localhost:5001/authors 10 | 11 | ### 12 | 13 | GET https://localhost:5001/authors/1 14 | 15 | ### 16 | 17 | # You can also issue commands with bodies like this POST 18 | POST https://localhost:5001/authors HTTP/1.1 19 | content-type: application/json 20 | 21 | { 22 | "name": "Julie Lerman", 23 | "pluralsightUrl": "https://www.pluralsight.com/authors/julie-lerman", 24 | "twitterAlias" : "julielerman" 25 | } 26 | 27 | ### 28 | 29 | PUT https://localhost:5001/authors HTTP/1.1 30 | content-type: application/json 31 | 32 | { 33 | "id": 3, 34 | "name": "Julia Lerman" 35 | } 36 | 37 | ### 38 | 39 | DELETE https://localhost:5001/authors/4 HTTP/1.1 40 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints.CodeAnalyzers/Ardalis.ApiEndpoints.CodeAnalyzers.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | false 6 | 7 | 8 | 9 | Ardalis.ApiEndpoints.CodeAnalyzers 10 | 4.1.0 11 | netstandard2.0 12 | Steve Smith (@ardalis), Philip Pittle (@ppittle) 13 | Code Analyzers supporting using Api Endpoints 14 | Code Analyzers increasing productivity of developers using the Api Endpoints framework. 15 | true 16 | api endpoints code analyzers roslyn aspnetcore 17 | Updating version to 4.1 to match ApiEndpoints. 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints.CodeAnalyzers/EndpointHasExtraPublicMethodAnalyzer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Diagnostics; 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.Diagnostics; 5 | 6 | namespace Ardalis.ApiEndpoints.CodeAnalyzers; 7 | 8 | [DiagnosticAnalyzer(LanguageNames.CSharp)] 9 | public class EndpointHasExtraPublicMethodAnalyzer : DiagnosticAnalyzer 10 | { 11 | public const string DiagnosticId = "ApiEndpoints101"; 12 | 13 | internal static readonly LocalizableString Title = "Endpoint has more than one public method"; 14 | internal static readonly LocalizableString MessageFormat = "Endpoint {0} has additional public method {1}. Endpoints must have only one public method."; 15 | private static readonly LocalizableString Description = "MVC will interpret additional public methods on an Endpoint as Actions. Limit Endpoints to a single Action."; 16 | private const string Category = "Naming"; 17 | 18 | private static readonly string[] EndpointMethodNames = new[] { "HandleAsync", "Handle" }; 19 | 20 | private static readonly DiagnosticDescriptor Rule = new( 21 | #pragma warning disable RS2008 // Enable analyzer release tracking 22 | DiagnosticId, 23 | #pragma warning restore RS2008 // Enable analyzer release tracking 24 | Title, 25 | MessageFormat, 26 | Category, 27 | DiagnosticSeverity.Warning, 28 | isEnabledByDefault: true, 29 | description: Description); 30 | 31 | public override ImmutableArray SupportedDiagnostics => ImmutableArray.Create(Rule); 32 | 33 | public override void Initialize(AnalysisContext context) 34 | { 35 | context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.Analyze | GeneratedCodeAnalysisFlags.None); 36 | context.EnableConcurrentExecution(); 37 | context.RegisterSymbolAction(AnalyzeMethodDeclaration, SymbolKind.Method); 38 | } 39 | 40 | private void AnalyzeMethodDeclaration(SymbolAnalysisContext context) 41 | { 42 | try 43 | { 44 | if (context.Symbol is not IMethodSymbol methodSymbol || !IsApiAction(methodSymbol)) 45 | { 46 | return; 47 | } 48 | 49 | var isApiEndpoint = methodSymbol.ContainingType 50 | .GetBaseTypesAndThis() 51 | .Any(t => t.ToString() == "Ardalis.ApiEndpoints.EndpointBase"); 52 | 53 | // not a type inheriting EndpointBase 54 | if (!isApiEndpoint) 55 | { 56 | return; 57 | } 58 | 59 | // gather all public methods ordered by "most correct" name and line number 60 | var allApiActions = methodSymbol.ContainingType 61 | .GetMembers() 62 | .OfType() 63 | .Where(IsApiAction) 64 | .OrderByDescending(m => EndpointMethodNames.Contains(m.Name)) 65 | .ThenBy(m => m.Locations.First().GetLineSpan().StartLinePosition) 66 | .ToList(); 67 | 68 | // if our methodSymbol is the first method, then don't display error 69 | if (allApiActions.First().Equals(methodSymbol, SymbolEqualityComparer.Default)) return; 70 | 71 | // at this point, we have a new public method on a EndpointBase that violates the rule 72 | var diagnostic = Diagnostic.Create( 73 | Rule, 74 | context.Symbol.Locations.First(), 75 | methodSymbol.ContainingType.Name, 76 | methodSymbol.Name); 77 | 78 | context.ReportDiagnostic(diagnostic); 79 | 80 | static bool IsApiAction(IMethodSymbol m) 81 | { 82 | return !m.IsStatic 83 | && m.MethodKind == MethodKind.Ordinary 84 | && m.DeclaredAccessibility == Accessibility.Public; 85 | } 86 | } 87 | catch (Exception e) 88 | { 89 | Debug.Write(e); 90 | 91 | if (Debugger.IsAttached) throw; 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints.CodeAnalyzers/EndpointHasExtraPublicMethodCodeFixProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Immutable; 2 | using System.Composition; 3 | using System.Diagnostics; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeActions; 6 | using Microsoft.CodeAnalysis.CodeFixes; 7 | using Microsoft.CodeAnalysis.CSharp; 8 | using Microsoft.CodeAnalysis.CSharp.Syntax; 9 | 10 | namespace Ardalis.ApiEndpoints.CodeAnalyzers; 11 | 12 | [ExportCodeFixProvider(LanguageNames.CSharp, Name = nameof(EndpointHasExtraPublicMethodCodeFixProvider)), Shared] 13 | public class EndpointHasExtraPublicMethodCodeFixProvider : CodeFixProvider 14 | { 15 | private const string MakeInternalTitle = "Make additional method internal."; 16 | private const string MakePrivateTitle = "Make additional method private."; 17 | 18 | public sealed override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create(EndpointHasExtraPublicMethodAnalyzer.DiagnosticId); 19 | 20 | public sealed override FixAllProvider GetFixAllProvider() 21 | { 22 | // See https://github.com/dotnet/roslyn/blob/master/docs/analyzers/FixAllProvider.md for more information on Fix All Providers 23 | return WellKnownFixAllProviders.BatchFixer; 24 | } 25 | 26 | public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) 27 | { 28 | var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken).ConfigureAwait(false); 29 | if (root == null) 30 | { 31 | return; 32 | } 33 | 34 | var diagnostic = context.Diagnostics.First(); 35 | var diagnosticSpan = diagnostic.Location.SourceSpan; 36 | 37 | // Find the method declaration identified by the diagnostic. 38 | var declaration = root 39 | .FindToken(diagnosticSpan.Start) 40 | .Parent! 41 | .AncestorsAndSelf() 42 | .OfType() 43 | .First(); 44 | 45 | // Register a code action that will invoke the fix. 46 | context.RegisterCodeFix( 47 | CodeAction.Create( 48 | title: MakeInternalTitle, 49 | createChangedDocument: c => ChangeMethodKindAsync(context.Document, declaration, SyntaxKind.InternalKeyword, c), 50 | equivalenceKey: MakeInternalTitle), 51 | diagnostic); 52 | 53 | context.RegisterCodeFix( 54 | CodeAction.Create( 55 | title: MakePrivateTitle, 56 | createChangedDocument: c => ChangeMethodKindAsync(context.Document, declaration, SyntaxKind.PrivateKeyword, c), 57 | equivalenceKey: MakePrivateTitle), 58 | diagnostic); 59 | } 60 | 61 | private async Task ChangeMethodKindAsync( 62 | Document document, 63 | MethodDeclarationSyntax method, 64 | SyntaxKind targetKind, 65 | CancellationToken cancellationToken) 66 | { 67 | try 68 | { 69 | var modifierList = 70 | // create the new modifier list, but replace the public token 71 | // goal is to preserve order 72 | method 73 | .Modifiers 74 | .Select(x => 75 | x.Kind() is SyntaxKind.PublicKeyword 76 | ? SyntaxFactory.Token(targetKind) 77 | : x) 78 | .ToList(); 79 | 80 | // remove the public modifier 81 | var newMethod = method.WithModifiers(new SyntaxTokenList(modifierList)); 82 | 83 | var tree = await document.GetSyntaxTreeAsync(cancellationToken).ConfigureAwait(false); 84 | var root = await tree!.GetRootAsync(cancellationToken).ConfigureAwait(false); 85 | 86 | var newRoot = root.ReplaceNode(method, newMethod); 87 | 88 | return document.WithSyntaxRoot(newRoot); 89 | } 90 | catch (Exception e) 91 | { 92 | Debug.Write(e); 93 | 94 | if (Debugger.IsAttached) 95 | throw; 96 | 97 | return document; 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints.CodeAnalyzers/Extensions/RoslynExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.CodeAnalysis; 2 | 3 | internal static class RoslynExtensions 4 | { 5 | public static IEnumerable GetBaseTypesAndThis(this ITypeSymbol type) 6 | { 7 | ITypeSymbol? current = type; 8 | while (current != null) 9 | { 10 | yield return current; 11 | current = current.BaseType; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints.CodeAnalyzers/tools/install.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not install analyzers via install.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | if (Test-Path $analyzersPath) 17 | { 18 | # Install the language agnostic analyzers. 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Install language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) 55 | } 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints.CodeAnalyzers/tools/uninstall.ps1: -------------------------------------------------------------------------------- 1 | param($installPath, $toolsPath, $package, $project) 2 | 3 | if($project.Object.SupportsPackageDependencyResolution) 4 | { 5 | if($project.Object.SupportsPackageDependencyResolution()) 6 | { 7 | # Do not uninstall analyzers via uninstall.ps1, instead let the project system handle it. 8 | return 9 | } 10 | } 11 | 12 | $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers") * -Resolve 13 | 14 | foreach($analyzersPath in $analyzersPaths) 15 | { 16 | # Uninstall the language agnostic analyzers. 17 | if (Test-Path $analyzersPath) 18 | { 19 | foreach ($analyzerFilePath in Get-ChildItem -Path "$analyzersPath\*.dll" -Exclude *.resources.dll) 20 | { 21 | if($project.Object.AnalyzerReferences) 22 | { 23 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 24 | } 25 | } 26 | } 27 | } 28 | 29 | # $project.Type gives the language name like (C# or VB.NET) 30 | $languageFolder = "" 31 | if($project.Type -eq "C#") 32 | { 33 | $languageFolder = "cs" 34 | } 35 | if($project.Type -eq "VB.NET") 36 | { 37 | $languageFolder = "vb" 38 | } 39 | if($languageFolder -eq "") 40 | { 41 | return 42 | } 43 | 44 | foreach($analyzersPath in $analyzersPaths) 45 | { 46 | # Uninstall language specific analyzers. 47 | $languageAnalyzersPath = join-path $analyzersPath $languageFolder 48 | if (Test-Path $languageAnalyzersPath) 49 | { 50 | foreach ($analyzerFilePath in Get-ChildItem -Path "$languageAnalyzersPath\*.dll" -Exclude *.resources.dll) 51 | { 52 | if($project.Object.AnalyzerReferences) 53 | { 54 | try 55 | { 56 | $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) 57 | } 58 | catch 59 | { 60 | 61 | } 62 | } 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints.NSwag/Ardalis.ApiEndpoints.NSwag.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Ardalis.ApiEndpoints.NSwag 5 | 4.1.0 6 | Steve Smith (@ardalis), Maksym Koshovyi 7 | OpenAPI support for ApiEndpoints using NSwag 8 | OpenAPI support for ApiEndpoints using NSwag 9 | api endpoints openapi nswag 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints.NSwag/Extensions/NSwagSwaggerGeneratorSettingsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.ApiEndpoints; 2 | using NSwag.Generation.AspNetCore; 3 | using NSwag.Generation.Processors; 4 | using NSwag.Generation.Processors.Contexts; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection; 7 | 8 | public static class NSwagSwaggerGeneratorSettingsExtensions 9 | { 10 | /// 11 | /// Updates Swagger document to support ApiEndpoints.

12 | /// For controllers inherited from :
13 | /// - Replaces action Tag with [namespace]
14 | /// - Replaces action OperationId with [namespace]_[controller] 15 | ///
16 | public static void UseApiEndpoints(this AspNetCoreOpenApiDocumentGeneratorSettings settings) 17 | { 18 | settings.OperationProcessors.Add(new EndpointsOperationProcessor()); 19 | } 20 | 21 | private class EndpointsOperationProcessor : IOperationProcessor 22 | { 23 | public bool Process(OperationProcessorContext context) 24 | { 25 | if (context.ControllerType.GetBaseTypesAndThis().Any(t => t == typeof(EndpointBase))) 26 | { 27 | var namespaceValue = context.ControllerType.Namespace?.Split('.').Last(); 28 | 29 | context.OperationDescription.Operation.Tags = new List { namespaceValue }; 30 | context.OperationDescription.Operation.OperationId = $"{namespaceValue}_{context.ControllerType.Name}"; 31 | } 32 | 33 | return true; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints.Swashbuckle/Ardalis.ApiEndpoints.Swashbuckle.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Ardalis.ApiEndpoints.Swashbuckle 5 | 4.1.0 6 | Steve Smith (@ardalis), Maksym Koshovyi 7 | OpenAPI support for ApiEndpoints using Swashbuckle 8 | OpenAPI support for ApiEndpoints using Swashbuckle 9 | api endpoints openapi swashbuckle 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints.Swashbuckle/Extensions/SwaggerGenOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Ardalis.ApiEndpoints; 2 | using Microsoft.AspNetCore.Mvc.ApiExplorer; 3 | using Microsoft.AspNetCore.Mvc.Controllers; 4 | using Swashbuckle.AspNetCore.SwaggerGen; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection; 7 | 8 | public static class SwaggerGenOptionsExtensions 9 | { 10 | /// 11 | /// Updates Swagger document to support ApiEndpoints.

12 | /// For controllers inherited from :
13 | /// - Replaces action Tag with [namespace]
14 | ///
15 | public static void UseApiEndpoints(this SwaggerGenOptions options) 16 | { 17 | options.TagActionsBy(EndpointNamespaceOrDefault); 18 | } 19 | 20 | private static IList EndpointNamespaceOrDefault(ApiDescription api) 21 | { 22 | if (api.ActionDescriptor is not ControllerActionDescriptor actionDescriptor) 23 | { 24 | throw new InvalidOperationException($"Unable to determine tag for endpoint: {api.ActionDescriptor.DisplayName}"); 25 | } 26 | 27 | if (actionDescriptor.ControllerTypeInfo.GetBaseTypesAndThis().Any(t => t == typeof(EndpointBase))) 28 | { 29 | return new[] { actionDescriptor.ControllerTypeInfo.Namespace?.Split('.').Last() }; 30 | } 31 | 32 | return new[] { actionDescriptor.ControllerName }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints/Ardalis.ApiEndpoints.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | 6 | 7 | 8 | Ardalis.ApiEndpoints 9 | Ardalis.ApiEndpoints 10 | Steve Smith (@ardalis) 11 | An alternative to Controllers for ASP.NET Core API Endpoints. 12 | Controllers promote creating bloated classes that lack cohesion. This project provides a simpler alternative that follows SOLID principles. An alternative to Controllers for ASP.NET Core API Endpoints. 13 | aspnet asp.net aspnetcore asp.net core api web api rest endpoint controller 14 | Add IAsyncEnumerable Support; other minor fixes 15 | 4.1.0 16 | Ardalis.ApiEndpoints 17 | LICENSE.txt 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints/EndpointBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Ardalis.ApiEndpoints; 4 | 5 | /// 6 | /// A base class for an API controller with single action (endpoint). 7 | /// 8 | [ApiController] 9 | public abstract class EndpointBase : ControllerBase 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints/Extensions/MvcOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text.RegularExpressions; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 4 | 5 | namespace Microsoft.Extensions.DependencyInjection; 6 | 7 | public static class MvcOptionsExtensions 8 | { 9 | /// 10 | /// Allows to use "[namespace]" as part of a route. 11 | /// 12 | public static MvcOptions UseNamespaceRouteToken(this MvcOptions options) 13 | { 14 | options.Conventions.Add(new CustomRouteToken( 15 | "namespace", 16 | c => c.ControllerType.Namespace?.Split('.').Last() 17 | )); 18 | 19 | return options; 20 | } 21 | 22 | private class CustomRouteToken : IApplicationModelConvention 23 | { 24 | private readonly string _tokenRegex; 25 | private readonly Func _valueGenerator; 26 | 27 | public CustomRouteToken(string tokenName, Func valueGenerator) 28 | { 29 | _tokenRegex = $@"(\[{tokenName}])(? a.Selectors), tokenValue); 40 | } 41 | } 42 | 43 | private void UpdateSelectors(IEnumerable selectors, string? tokenValue) 44 | { 45 | foreach (var selector in selectors.Where(s => s.AttributeRouteModel != null)) 46 | { 47 | selector.AttributeRouteModel.Template = InsertTokenValue(selector.AttributeRouteModel.Template, tokenValue); 48 | selector.AttributeRouteModel.Name = InsertTokenValue(selector.AttributeRouteModel.Name, tokenValue); 49 | } 50 | } 51 | 52 | private string? InsertTokenValue(string? template, string? tokenValue) 53 | { 54 | if (template is null) 55 | { 56 | return template; 57 | } 58 | 59 | return Regex.Replace(template, _tokenRegex, tokenValue); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace System; 2 | 3 | internal static class TypeExtensions 4 | { 5 | public static IEnumerable GetBaseTypesAndThis(this Type type) 6 | { 7 | Type? current = type; 8 | while (current != null) 9 | { 10 | yield return current; 11 | current = current.BaseType; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints/FluentGenerics/EndpointBaseAsync.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Ardalis.ApiEndpoints; 4 | 5 | /// 6 | /// A base class for an endpoint that accepts parameters. 7 | /// 8 | public static class EndpointBaseAsync 9 | { 10 | public static class WithRequest 11 | { 12 | public abstract class WithResult : EndpointBase 13 | { 14 | public abstract Task HandleAsync( 15 | TRequest request, 16 | CancellationToken cancellationToken = default 17 | ); 18 | } 19 | 20 | public abstract class WithoutResult : EndpointBase 21 | { 22 | public abstract Task HandleAsync( 23 | TRequest request, 24 | CancellationToken cancellationToken = default 25 | ); 26 | } 27 | 28 | public abstract class WithActionResult : EndpointBase 29 | { 30 | public abstract Task> HandleAsync( 31 | TRequest request, 32 | CancellationToken cancellationToken = default 33 | ); 34 | } 35 | 36 | public abstract class WithActionResult : EndpointBase 37 | { 38 | public abstract Task HandleAsync( 39 | TRequest request, 40 | CancellationToken cancellationToken = default 41 | ); 42 | } 43 | public abstract class WithAsyncEnumerableResult : EndpointBase 44 | { 45 | public abstract IAsyncEnumerable HandleAsync( 46 | TRequest request, 47 | CancellationToken cancellationToken = default 48 | ); 49 | } 50 | } 51 | 52 | public static class WithoutRequest 53 | { 54 | public abstract class WithResult : EndpointBase 55 | { 56 | public abstract Task HandleAsync( 57 | CancellationToken cancellationToken = default 58 | ); 59 | } 60 | 61 | public abstract class WithoutResult : EndpointBase 62 | { 63 | public abstract Task HandleAsync( 64 | CancellationToken cancellationToken = default 65 | ); 66 | } 67 | 68 | public abstract class WithActionResult : EndpointBase 69 | { 70 | public abstract Task> HandleAsync( 71 | CancellationToken cancellationToken = default 72 | ); 73 | } 74 | 75 | public abstract class WithActionResult : EndpointBase 76 | { 77 | public abstract Task HandleAsync( 78 | CancellationToken cancellationToken = default 79 | ); 80 | } 81 | 82 | public abstract class WithAsyncEnumerableResult : EndpointBase 83 | { 84 | public abstract IAsyncEnumerable HandleAsync( 85 | CancellationToken cancellationToken = default 86 | ); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints/FluentGenerics/EndpointBaseSync.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Ardalis.ApiEndpoints; 4 | 5 | /// 6 | /// A base class for an endpoint that accepts parameters. 7 | /// 8 | public static class EndpointBaseSync 9 | { 10 | public static class WithRequest 11 | { 12 | public abstract class WithResult : EndpointBase 13 | { 14 | public abstract TResponse Handle(TRequest request); 15 | } 16 | 17 | public abstract class WithoutResult : EndpointBase 18 | { 19 | public abstract void Handle(TRequest request); 20 | } 21 | 22 | public abstract class WithActionResult : EndpointBase 23 | { 24 | public abstract ActionResult Handle(TRequest request); 25 | } 26 | 27 | public abstract class WithActionResult : EndpointBase 28 | { 29 | public abstract ActionResult Handle(TRequest request); 30 | } 31 | } 32 | 33 | public static class WithoutRequest 34 | { 35 | public abstract class WithResult : EndpointBase 36 | { 37 | public abstract TResponse Handle(); 38 | } 39 | 40 | public abstract class WithoutResult : EndpointBase 41 | { 42 | public abstract void Handle(); 43 | } 44 | 45 | public abstract class WithActionResult : EndpointBase 46 | { 47 | public abstract ActionResult Handle(); 48 | } 49 | 50 | public abstract class WithActionResult : EndpointBase 51 | { 52 | public abstract ActionResult Handle(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints/LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Steve Smith 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 | -------------------------------------------------------------------------------- /src/Ardalis.ApiEndpoints/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("Ardalis.ApiEndpoints.CodeAnalyzers.Test")] 4 | [assembly: InternalsVisibleTo("Ardalis.ApiEndpoints.Swashbuckle")] 5 | [assembly: InternalsVisibleTo("Ardalis.ApiEndpoints.NSwag")] 6 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | Ardalis.com 8 | https://github.com/ardalis/ApiEndpoints 9 | https://github.com/ardalis/ApiEndpoints 10 | git 11 | https://user-images.githubusercontent.com/782127/33497760-facf6550-d69c-11e7-94e4-b3856da259a9.png 12 | 13 | 14 | 15 | true 16 | true 17 | true 18 | snupkg 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/Ardalis.ApiEndpoints.CodeAnalyzers.Test/Ardalis.ApiEndpoints.CodeAnalyzers.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | disable 6 | 7 | true 8 | true 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /tests/Ardalis.ApiEndpoints.CodeAnalyzers.Test/Helpers/CodeFixVerifier.Helper.cs: -------------------------------------------------------------------------------- 1 | // https://raw.githubusercontent.com/dotnet/samples/master/csharp/roslyn-sdk/Tutorials/MakeConst/MakeConst.Test/Helpers/CodeFixVerifier.Helper.cs 2 | 3 | using Microsoft.CodeAnalysis; 4 | using Microsoft.CodeAnalysis.CodeActions; 5 | using Microsoft.CodeAnalysis.Formatting; 6 | using Microsoft.CodeAnalysis.Simplification; 7 | 8 | namespace Ardalis.ApiEndpoints.CodeAnalyzers.Test.Verifiers; 9 | 10 | /// 11 | /// Diagnostic Producer class with extra methods dealing with applying codefixes 12 | /// All methods are static 13 | /// 14 | public abstract partial class CodeFixVerifier : DiagnosticVerifier 15 | { 16 | /// 17 | /// Apply the inputted CodeAction to the inputted document. 18 | /// Meant to be used to apply codefixes. 19 | /// 20 | /// The Document to apply the fix on 21 | /// A CodeAction that will be applied to the Document. 22 | /// A Document with the changes from the CodeAction 23 | private static Document ApplyFix(Document document, CodeAction codeAction) 24 | { 25 | var operations = codeAction.GetOperationsAsync(CancellationToken.None).Result; 26 | var solution = operations.OfType().Single().ChangedSolution; 27 | return solution.GetDocument(document.Id); 28 | } 29 | 30 | /// 31 | /// Compare two collections of Diagnostics,and return a list of any new diagnostics that appear only in the second collection. 32 | /// Note: Considers Diagnostics to be the same if they have the same Ids. In the case of multiple diagnostics with the same Id in a row, 33 | /// this method may not necessarily return the new one. 34 | /// 35 | /// The Diagnostics that existed in the code before the CodeFix was applied 36 | /// The Diagnostics that exist in the code after the CodeFix was applied 37 | /// A list of Diagnostics that only surfaced in the code after the CodeFix was applied 38 | private static IEnumerable GetNewDiagnostics(IEnumerable diagnostics, IEnumerable newDiagnostics) 39 | { 40 | var oldArray = diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 41 | var newArray = newDiagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 42 | 43 | int oldIndex = 0; 44 | int newIndex = 0; 45 | 46 | while (newIndex < newArray.Length) 47 | { 48 | if (oldIndex < oldArray.Length && oldArray[oldIndex].Id == newArray[newIndex].Id) 49 | { 50 | ++oldIndex; 51 | ++newIndex; 52 | } 53 | else 54 | { 55 | yield return newArray[newIndex++]; 56 | } 57 | } 58 | } 59 | 60 | /// 61 | /// Get the existing compiler diagnostics on the inputted document. 62 | /// 63 | /// The Document to run the compiler diagnostic analyzers on 64 | /// The compiler diagnostics that were found in the code 65 | private static IEnumerable GetCompilerDiagnostics(Document document) 66 | { 67 | return document.GetSemanticModelAsync().Result.GetDiagnostics(); 68 | } 69 | 70 | /// 71 | /// Given a document, turn it into a string based on the syntax root 72 | /// 73 | /// The Document to be converted to a string 74 | /// A string containing the syntax of the Document after formatting 75 | private static string GetStringFromDocument(Document document) 76 | { 77 | var simplifiedDoc = Simplifier.ReduceAsync(document, Simplifier.Annotation).Result; 78 | var root = simplifiedDoc.GetSyntaxRootAsync().Result; 79 | root = Formatter.Format(root, Formatter.Annotation, simplifiedDoc.Project.Solution.Workspace); 80 | return root.GetText().ToString(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /tests/Ardalis.ApiEndpoints.CodeAnalyzers.Test/Helpers/DiagnosticResult.cs: -------------------------------------------------------------------------------- 1 | // https://raw.githubusercontent.com/dotnet/samples/master/csharp/roslyn-sdk/Tutorials/MakeConst/MakeConst.Test/Helpers/DiagnosticResult.cs 2 | 3 | using Microsoft.CodeAnalysis; 4 | 5 | namespace Ardalis.ApiEndpoints.CodeAnalyzers.Test.Helpers; 6 | 7 | /// 8 | /// Location where the diagnostic appears, as determined by path, line number, and column number. 9 | /// 10 | public struct DiagnosticResultLocation 11 | { 12 | public DiagnosticResultLocation(string path, int line, int column) 13 | { 14 | if (line < -1) 15 | { 16 | throw new ArgumentOutOfRangeException(nameof(line), "line must be >= -1"); 17 | } 18 | 19 | if (column < -1) 20 | { 21 | throw new ArgumentOutOfRangeException(nameof(column), "column must be >= -1"); 22 | } 23 | 24 | this.Path = path; 25 | this.Line = line; 26 | this.Column = column; 27 | } 28 | 29 | public string Path { get; } 30 | public int Line { get; } 31 | public int Column { get; } 32 | } 33 | 34 | /// 35 | /// Struct that stores information about a Diagnostic appearing in a source 36 | /// 37 | public struct DiagnosticResult 38 | { 39 | private DiagnosticResultLocation[] locations; 40 | 41 | public DiagnosticResultLocation[] Locations 42 | { 43 | get 44 | { 45 | this.locations ??= Array.Empty(); 46 | return this.locations; 47 | } 48 | set 49 | { 50 | this.locations = value; 51 | } 52 | } 53 | 54 | public DiagnosticSeverity Severity { get; set; } 55 | 56 | public string Id { get; set; } 57 | 58 | public string Message { get; set; } 59 | 60 | public string Path 61 | { 62 | get 63 | { 64 | return this.Locations.Length > 0 ? this.Locations[0].Path : ""; 65 | } 66 | } 67 | 68 | public int Line 69 | { 70 | get 71 | { 72 | return this.Locations.Length > 0 ? this.Locations[0].Line : -1; 73 | } 74 | } 75 | 76 | public int Column 77 | { 78 | get 79 | { 80 | return this.Locations.Length > 0 ? this.Locations[0].Column : -1; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /tests/Ardalis.ApiEndpoints.CodeAnalyzers.Test/Helpers/DiagnosticVerifier.Helper.cs: -------------------------------------------------------------------------------- 1 | // https://raw.githubusercontent.com/dotnet/samples/master/csharp/roslyn-sdk/Tutorials/MakeConst/MakeConst.Test/Helpers/DiagnosticVerifier.Helper.cs 2 | 3 | using System.Collections.Immutable; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CSharp; 6 | using Microsoft.CodeAnalysis.Diagnostics; 7 | using Microsoft.CodeAnalysis.Text; 8 | 9 | namespace Ardalis.ApiEndpoints.CodeAnalyzers.Test.Verifiers; 10 | 11 | /// 12 | /// Class for turning strings into documents and getting the diagnostics on them 13 | /// All methods are static 14 | /// 15 | public abstract partial class DiagnosticVerifier 16 | { 17 | private static readonly MetadataReference CorlibReference = MetadataReference.CreateFromFile(typeof(object).Assembly.Location); 18 | private static readonly MetadataReference SystemCoreReference = MetadataReference.CreateFromFile(typeof(Enumerable).Assembly.Location); 19 | private static readonly MetadataReference CSharpSymbolsReference = MetadataReference.CreateFromFile(typeof(CSharpCompilation).Assembly.Location); 20 | private static readonly MetadataReference CodeAnalysisReference = MetadataReference.CreateFromFile(typeof(Compilation).Assembly.Location); 21 | 22 | internal static string DefaultFilePathPrefix = "Test"; 23 | internal static string CSharpDefaultFileExt = "cs"; 24 | internal static string VisualBasicDefaultExt = "vb"; 25 | internal static string TestProjectName = "TestProject"; 26 | 27 | #region Get Diagnostics 28 | 29 | /// 30 | /// Given classes in the form of strings, their language, and an IDiagnosticAnalyzer to apply to it, return the diagnostics found in the string after converting it to a document. 31 | /// 32 | /// Classes in the form of strings 33 | /// The language the source classes are in 34 | /// The analyzer to be run on the sources 35 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 36 | private static Diagnostic[] GetSortedDiagnostics(string[] sources, string language, DiagnosticAnalyzer analyzer) 37 | { 38 | return GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(sources, language)); 39 | } 40 | 41 | /// 42 | /// Given an analyzer and a document to apply it to, run the analyzer and gather an array of diagnostics found in it. 43 | /// The returned diagnostics are then ordered by location in the source document. 44 | /// 45 | /// The analyzer to run on the documents 46 | /// The Documents that the analyzer will be run on 47 | /// An IEnumerable of Diagnostics that surfaced in the source code, sorted by Location 48 | protected static Diagnostic[] GetSortedDiagnosticsFromDocuments(DiagnosticAnalyzer analyzer, Document[] documents) 49 | { 50 | var projects = new HashSet(); 51 | foreach (var document in documents) 52 | { 53 | projects.Add(document.Project); 54 | } 55 | 56 | var diagnostics = new List(); 57 | foreach (var project in projects) 58 | { 59 | var compilationWithAnalyzers = project.GetCompilationAsync().Result.WithAnalyzers(ImmutableArray.Create(analyzer)); 60 | var diags = compilationWithAnalyzers.GetAnalyzerDiagnosticsAsync().Result; 61 | foreach (var diag in diags) 62 | { 63 | if (diag.Location == Location.None || diag.Location.IsInMetadata) 64 | { 65 | diagnostics.Add(diag); 66 | } 67 | else 68 | { 69 | for (int i = 0; i < documents.Length; i++) 70 | { 71 | var document = documents[i]; 72 | var tree = document.GetSyntaxTreeAsync().Result; 73 | if (tree == diag.Location.SourceTree) 74 | { 75 | diagnostics.Add(diag); 76 | } 77 | } 78 | } 79 | } 80 | } 81 | 82 | var results = SortDiagnostics(diagnostics); 83 | diagnostics.Clear(); 84 | return results; 85 | } 86 | 87 | /// 88 | /// Sort diagnostics by location in source document 89 | /// 90 | /// The list of Diagnostics to be sorted 91 | /// An IEnumerable containing the Diagnostics in order of Location 92 | private static Diagnostic[] SortDiagnostics(IEnumerable diagnostics) 93 | { 94 | return diagnostics.OrderBy(d => d.Location.SourceSpan.Start).ToArray(); 95 | } 96 | 97 | #endregion 98 | 99 | #region Set up compilation and documents 100 | /// 101 | /// Given an array of strings as sources and a language, turn them into a project and return the documents and spans of it. 102 | /// 103 | /// Classes in the form of strings 104 | /// The language the source code is in 105 | /// A Tuple containing the Documents produced from the sources and their TextSpans if relevant 106 | private static Document[] GetDocuments(string[] sources, string language) 107 | { 108 | if (language != LanguageNames.CSharp && language != LanguageNames.VisualBasic) 109 | { 110 | throw new ArgumentException("Unsupported Language"); 111 | } 112 | 113 | var project = CreateProject(sources, language); 114 | var documents = project.Documents.ToArray(); 115 | 116 | if (sources.Length != documents.Length) 117 | { 118 | throw new InvalidOperationException("Amount of sources did not match amount of Documents created"); 119 | } 120 | 121 | return documents; 122 | } 123 | 124 | /// 125 | /// Create a Document from a string through creating a project that contains it. 126 | /// 127 | /// Classes in the form of a string 128 | /// The language the source code is in 129 | /// A Document created from the source string 130 | protected static Document CreateDocument(string source, string language = LanguageNames.CSharp) 131 | { 132 | return CreateProject(new[] { source }, language).Documents.First(); 133 | } 134 | 135 | /// 136 | /// Create a project using the inputted strings as sources. 137 | /// 138 | /// Classes in the form of strings 139 | /// The language the source code is in 140 | /// A Project created out of the Documents created from the source strings 141 | private static Project CreateProject(string[] sources, string language = LanguageNames.CSharp) 142 | { 143 | string fileNamePrefix = DefaultFilePathPrefix; 144 | string fileExt = language == LanguageNames.CSharp ? CSharpDefaultFileExt : VisualBasicDefaultExt; 145 | 146 | var projectId = ProjectId.CreateNewId(debugName: TestProjectName); 147 | 148 | var solution = new AdhocWorkspace() 149 | .CurrentSolution 150 | .AddProject(projectId, TestProjectName, TestProjectName, language) 151 | .AddMetadataReference(projectId, CorlibReference) 152 | .AddMetadataReference(projectId, SystemCoreReference) 153 | .AddMetadataReference(projectId, CSharpSymbolsReference) 154 | .AddMetadataReference(projectId, CodeAnalysisReference); 155 | 156 | int count = 0; 157 | foreach (var source in sources) 158 | { 159 | var newFileName = fileNamePrefix + count + "." + fileExt; 160 | var documentId = DocumentId.CreateNewId(projectId, debugName: newFileName); 161 | solution = solution.AddDocument(documentId, newFileName, SourceText.From(source)); 162 | count++; 163 | } 164 | return solution.GetProject(projectId); 165 | } 166 | #endregion 167 | } 168 | -------------------------------------------------------------------------------- /tests/Ardalis.ApiEndpoints.CodeAnalyzers.Test/Verifiers/CodeFixVerifier.cs: -------------------------------------------------------------------------------- 1 | // https://raw.githubusercontent.com/dotnet/samples/master/csharp/roslyn-sdk/Tutorials/MakeConst/MakeConst.Test/Verifiers/CodeFixVerifier.cs 2 | 3 | using System.Text.RegularExpressions; 4 | using Microsoft.CodeAnalysis; 5 | using Microsoft.CodeAnalysis.CodeActions; 6 | using Microsoft.CodeAnalysis.CodeFixes; 7 | using Microsoft.CodeAnalysis.Diagnostics; 8 | using Microsoft.CodeAnalysis.Formatting; 9 | using Microsoft.VisualStudio.TestTools.UnitTesting; 10 | 11 | namespace Ardalis.ApiEndpoints.CodeAnalyzers.Test.Verifiers; 12 | 13 | /// 14 | /// Superclass of all Unit tests made for diagnostics with codefixes. 15 | /// Contains methods used to verify correctness of codefixes 16 | /// 17 | public abstract partial class CodeFixVerifier : DiagnosticVerifier 18 | { 19 | /// 20 | /// Returns the codefix being tested (C#) - to be implemented in non-abstract class 21 | /// 22 | /// The CodeFixProvider to be used for CSharp code 23 | protected virtual CodeFixProvider GetCSharpCodeFixProvider() 24 | { 25 | return null; 26 | } 27 | 28 | /// 29 | /// Returns the codefix being tested (VB) - to be implemented in non-abstract class 30 | /// 31 | /// The CodeFixProvider to be used for VisualBasic code 32 | protected virtual CodeFixProvider GetBasicCodeFixProvider() 33 | { 34 | return null; 35 | } 36 | 37 | /// 38 | /// Called to test a C# codefix when applied on the inputted string as a source 39 | /// 40 | /// A class in the form of a string before the CodeFix was applied to it 41 | /// A class in the form of a string after the CodeFix was applied to it 42 | /// Index determining which codefix to apply if there are multiple 43 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 44 | protected void VerifyCSharpFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) 45 | { 46 | VerifyFix(LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), GetCSharpCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); 47 | } 48 | 49 | /// 50 | /// Called to test a VB codefix when applied on the inputted string as a source 51 | /// 52 | /// A class in the form of a string before the CodeFix was applied to it 53 | /// A class in the form of a string after the CodeFix was applied to it 54 | /// Index determining which codefix to apply if there are multiple 55 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 56 | protected void VerifyBasicFix(string oldSource, string newSource, int? codeFixIndex = null, bool allowNewCompilerDiagnostics = false) 57 | { 58 | VerifyFix(LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), GetBasicCodeFixProvider(), oldSource, newSource, codeFixIndex, allowNewCompilerDiagnostics); 59 | } 60 | 61 | /// 62 | /// General verifier for codefixes. 63 | /// Creates a Document from the source string, then gets diagnostics on it and applies the relevant codefixes. 64 | /// Then gets the string after the codefix is applied and compares it with the expected result. 65 | /// Note: If any codefix causes new diagnostics to show up, the test fails unless allowNewCompilerDiagnostics is set to true. 66 | /// 67 | /// The language the source code is in 68 | /// The analyzer to be applied to the source code 69 | /// The codefix to be applied to the code wherever the relevant Diagnostic is found 70 | /// A class in the form of a string before the CodeFix was applied to it 71 | /// A class in the form of a string after the CodeFix was applied to it 72 | /// Index determining which codefix to apply if there are multiple 73 | /// A bool controlling whether or not the test will fail if the CodeFix introduces other warnings after being applied 74 | private void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider codeFixProvider, string oldSource, string newSource, int? codeFixIndex, bool allowNewCompilerDiagnostics) 75 | { 76 | var document = CreateDocument(oldSource, language); 77 | var analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); 78 | var compilerDiagnostics = GetCompilerDiagnostics(document); 79 | var attempts = analyzerDiagnostics.Length; 80 | 81 | for (int i = 0; i < attempts; ++i) 82 | { 83 | var actions = new List(); 84 | var context = new CodeFixContext(document, analyzerDiagnostics[0], (a, d) => actions.Add(a), CancellationToken.None); 85 | codeFixProvider.RegisterCodeFixesAsync(context).Wait(); 86 | 87 | if (!actions.Any()) 88 | { 89 | break; 90 | } 91 | 92 | if (codeFixIndex != null) 93 | { 94 | document = ApplyFix(document, actions.ElementAt((int)codeFixIndex)); 95 | break; 96 | } 97 | 98 | document = ApplyFix(document, actions.ElementAt(0)); 99 | analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); 100 | 101 | var newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); 102 | 103 | //check if applying the code fix introduced any new compiler diagnostics 104 | if (!allowNewCompilerDiagnostics && newCompilerDiagnostics.Any()) 105 | { 106 | // Format and get the compiler diagnostics again so that the locations make sense in the output 107 | document = document.WithSyntaxRoot(Formatter.Format(document.GetSyntaxRootAsync().Result, Formatter.Annotation, document.Project.Solution.Workspace)); 108 | newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); 109 | 110 | Assert.IsTrue(false, 111 | string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n", 112 | string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())), 113 | document.GetSyntaxRootAsync().Result.ToFullString())); 114 | } 115 | 116 | //check if there are analyzer diagnostics left after the code fix 117 | if (!analyzerDiagnostics.Any()) 118 | { 119 | break; 120 | } 121 | } 122 | 123 | //after applying all of the code fixes, compare the resulting string to the inputted one 124 | var actual = GetStringFromDocument(document); 125 | Assert.AreEqual( 126 | RemoveMostWhiteSpace(newSource), 127 | RemoveMostWhiteSpace(actual)); 128 | } 129 | 130 | private string RemoveMostWhiteSpace(string code) 131 | { 132 | var options = RegexOptions.None; 133 | var regex = new Regex("[ ]{2,}", options); 134 | return regex.Replace(code, " "); 135 | } 136 | } 137 | --------------------------------------------------------------------------------