├── .editorconfig
├── .github
├── FUNDING.yml
└── workflows
│ └── build-test-publish.yml
├── .gitignore
├── BenchmarkTests
├── BenchmarkTests.csproj
├── CacheCommandTests.cs
├── ConnectionParametersTests.cs
├── FormatStringTests.cs
├── HttpClientTests.cs
├── ParseStringsTests.cs
└── Program.cs
├── LICENSE
├── NpgsqlRest.sln
├── NpgsqlRest
├── Auth
│ ├── AuthHandler.cs
│ └── ClaimsDictionary.cs
├── Consts.cs
├── Defaults
│ ├── DefaultCommentParser.cs
│ ├── DefaultEndpoint.cs
│ ├── DefaultNameConverter.cs
│ └── DefaultUrlBuilder.cs
├── Enums.cs
├── Extensions.cs
├── Formatter.cs
├── Interfaces.cs
├── Log.cs
├── NpgsqlRest.csproj
├── NpgsqlRestBatch.cs
├── NpgsqlRestCommand.cs
├── NpgsqlRestLogger.cs
├── NpgsqlRestMetadata.cs
├── NpgsqlRestMiddleware.cs
├── NpgsqlRestMiddlewareExtensions.cs
├── NpgsqlRestOptions.cs
├── NpgsqlRestParameter.cs
├── ParameterParser.cs
├── ParameterValidationValues.cs
├── Parser.cs
├── PasswordHasher.cs
├── PgConverters.cs
├── Routine.cs
├── RoutineCache.cs
├── RoutineEndpoint.cs
├── RoutineSource.cs
├── RoutineSourceParameterFormatter.cs
├── RoutineSourceQuery.cs
├── TypeDescriptor.cs
└── UploadHandlers
│ ├── CsvUploadHandler.cs
│ ├── DefaultUploadHandler.cs
│ ├── FileSystemUploadHandler.cs
│ ├── IUploadHandler.cs
│ ├── LargeObjectUploadHandler.cs
│ ├── UploadExtensions.cs
│ ├── UploadHandlerOptions.cs
│ └── UploadLog.cs
├── NpgsqlRestClient
├── App.cs
├── AppStaticFileMiddleware.cs
├── Arguments.cs
├── Builder.cs
├── Config.cs
├── DbDataProtection.cs
├── DbLogging.cs
├── DefaultParser.cs
├── ExternalAuth.cs
├── NpgsqlRestClient.csproj
├── Program.cs
├── TokenRefreshAuth.cs
└── appsettings.json
├── NpgsqlRestTests
├── ArrayTests
│ ├── ArrayParametersTests.cs
│ └── ArrayTypeTests.cs
├── AuthTests
│ ├── AuthLoginTests.cs
│ ├── AuthPasswordLoginTests.cs
│ ├── AuthorizedTests.cs
│ ├── HashedParameterTests.cs
│ ├── PasswordHasherTests.cs
│ ├── UserContextTests.cs
│ └── UserParamsTests.cs
├── BodyTests
│ ├── BodyParameterNameTests.cs
│ └── BodyParamsTests.cs
├── CommandCallbackTests.cs
├── CommentTests
│ ├── CommentAuthAttrTests.cs
│ ├── CommentHttpAttrTests.cs
│ ├── CommentParamTypeAttrTests.cs
│ └── CommentResponseHeadersTests.cs
├── ConnectionNameTests
│ ├── ConnectionNameTests.cs
│ └── ConnectionNameUsingParamsTests.cs
├── CrudTests
│ ├── CrudDeleteTests.cs
│ ├── CrudInsertOnConflictDoNothingReturningTests.cs
│ ├── CrudInsertOnConflictDoNothingTests.cs
│ ├── CrudInsertOnConflictDoUpdateReturningTests.cs
│ ├── CrudInsertOnConflictDoUpdateTests.cs
│ ├── CrudInsertReturningTests.cs
│ ├── CrudInsertTests.cs
│ ├── CrudSelectTests.cs
│ ├── CrudTableTagCommentTests.cs
│ ├── CrudTableTests.cs
│ ├── CrudUpdateReturningTests.cs
│ └── CrudUpdateTests.cs
├── CustomSourceTests.cs
├── CustomTableTypeParametersTests.cs
├── CustomTypeParametersTests.cs
├── EndingSlashTests.cs
├── ErrorHandlingTests.cs
├── IdentNamesTests.cs
├── MixinTests.cs
├── NonPublicSchemaTests.cs
├── NotDotnetCompliantTypeParams.cs
├── NpgsqlRestTests.csproj
├── NullHandlingTests.cs
├── ParamTests
│ ├── DefaultParametersTests.cs
│ ├── MultiParamsQueryStringTests1.cs
│ ├── MultiParamsQueryStringTests2.cs
│ ├── MultiParamsTests1.cs
│ ├── MultiParamsTests2.cs
│ ├── NoneExistingParamTests.cs
│ ├── OverloadQueryStringTests.cs
│ ├── OverloadTests.cs
│ ├── ReturnIntTests.cs
│ ├── UnnamedParamsTests.cs
│ └── VariadicParamTests.cs
├── ParserTests
│ ├── DatabaseSerializerTests.cs
│ ├── DefaultParserTests.cs
│ ├── PatternMatcherTests.cs
│ └── TimeSpanParserTests.cs
├── QuotedJsonTests.cs
├── RawContentTests
│ ├── RawDownloadTests.cs
│ ├── RawResponseTests.cs
│ └── RawResponseUsingParamsTests.cs
├── RequestHeadersTests.cs
├── ReturnTests
│ ├── ReturnBooleanTests.cs
│ ├── ReturnJsonTests.cs
│ ├── ReturnLongTableTests.cs
│ ├── ReturnSetOfBoolTests.cs
│ ├── ReturnSetOfIntTests.cs
│ ├── ReturnSetOfJsonTests.cs
│ ├── ReturnSetOfTextTests.cs
│ ├── ReturnTableTests.cs
│ ├── ReturnTableTypeTests.cs
│ ├── ReturnTextTests.cs
│ └── ReturnTypeTests.cs
├── SetofRecordTests.cs
├── SetofTableTests.cs
├── Setup
│ ├── Database.cs
│ ├── Program.cs
│ ├── TestFixture.cs
│ └── Usings.cs
├── SingleRecordTests.cs
├── StoredProcedureTests.cs
├── StrictFunctionsTests.cs
├── TimestampTests.cs
├── UploadTests
│ ├── CsvExampleTests.cs
│ ├── CsvUploadTests.cs
│ ├── FileStatusCheckerTests.cs
│ ├── FileSystemUploadTests.cs
│ ├── LargeObjectUploadTests.cs
│ └── MimeTypeFilterTests.cs
├── VoidUnitTests.cs
└── VolatilityVerbTests.cs
├── PerfomanceTests
├── k6
│ └── script.js
└── readme.md
├── README.md
├── annotations.md
├── changelog-client-old.md
├── changelog-old.md
├── changelog.md
├── client.md
├── docker
├── Dockerfile
└── readme.md
├── login-endpoints.md
├── npm
├── config-copy.js
├── package.json
├── postinstall.js
├── readme.md
└── uninstall.js
├── options.md
└── plugins
├── NpgsqlRest.CrudSource
├── CrudSource.cs
├── CrudSourceQuery.cs
├── NpgsqlRest.CrudSource.csproj
├── ParameterFormatters.cs
└── README.md
├── NpgsqlRest.HttpFiles
├── Enums.cs
├── HttpFile.cs
├── HttpFileOptions.cs
├── NpgsqlRest.HttpFiles.csproj
└── README.md
└── NpgsqlRest.TsClient
├── NpgsqlRest.TsClient.csproj
├── README.md
├── TsClient.cs
└── TsClientOptions.cs
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.cs]
2 |
3 | # Default severity for analyzer diagnostics with category 'Naming'
4 | dotnet_analyzer_diagnostic.category-Naming.severity = silent
5 | csharp_using_directive_placement = outside_namespace:silent
6 | csharp_prefer_simple_using_statement = true:suggestion
7 | csharp_prefer_braces = true:silent
8 | csharp_style_namespace_declarations = block_scoped:silent
9 | csharp_style_prefer_method_group_conversion = true:silent
10 | csharp_style_prefer_top_level_statements = true:silent
11 | csharp_style_prefer_primary_constructors = true:suggestion
12 | csharp_style_expression_bodied_methods = false:silent
13 | csharp_style_expression_bodied_constructors = false:silent
14 | csharp_style_expression_bodied_operators = false:silent
15 | csharp_style_expression_bodied_properties = true:silent
16 | csharp_style_expression_bodied_indexers = true:silent
17 | csharp_style_expression_bodied_accessors = true:silent
18 | csharp_style_expression_bodied_lambdas = true:silent
19 | csharp_style_expression_bodied_local_functions = false:silent
20 | csharp_indent_labels = one_less_than_current
21 | csharp_space_around_binary_operators = before_and_after
22 |
23 | [*.{cs,vb}]
24 | #### Naming styles ####
25 |
26 | # Naming rules
27 |
28 | dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
29 | dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
30 | dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
31 |
32 | dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
33 | dotnet_naming_rule.types_should_be_pascal_case.symbols = types
34 | dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
35 |
36 | dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
37 | dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
38 | dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
39 |
40 | # Symbol specifications
41 |
42 | dotnet_naming_symbols.interface.applicable_kinds = interface
43 | dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
44 | dotnet_naming_symbols.interface.required_modifiers =
45 |
46 | dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
47 | dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
48 | dotnet_naming_symbols.types.required_modifiers =
49 |
50 | dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
51 | dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
52 | dotnet_naming_symbols.non_field_members.required_modifiers =
53 |
54 | # Naming styles
55 |
56 | dotnet_naming_style.begins_with_i.required_prefix = I
57 | dotnet_naming_style.begins_with_i.required_suffix =
58 | dotnet_naming_style.begins_with_i.word_separator =
59 | dotnet_naming_style.begins_with_i.capitalization = pascal_case
60 |
61 | dotnet_naming_style.pascal_case.required_prefix =
62 | dotnet_naming_style.pascal_case.required_suffix =
63 | dotnet_naming_style.pascal_case.word_separator =
64 | dotnet_naming_style.pascal_case.capitalization = pascal_case
65 |
66 | dotnet_naming_style.pascal_case.required_prefix =
67 | dotnet_naming_style.pascal_case.required_suffix =
68 | dotnet_naming_style.pascal_case.word_separator =
69 | dotnet_naming_style.pascal_case.capitalization = pascal_case
70 | dotnet_style_coalesce_expression = true:suggestion
71 | dotnet_style_null_propagation = true:suggestion
72 | dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion
73 | dotnet_style_prefer_auto_properties = true:silent
74 | dotnet_style_object_initializer = true:suggestion
75 | dotnet_style_prefer_collection_expression = true:suggestion
76 | dotnet_style_collection_initializer = true:suggestion
77 | dotnet_style_prefer_simplified_boolean_expressions = true:suggestion
78 | dotnet_style_prefer_conditional_expression_over_assignment = true:silent
79 | dotnet_style_prefer_conditional_expression_over_return = true:silent
80 | dotnet_style_explicit_tuple_names = true:suggestion
81 | dotnet_style_operator_placement_when_wrapping = beginning_of_line
82 | tab_width = 4
83 | indent_size = 4
84 | end_of_line = crlf
85 | dotnet_style_prefer_inferred_tuple_names = true:suggestion
86 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | patreon: vbconsulting
4 | buy_me_a_coffee: vbilopavu
5 |
--------------------------------------------------------------------------------
/.github/workflows/build-test-publish.yml:
--------------------------------------------------------------------------------
1 | name: Build, Test, Publish and Release
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 | workflow_dispatch:
9 |
10 | jobs:
11 | build-test-publish:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Install PosgtreSQL
15 | uses: vb-consulting/postgresql-action@v1
16 | with:
17 | postgresql version: '17'
18 | postgresql user: 'postgres'
19 | postgresql password: 'postgres'
20 | - uses: actions/checkout@v2
21 | - name: Setup .NET9
22 | uses: actions/setup-dotnet@v1
23 | with:
24 | dotnet-version: 9.0.x
25 | - name: Install dependencies
26 | run: dotnet restore
27 | - name: Build
28 | run: dotnet build --configuration Release --no-restore
29 | - name: Test
30 | run: dotnet test --no-restore --verbosity m
31 | - name: Publish
32 | run: dotnet nuget push **\*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{secrets.NUGET_API_KEY}} --skip-duplicate
33 |
34 | create-release:
35 | needs: build-test-publish
36 | runs-on: ubuntu-latest
37 | outputs:
38 | upload_url: ${{ steps.create_release.outputs.upload_url }}
39 | steps:
40 | - name: Create Release
41 | id: create_release
42 | uses: actions/create-release@v1
43 | env:
44 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
45 | with:
46 | tag_name: v2.27.0-client-v2.22.0
47 | release_name: "AOT Client v2.22.0 NpgsqlRest v2.27.0"
48 | draft: true
49 | prerelease: true
50 |
51 | upload-assets:
52 | needs: create-release
53 | runs-on: ubuntu-latest
54 | steps:
55 | - name: Checkout code
56 | uses: actions/checkout@v2
57 |
58 | - name: Upload appsettings.json to release
59 | uses: actions/upload-release-asset@v1
60 | env:
61 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62 | with:
63 | upload_url: ${{ needs.create-release.outputs.upload_url }}
64 | asset_path: ./NpgsqlRestClient/appsettings.json
65 | asset_name: appsettings.json
66 | asset_content_type: application/json
67 |
68 | build-windows:
69 | needs: create-release
70 | runs-on: windows-latest
71 | steps:
72 | - uses: actions/checkout@v2
73 | - name: Setup .NET9
74 | uses: actions/setup-dotnet@v1
75 | with:
76 | dotnet-version: 9.0.x
77 | - name: Build Windows AOT
78 | run: dotnet publish ./NpgsqlRestClient/NpgsqlRestClient.csproj -r win-x64 -c Release
79 | - name: Upload Windows executable
80 | uses: actions/upload-release-asset@v1
81 | env:
82 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
83 | with:
84 | upload_url: ${{ needs.create-release.outputs.upload_url }}
85 | asset_path: ./NpgsqlRestClient/bin/Release/net9.0/win-x64/publish/NpgsqlRestClient.exe
86 | asset_name: npgsqlrest-win64.exe
87 | asset_content_type: application/octet-stream
88 |
89 | build-linux:
90 | needs: create-release
91 | runs-on: ubuntu-22.04
92 | steps:
93 | - uses: actions/checkout@v2
94 | - name: Setup .NET9
95 | uses: actions/setup-dotnet@v1
96 | with:
97 | dotnet-version: 9.0.x
98 | - name: Build Linux AOT
99 | run: dotnet publish ./NpgsqlRestClient/NpgsqlRestClient.csproj -r linux-x64 -c Release
100 | - name: Upload Linux executable
101 | uses: actions/upload-release-asset@v1
102 | env:
103 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
104 | with:
105 | upload_url: ${{ needs.create-release.outputs.upload_url }}
106 | asset_path: ./NpgsqlRestClient/bin/Release/net9.0/linux-x64/publish/NpgsqlRestClient
107 | asset_name: npgsqlrest-linux64
108 | asset_content_type: application/octet-stream
109 |
110 | build-macos:
111 | needs: create-release
112 | runs-on: macos-latest
113 | steps:
114 | - uses: actions/checkout@v2
115 | - name: Setup .NET9
116 | uses: actions/setup-dotnet@v1
117 | with:
118 | dotnet-version: 9.0.x
119 | - name: Build macOS AOT
120 | run: dotnet publish ./NpgsqlRestClient/NpgsqlRestClient.csproj -r osx-x64 -c Release
121 | - name: Upload macOS executable
122 | uses: actions/upload-release-asset@v1
123 | env:
124 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
125 | with:
126 | upload_url: ${{ needs.create-release.outputs.upload_url }}
127 | asset_path: ./NpgsqlRestClient/bin/Release/net9.0/osx-x64/publish/NpgsqlRestClient
128 | asset_name: npgsqlrest-osx64
129 | asset_content_type: application/octet-stream
--------------------------------------------------------------------------------
/BenchmarkTests/BenchmarkTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Exe
5 | net9.0
6 | enable
7 | enable
8 | True
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/BenchmarkTests/CacheCommandTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using BenchmarkDotNet.Attributes;
3 | using Npgsql;
4 | using NpgsqlRest;
5 |
6 | namespace BenchmarkTests;
7 |
8 | public class NpgsqlCachedCommand : NpgsqlCommand
9 | {
10 | private static readonly NpgsqlCachedCommand _instanceCache = new();
11 |
12 | #pragma warning disable CS8603 // Possible null reference return.
13 | private NpgsqlCommand CachedCommandClone() => MemberwiseClone() as NpgsqlCommand;
14 | #pragma warning restore CS8603 // Possible null reference return.
15 |
16 | public static NpgsqlCommand Create(NpgsqlConnection connection)
17 | {
18 | var result = _instanceCache.CachedCommandClone();
19 | result.Connection = connection;
20 | return result;
21 | }
22 | }
23 |
24 | public class NpgsqlCachedParameter : NpgsqlParameter
25 | {
26 | private static readonly NpgsqlCachedParameter _textParam = new()
27 | {
28 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text,
29 | Value = DBNull.Value,
30 | };
31 |
32 | #pragma warning disable CS8603 // Possible null reference return.
33 | public NpgsqlParameter CachedParameterMemberwiseClone() => MemberwiseClone() as NpgsqlParameter;
34 | #pragma warning restore CS8603 // Possible null reference return.
35 |
36 | public static NpgsqlParameter CreateTextParam(string? value)
37 | {
38 | var result = _textParam.CachedParameterMemberwiseClone();
39 | if (value is not null)
40 | {
41 | result.Value = value;
42 | }
43 | return result;
44 | }
45 | }
46 |
47 | [MemoryDiagnoser]
48 | public class CacheCommandTests
49 | {
50 | private readonly string _connectionStr = "Host=127.0.0.1;Port=5432;Database=postgres;Username=postgres;Password=postgres";
51 |
52 |
53 | [Benchmark(Baseline = true)]
54 | public async Task NormalCommand()
55 | {
56 | using var connection = new NpgsqlConnection(_connectionStr);
57 | connection.Open();
58 | using var cmd = connection.CreateCommand();
59 | cmd.CommandText = "select $1,$2,$3";
60 | cmd.Parameters.Add(new NpgsqlParameter
61 | {
62 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text,
63 | Value = "test1"
64 | });
65 | cmd.Parameters.Add(new NpgsqlParameter
66 | {
67 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text,
68 | Value = "test2"
69 | });
70 | cmd.Parameters.Add(new NpgsqlParameter
71 | {
72 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text,
73 | Value = "test3"
74 | });
75 | await using var reader = await cmd.ExecuteReaderAsync();
76 | }
77 |
78 | [Benchmark]
79 | public async Task CachedCommand()
80 | {
81 | using var connection = new NpgsqlConnection(_connectionStr);
82 | connection.Open();
83 | using var cmd = NpgsqlCachedCommand.Create(connection);
84 | cmd.CommandText = "select $1,$2,$3";
85 | cmd.Parameters.Add(new NpgsqlParameter
86 | {
87 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text,
88 | Value = "test1"
89 | });
90 | cmd.Parameters.Add(new NpgsqlParameter
91 | {
92 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text,
93 | Value = "test2"
94 | });
95 | cmd.Parameters.Add(new NpgsqlParameter
96 | {
97 | NpgsqlDbType = NpgsqlTypes.NpgsqlDbType.Text,
98 | Value = "test3"
99 | });
100 | await using var reader = await cmd.ExecuteReaderAsync();
101 | }
102 |
103 | [Benchmark]
104 | public async Task CachedCommandAndParemeters()
105 | {
106 | using var connection = new NpgsqlConnection(_connectionStr);
107 | connection.Open();
108 | using var cmd = NpgsqlCachedCommand.Create(connection);
109 | cmd.CommandText = "select $1,$2,$3";
110 | cmd.Parameters.Add(NpgsqlCachedParameter.CreateTextParam("test1"));
111 | cmd.Parameters.Add(NpgsqlCachedParameter.CreateTextParam("test2"));
112 | cmd.Parameters.Add(NpgsqlCachedParameter.CreateTextParam("test3"));
113 | await using var reader = await cmd.ExecuteReaderAsync();
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/BenchmarkTests/FormatStringTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using System.Text.RegularExpressions;
4 | using BenchmarkDotNet.Attributes;
5 | using NpgsqlRest;
6 |
7 | namespace BenchmarkTests;
8 |
9 | public class FormatStringTests
10 | {
11 | private readonly Dictionary replacements = new()
12 | {
13 | { "name", "John" },
14 | { "place", "NpgsqlRest" }
15 | };
16 |
17 | [Benchmark]
18 | public ReadOnlySpan FormatStringMethod()
19 | {
20 | ReadOnlySpan input = "Hello, {name}! Welcome to {place}.";
21 | return Formatter.FormatString(input, replacements);
22 | }
23 |
24 | [Benchmark]
25 | public string RegexMethod()
26 | {
27 | ReadOnlySpan input = "Hello, {name}! Welcome to {place}.";
28 | string pattern = @"\{(\w+)\}";
29 | return Regex.Replace(input.ToString(), pattern, match =>
30 | {
31 | string key = match.Groups[1].Value;
32 | return replacements.TryGetValue(key, out var value) ? value : match.Value;
33 | });
34 | }
35 |
36 | [Benchmark]
37 | public string RegexCodeGenMethod()
38 | {
39 | ReadOnlySpan input = "Hello, {name}! Welcome to {place}.";
40 | return ReplaceRegex.Value().Replace(input.ToString(), match =>
41 | {
42 | string key = match.Groups[1].Value;
43 | return replacements.TryGetValue(key, out var value) ? value : match.Value;
44 | });
45 | }
46 | }
47 |
48 | public static partial class ReplaceRegex
49 | {
50 | [GeneratedRegex(@"\{(\w+)\}", RegexOptions.Compiled)]
51 | public static partial Regex Value();
52 | }
--------------------------------------------------------------------------------
/BenchmarkTests/HttpClientTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text;
3 | using BenchmarkDotNet.Attributes;
4 |
5 | namespace BenchmarkTests;
6 |
7 | public class HttpClientTests
8 | {
9 | private HttpClient _client = null!;
10 |
11 | [GlobalSetup]
12 | public void Setup()
13 | {
14 | _client = new();
15 | }
16 |
17 | [GlobalCleanup]
18 | public void Cleanup()
19 | {
20 | _client.Dispose();
21 | }
22 |
23 | [Benchmark]
24 | public async Task CallPerfTests()
25 | {
26 | string json = """
27 | {
28 | "records": 25,
29 | "textParam": "XYZ",
30 | "intParam": 3,
31 | "tsParam": "2024-04-04T03:03:03.00",
32 | "boolParam": false
33 | }
34 | """;
35 | using var content = new StringContent(json, Encoding.UTF8, "application/json");
36 | using var result = await _client.PostAsync("http://localhost:5000/api/perf-test/", content);
37 | var response = await result.Content.ReadAsStringAsync();
38 | }
39 | }
--------------------------------------------------------------------------------
/BenchmarkTests/ParseStringsTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Text.RegularExpressions;
3 | using BenchmarkDotNet.Attributes;
4 | using NpgsqlRest;
5 |
6 | namespace BenchmarkTests;
7 |
8 | public class ParseStringsTests
9 | {
10 | readonly string[] names = ["test.txt", "x", "verylongfilename.txt", "abcde"];
11 | readonly string[] patterns = ["*.txt", "*?*", "*long*.txt", "a?c*"];
12 |
13 | [Benchmark]
14 | public void IsPatternMatchMethod()
15 | {
16 | foreach (var name in names)
17 | foreach (var pattern in patterns)
18 | {
19 | Parser.IsPatternMatch(name, pattern);
20 | }
21 | }
22 |
23 | //[Benchmark]
24 | //public void LikePatternIsMethod()
25 | //{
26 | // foreach (var name in names)
27 | // foreach (var pattern in patterns)
28 | // {
29 | // DefaultResponseParser.LikePatternIsMatch(name, pattern);
30 | // }
31 | //}
32 |
33 | //[Benchmark]
34 | //public void LikePatternIsMatchFastMethod()
35 | //{
36 | // foreach (var name in names)
37 | // foreach (var pattern in patterns)
38 | // {
39 | // DefaultResponseParser.LikePatternIsMatchFast(name, pattern);
40 | // }
41 | //}
42 | }
43 |
--------------------------------------------------------------------------------
/BenchmarkTests/Program.cs:
--------------------------------------------------------------------------------
1 | using BenchmarkDotNet.Configs;
2 | using BenchmarkDotNet.Jobs;
3 | using BenchmarkDotNet.Loggers;
4 | using BenchmarkDotNet.Running;
5 | using BenchmarkDotNet.Toolchains.InProcess.Emit;
6 | using BenchmarkTests;
7 | using Perfolizer.Horology;
8 |
9 | BenchmarkRunner.Run();
10 |
11 | //BenchmarkRunner
12 | // .Run(
13 | // DefaultConfig.Instance.AddJob(
14 | // Job.Default.WithToolchain(new InProcessEmitToolchain(timeout: TimeSpan.FromSeconds(9), logOutput: false))
15 | // .WithLaunchCount(1)
16 | // .WithWarmupCount(5)
17 | // .WithIterationCount(100)
18 | // .WithIterationTime(TimeInterval.FromMilliseconds(80)))
19 | // .AddLogger(new ConsoleLogger(unicodeSupport: true, ConsoleLogger.CreateGrayScheme()))
20 | // .WithOptions(ConfigOptions.DisableLogFile));
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023, 2024, 2025, 2026, 2027, 2028 VB-Consulting
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 |
--------------------------------------------------------------------------------
/NpgsqlRest/Consts.cs:
--------------------------------------------------------------------------------
1 | namespace NpgsqlRest;
2 |
3 | public static class Consts
4 | {
5 | public const string True = "true";
6 | public const string False = "false";
7 | public const string Null = "null";
8 | public const char DoubleQuote = '"';
9 | public const string DoubleQuoteColon = "\":";
10 | public const char OpenParenthesis = '(';
11 | public const char CloseParenthesis = ')';
12 | public const char Comma = ',';
13 | public const char Dot = '.';
14 | public const char Backslash = '\\';
15 | public const char Colon = ':';
16 | public const char Equal = '=';
17 | public const char OpenBrace = '{';
18 | public const char CloseBrace = '}';
19 | public const char OpenBracket = '[';
20 | public const char CloseBracket = ']';
21 | public const char Space = ' ';
22 | public const string DoubleColon = "::";
23 | public const string FirstParam = "$1";
24 | public const string FirstNamedParam = "=>$1";
25 | public const string CloseParenthesisStr = ")";
26 | public const char Dollar = '$';
27 | public const string NamedParam = "=>$";
28 | public const string OpenRow = "=>row(";
29 | public const string CloseRow = ")::";
30 | public const char At = '@';
31 | public const char Multiply = '*';
32 | public const char Question = '?';
33 | public const string EmptyArray = "[]";
34 | public const string EmptyObj = "{}";
35 | public const string SetContext = "select set_config($1,$2,false)";
36 | }
37 |
--------------------------------------------------------------------------------
/NpgsqlRest/Defaults/DefaultEndpoint.cs:
--------------------------------------------------------------------------------
1 | namespace NpgsqlRest.Defaults;
2 |
3 | internal static class DefaultEndpoint
4 | {
5 | internal static RoutineEndpoint? Create(
6 | Routine routine,
7 | NpgsqlRestOptions options,
8 | ILogger? logger)
9 | {
10 | var url = options.UrlPathBuilder(routine, options);
11 | if (routine.FormatUrlPattern is not null)
12 | {
13 | url = string.Format(routine.FormatUrlPattern, url);
14 | }
15 |
16 | var method = routine.CrudType switch
17 | {
18 | CrudType.Select => Method.GET,
19 | CrudType.Update => Method.POST,
20 | CrudType.Insert => Method.PUT,
21 | CrudType.Delete => Method.DELETE,
22 | _ => Method.POST
23 | };
24 | var requestParamType = method == Method.GET || method == Method.DELETE ?
25 | RequestParamType.QueryString :
26 | RequestParamType.BodyJson;
27 |
28 | RoutineEndpoint routineEndpoint = new(
29 | routine,
30 | url: url,
31 | method: method,
32 | requestParamType: requestParamType,
33 | requiresAuthorization: options.RequiresAuthorization,
34 | commandTimeout: options.CommandTimeout,
35 | responseContentType: null,
36 | responseHeaders: [],
37 | requestHeadersMode: options.RequestHeadersMode,
38 | requestHeadersParameterName: options.RequestHeadersParameterName,
39 | bodyParameterName: null,
40 | textResponseNullHandling: options.TextResponseNullHandling,
41 | queryStringNullHandling: options.QueryStringNullHandling,
42 | userContext: options.AuthenticationOptions.UseUserContext,
43 | userParameters: options.AuthenticationOptions.UseUserParameters);
44 |
45 | if (options.LogCommands && logger != null)
46 | {
47 | routineEndpoint.LogCallback = LoggerMessage.Define(LogLevel.Information,
48 | new EventId(5, nameof(routineEndpoint.LogCallback)),
49 | "{parameters}{command}",
50 | NpgsqlRestLogger.LogDefineOptions);
51 | }
52 | else
53 | {
54 | routineEndpoint.LogCallback = null;
55 | }
56 |
57 | if (routine.EndpointHandler is not null)
58 | {
59 | var parsed = DefaultCommentParser.Parse(
60 | routine,
61 | routineEndpoint,
62 | options,
63 | logger);
64 |
65 | return routine.EndpointHandler(parsed);
66 | }
67 |
68 | return DefaultCommentParser.Parse(
69 | routine,
70 | routineEndpoint,
71 | options,
72 | logger);
73 | }
74 | }
--------------------------------------------------------------------------------
/NpgsqlRest/Defaults/DefaultNameConverter.cs:
--------------------------------------------------------------------------------
1 | namespace NpgsqlRest.Defaults;
2 |
3 | public static class DefaultNameConverter
4 | {
5 | private static readonly string[] separator = ["_"];
6 |
7 | public static string? ConvertToCamelCase(string? value)
8 | {
9 | if (value is null)
10 | {
11 | return string.Empty;
12 | }
13 | if (value.Length == 0)
14 | {
15 | return string.Empty;
16 | }
17 | return value
18 | .Split(separator, StringSplitOptions.RemoveEmptyEntries)
19 | .Select((s, i) =>
20 | string.Concat(i == 0 ? char.ToLowerInvariant(s[0]) : char.ToUpperInvariant(s[0]), s[1..]))
21 | .Aggregate(string.Empty, string.Concat)
22 | .Trim(Consts.DoubleQuote);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/NpgsqlRest/Defaults/DefaultUrlBuilder.cs:
--------------------------------------------------------------------------------
1 | namespace NpgsqlRest.Defaults;
2 |
3 | public static class DefaultUrlBuilder
4 | {
5 | public static string CreateUrl(Routine routine, NpgsqlRestOptions options)
6 | {
7 | var schema = routine.Schema.ToLowerInvariant()
8 | .Replace("_", "-")
9 | .Replace(" ", "-")
10 | .Replace("\"", "")
11 | .Trim('/');
12 |
13 | schema = string.IsNullOrEmpty(schema) || string.Equals(schema, "public", StringComparison.OrdinalIgnoreCase) ? "" : string.Concat(schema, "/");
14 | var name = routine.Name.ToLowerInvariant()
15 | .Replace("_", "-")
16 | .Replace(" ", "-")
17 | .Replace("\"", "")
18 | .Trim('/');
19 |
20 | var prefix = options.UrlPathPrefix is null ? "/" :
21 | string.Concat("/", options.UrlPathPrefix
22 | .ToLowerInvariant()
23 | .Replace("_", "-")
24 | .Replace(" ", "-")
25 | .Replace("\"", "")
26 | .Trim('/'),
27 | "/");
28 |
29 | return string.Concat(prefix, schema, name).TrimEnd('/').Trim(Consts.DoubleQuote);
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/NpgsqlRest/Enums.cs:
--------------------------------------------------------------------------------
1 | namespace NpgsqlRest;
2 |
3 | public enum RoutineType { Table, View, Function, Procedure, Other }
4 | public enum CrudType { Select, Insert, Update, Delete }
5 | public enum Method { GET, PUT, POST, DELETE, HEAD, OPTIONS, TRACE, PATCH, CONNECT }
6 | public enum RequestParamType { QueryString, BodyJson }
7 | public enum ParamType { QueryString, BodyJson, BodyParam, HeaderParam }
8 | public enum CommentHeader { None, Simple, Full }
9 | public enum TextResponseNullHandling { EmptyString, NullLiteral, NoContent }
10 | public enum QueryStringNullHandling { EmptyString, NullLiteral, Ignore }
11 |
12 | public enum ServiceProviderObject
13 | {
14 | ///
15 | /// Connection is not provided in service provider. Connection is supplied either by ConnectionString or by DataSource option.
16 | ///
17 | None,
18 | ///
19 | /// NpgsqlRest attempts to get NpgsqlDataSource from service provider (assuming one is provided).
20 | ///
21 | NpgsqlDataSource,
22 | ///
23 | /// NpgsqlRest attempts to get NpgsqlConnection from service provider (assuming one is provided).
24 | ///
25 | NpgsqlConnection
26 | }
27 |
28 | public enum CommentsMode
29 | {
30 | ///
31 | /// Routine comments are ignored.
32 | ///
33 | Ignore,
34 | ///
35 | /// Creates all endpoints and parses comments for to configure endpoint meta data.
36 | ///
37 | ParseAll,
38 | ///
39 | /// Creates only endpoints from routines containing a comment with HTTP tag and and configures endpoint meta data.
40 | ///
41 | OnlyWithHttpTag
42 | }
43 |
44 | public enum RequestHeadersMode
45 | {
46 | ///
47 | /// Ignore request headers, don't send them to PostgreSQL (default).
48 | ///
49 | Ignore,
50 | ///
51 | /// Send all request headers as json object to PostgreSQL by executing set_config('context.headers', headers, false) before routine call.
52 | ///
53 | Context,
54 | ///
55 | /// Send all request headers as json object to PostgreSQL as default routine parameter with name set by RequestHeadersParameterName option.
56 | /// This parameter has to have the default value (null) in the routine and have to be text or json type.
57 | ///
58 | Parameter
59 | }
60 |
61 | public enum PostgresConnectionNoticeLoggingMode
62 | {
63 | ///
64 | /// Log only connection messages.
65 | ///
66 | MessageOnly,
67 | ///
68 | /// Log last stack trace and message.
69 | ///
70 | FirstStackFrameAndMessage,
71 | ///
72 | /// Log full stack trace and message.
73 | ///
74 | FullStackAndMessage
75 | }
--------------------------------------------------------------------------------
/NpgsqlRest/Formatter.cs:
--------------------------------------------------------------------------------
1 | namespace NpgsqlRest;
2 |
3 | public static class Formatter
4 | {
5 | public static ReadOnlySpan FormatString(ReadOnlySpan input, Dictionary replacements)
6 | {
7 | if (replacements is null || replacements.Count == 0)
8 | {
9 | return input;
10 | }
11 |
12 | int inputLength = input.Length;
13 |
14 | if (inputLength == 0)
15 | {
16 | return input;
17 | }
18 |
19 | return FormatString(input, replacements.GetAlternateLookup>());
20 | }
21 |
22 | public static ReadOnlySpan FormatString(ReadOnlySpan input, Dictionary.AlternateLookup> lookup)
23 | {
24 | int inputLength = input.Length;
25 |
26 | if (inputLength == 0)
27 | {
28 | return input;
29 | }
30 |
31 | int resultLength = 0;
32 | bool inside = false;
33 | int startIndex = 0;
34 | for (int i = 0; i < inputLength; i++)
35 | {
36 | var ch = input[i];
37 | if (ch == Consts.OpenBrace)
38 | {
39 | if (inside is true)
40 | {
41 | resultLength += input[startIndex..i].Length;
42 | }
43 | inside = true;
44 | startIndex = i;
45 | continue;
46 | }
47 |
48 | if (ch == Consts.CloseBrace && inside is true)
49 | {
50 | inside = false;
51 | if (lookup.TryGetValue(input[(startIndex + 1)..i], out var value))
52 | {
53 | resultLength += value.Length;
54 | }
55 | else
56 | {
57 | resultLength += input[startIndex..i].Length + 1;
58 | }
59 | continue;
60 | }
61 |
62 | if (inside is false)
63 | {
64 | resultLength += 1;
65 | }
66 | }
67 | if (inside is true)
68 | {
69 | resultLength += input[startIndex..].Length;
70 | }
71 |
72 | char[] resultArray = new char[resultLength];
73 | Span result = resultArray;
74 | int resultPos = 0;
75 |
76 | inside = false;
77 | startIndex = 0;
78 | for (int i = 0; i < inputLength; i++)
79 | {
80 | var ch = input[i];
81 | if (ch == Consts.OpenBrace)
82 | {
83 | if (inside is true)
84 | {
85 | input[startIndex..i].CopyTo(result[resultPos..]);
86 | resultPos += input[startIndex..i].Length;
87 | }
88 | inside = true;
89 | startIndex = i;
90 | continue;
91 | }
92 |
93 | if (ch == Consts.CloseBrace && inside is true)
94 | {
95 | inside = false;
96 | if (lookup.TryGetValue(input[(startIndex + 1)..i], out var value))
97 | {
98 | value.AsSpan().CopyTo(result[resultPos..]);
99 | resultPos += value.Length;
100 | }
101 | else
102 | {
103 | input[startIndex..i].CopyTo(result[resultPos..]);
104 | resultPos += input[startIndex..i].Length;
105 | result[resultPos] = ch;
106 | resultPos++;
107 | }
108 | continue;
109 | }
110 |
111 | if (inside is false)
112 | {
113 | result[resultPos] = ch;
114 | resultPos++;
115 | }
116 | }
117 | if (inside is true)
118 | {
119 | input[startIndex..].CopyTo(result[resultPos..]);
120 | }
121 |
122 | return resultArray;
123 | }
124 | }
--------------------------------------------------------------------------------
/NpgsqlRest/NpgsqlRest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Library
5 | net9.0
6 | 13.0
7 | enable
8 | enable
9 | true
10 | $(NoWarn);1591
11 | true
12 | NpgsqlRest
13 | VB-Consulting
14 | VB-Consulting
15 | VB-Consulting
16 | Automatic REST API for Any Postgres Database as .NET9 Middleware
17 | api;api-rest;restful-api;http;postgres;dotnet;database;rest;server;postgresql;npgsqlrest;pgsql;pg;automatic
18 | https://github.com/vb-consulting/NpgsqlRest
19 | https://github.com/vb-consulting/NpgsqlRest
20 | https://github.com/vb-consulting/NpgsqlRest/blob/master/changelog.md
21 | NpgsqlRest
22 | LICENSE
23 | true
24 | true
25 | snupkg
26 | true
27 | true
28 | true
29 | true
30 | README.MD
31 | bin\$(Configuration)\$(AssemblyName).xml
32 | 2.27.0
33 | 2.27.0
34 | 2.27.0
35 | 2.27.0
36 |
37 |
38 |
39 | true
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 | True
51 |
52 |
53 |
54 |
55 |
56 |
57 | True
58 |
59 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/NpgsqlRest/NpgsqlRestBatch.cs:
--------------------------------------------------------------------------------
1 | using Npgsql;
2 |
3 | namespace NpgsqlRest;
4 |
5 | public class NpgsqlRestBatch : NpgsqlBatch
6 | {
7 | private NpgsqlBatch NpgsqlBatchClone()
8 | {
9 | #pragma warning disable CS8603 // Possible null reference return.
10 | return MemberwiseClone() as NpgsqlBatch;
11 | #pragma warning restore CS8603 // Possible null reference return.
12 | }
13 |
14 | private static readonly NpgsqlRestBatch _instanceCache = new();
15 |
16 | public static NpgsqlBatch Create(NpgsqlConnection connection)
17 | {
18 | var result = _instanceCache.NpgsqlBatchClone();
19 | result.Connection = connection;
20 | return result;
21 | }
22 | }
--------------------------------------------------------------------------------
/NpgsqlRest/NpgsqlRestCommand.cs:
--------------------------------------------------------------------------------
1 | using Npgsql;
2 |
3 | namespace NpgsqlRest;
4 |
5 | public class NpgsqlRestCommand : NpgsqlCommand
6 | {
7 | private NpgsqlCommand NpgsqlCommandClone()
8 | {
9 | #pragma warning disable CS8603 // Possible null reference return.
10 | return MemberwiseClone() as NpgsqlCommand;
11 | #pragma warning restore CS8603 // Possible null reference return.
12 | }
13 |
14 | private static readonly NpgsqlRestCommand _instanceCache = new();
15 |
16 | public static NpgsqlCommand Create(NpgsqlConnection connection)
17 | {
18 | var result = _instanceCache.NpgsqlCommandClone();
19 | result.Connection = connection;
20 | return result;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/NpgsqlRest/NpgsqlRestMiddlewareExtensions.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Logging;
2 | using System.Net;
3 | using static System.Net.Mime.MediaTypeNames;
4 |
5 | namespace NpgsqlRest;
6 |
7 | public static class NpgsqlRestMiddlewareExtensions
8 | {
9 | public static IApplicationBuilder UseNpgsqlRest(this IApplicationBuilder builder, NpgsqlRestOptions options)
10 | {
11 | if (options.ConnectionString is null && options.DataSource is null && options.ServiceProviderMode == ServiceProviderObject.None)
12 | {
13 | throw new ArgumentException("ConnectionString and DataSource are null and ServiceProviderMode is set to None. You must specify connection with connection string, DataSource object or with ServiceProvider");
14 | }
15 |
16 | if (options.ConnectionString is not null && options.DataSource is not null && options.ServiceProviderMode == ServiceProviderObject.None)
17 | {
18 | throw new ArgumentException("Both ConnectionString and DataSource are provided. Please specify only one.");
19 | }
20 |
21 | if (options.Logger is not null)
22 | {
23 | NpgsqlRestMiddleware.SetLogger(options.Logger);
24 | }
25 | else if (builder is WebApplication app)
26 | {
27 | var factory = app.Services.GetRequiredService();
28 | NpgsqlRestMiddleware.SetLogger(factory is not null ? factory.CreateLogger(options.LoggerName ?? typeof(NpgsqlRestMiddlewareExtensions).Namespace ?? "NpgsqlRest") : app.Logger);
29 | }
30 |
31 |
32 | NpgsqlRestMiddleware.SetMetadata(NpgsqlRestMetadataBuilder.Build(options, NpgsqlRestMiddleware.Logger, builder));
33 | if (NpgsqlRestMiddleware.Metadata.Entries.Count == 0)
34 | {
35 | return builder;
36 | }
37 | NpgsqlRestMiddleware.SetOptions(options);
38 | NpgsqlRestMiddleware.SetServiceProvider(builder.ApplicationServices);
39 |
40 | if (options.RefreshEndpointEnabled)
41 | {
42 | var refreshMethodUpper = options.RefreshMethod.ToUpperInvariant();
43 | var refreshPathUpper = options.RefreshPath.ToUpperInvariant();
44 |
45 | builder.Use(async (context, next) =>
46 | {
47 | if (context.Request.Method.Equals(refreshMethodUpper, StringComparison.OrdinalIgnoreCase) &&
48 | context.Request.Path.Equals(refreshPathUpper, StringComparison.OrdinalIgnoreCase))
49 | {
50 | try
51 | {
52 | Volatile.Write(ref NpgsqlRestMiddleware.metadata, NpgsqlRestMetadataBuilder.Build(options, options.Logger, builder));
53 | NpgsqlRestMiddleware.lookup = NpgsqlRestMiddleware.metadata.Entries.GetAlternateLookup>();
54 | context.Response.StatusCode = (int)HttpStatusCode.OK;
55 | await context.Response.CompleteAsync();
56 | }
57 | catch (Exception e)
58 | {
59 | options.Logger?.LogError(e, "Failed to refresh metadata");
60 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
61 | context.Response.ContentType = Text.Plain;
62 | await context.Response.WriteAsync($"Failed to refresh metadata: {e.Message}");
63 | await context.Response.CompleteAsync();
64 | }
65 | return;
66 | }
67 | await next(context);
68 | });
69 | }
70 |
71 | return builder.UseMiddleware();
72 | }
73 | }
--------------------------------------------------------------------------------
/NpgsqlRest/ParameterValidationValues.cs:
--------------------------------------------------------------------------------
1 | namespace NpgsqlRest;
2 |
3 | public class ParameterValidationValues(
4 | HttpContext context,
5 | Routine routine,
6 | NpgsqlRestParameter parameter)
7 | {
8 | ///
9 | /// Current HttpContext.
10 | ///
11 | public readonly HttpContext Context = context;
12 | ///
13 | /// Current Routine.
14 | ///
15 | public readonly Routine Routine = routine;
16 | ///
17 | /// Parameter to be validated. Note: if parameter is using default value and value not provided, parameter.Value is null.
18 | ///
19 | public readonly NpgsqlRestParameter Parameter = parameter;
20 | }
21 |
--------------------------------------------------------------------------------
/NpgsqlRest/Parser.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.CompilerServices;
2 | using System.Text.RegularExpressions;
3 |
4 | namespace NpgsqlRest;
5 |
6 | public static partial class Parser
7 | {
8 | [GeneratedRegex(@"^(\d*\.?\d+)\s*([a-z]+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase)]
9 | private static partial Regex IntervalRegex();
10 |
11 | public static TimeSpan? ParsePostgresInterval(string interval)
12 | {
13 | if (string.IsNullOrWhiteSpace(interval))
14 | {
15 | return null;
16 | }
17 |
18 | interval = interval.Trim().ToLowerInvariant();
19 |
20 | // Match number (integer or decimal) followed by optional space and unit
21 | var match = IntervalRegex().Match(interval);
22 | if (!match.Success)
23 | {
24 | return null;
25 | }
26 |
27 | string numberPart = match.Groups[1].Value;
28 | string unitPart = match.Groups[2].Value;
29 |
30 | if (!double.TryParse(numberPart, System.Globalization.NumberStyles.Any,
31 | System.Globalization.CultureInfo.InvariantCulture, out double value))
32 | {
33 | return null;
34 | }
35 |
36 | // Map PostgreSQL units to TimeSpan conversions
37 | return unitPart switch
38 | {
39 | "s" or "sec" or "second" or "seconds" => TimeSpan.FromSeconds(value),
40 | "m" or "min" or "minute" or "minutes" => TimeSpan.FromMinutes(value),
41 | "h" or "hour" or "hours" => TimeSpan.FromHours(value),
42 | "d" or "day" or "days" => TimeSpan.FromDays(value),
43 | _ => null
44 | };
45 | }
46 |
47 | [MethodImpl(MethodImplOptions.AggressiveInlining)]
48 | public static bool IsPatternMatch(string name, string pattern)
49 | {
50 | if (name == null || pattern == null) return false;
51 | int nl = name.Length, pl = pattern.Length;
52 | if (nl == 0 || pl == 0) return false;
53 |
54 | if (pl > 1 && pattern[0] == Consts.Multiply && pattern[1] == Consts.Dot)
55 | {
56 | ReadOnlySpan ext = pattern.AsSpan(1);
57 | return nl > ext.Length && name.AsSpan(nl - ext.Length).Equals(ext, StringComparison.OrdinalIgnoreCase);
58 | }
59 |
60 | int ni = 0, pi = 0;
61 | int lastStar = -1, lastMatch = 0;
62 |
63 | while (ni < nl)
64 | {
65 | if (pi < pl)
66 | {
67 | char pc = pattern[pi];
68 | if (pc == Consts.Multiply)
69 | {
70 | lastStar = pi++;
71 | lastMatch = ni;
72 | continue;
73 | }
74 | if (pc == Consts.Question ? ni < nl : char.ToLowerInvariant(pc) == char.ToLowerInvariant(name[ni]))
75 | {
76 | ni++;
77 | pi++;
78 | continue;
79 | }
80 | }
81 | if (lastStar >= 0)
82 | {
83 | pi = lastStar + 1;
84 | ni = ++lastMatch;
85 | continue;
86 | }
87 | return false;
88 | }
89 |
90 | while (pi < pl && pattern[pi] == Consts.Multiply) pi++;
91 | return pi == pl;
92 | }
93 | }
--------------------------------------------------------------------------------
/NpgsqlRest/PasswordHasher.cs:
--------------------------------------------------------------------------------
1 | using System.Security.Cryptography;
2 |
3 | namespace NpgsqlRest;
4 |
5 | public interface IPasswordHasher
6 | {
7 | ///
8 | /// Hashes a password.
9 | ///
10 | /// The password to hash.
11 | /// A hashed representation of the password.
12 | string HashPassword(string password);
13 |
14 | ///
15 | /// Verifies a provided password against a stored hash.
16 | ///
17 | /// The stored hashed password.
18 | /// The password to verify.
19 | /// True if the password matches, false otherwise.
20 | bool VerifyHashedPassword(string hashedPassword, string providedPassword);
21 | }
22 |
23 | public class PasswordHasher : IPasswordHasher
24 | {
25 | // Configuration constants
26 | private const int SaltByteSize = 16; // 128-bit salt
27 | private const int HashByteSize = 32; // 256-bit hash
28 | private const int Iterations = 600_000; // OWASP-recommended iteration count for PBKDF2-SHA256 (2025)
29 |
30 | ///
31 | /// Hashes a password.
32 | ///
33 | /// The password to hash.
34 | /// A hashed representation of the password.
35 | /// Thrown if the password is null or empty.
36 | public string HashPassword(string password)
37 | {
38 | if (string.IsNullOrEmpty(password))
39 | throw new ArgumentException("Password cannot be null or empty.", nameof(password));
40 |
41 | // Generate a random salt
42 | byte[] salt = RandomNumberGenerator.GetBytes(SaltByteSize);
43 |
44 | // Hash the password using PBKDF2
45 | using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256);
46 | byte[] hash = pbkdf2.GetBytes(HashByteSize);
47 |
48 | // Combine salt and hash into a single byte array
49 | byte[] hashBytes = new byte[SaltByteSize + HashByteSize];
50 | Array.Copy(salt, 0, hashBytes, 0, SaltByteSize);
51 | Array.Copy(hash, 0, hashBytes, SaltByteSize, HashByteSize);
52 |
53 | // Convert to base64 for storage
54 | return Convert.ToBase64String(hashBytes);
55 | }
56 |
57 | ///
58 | /// Verifies a provided password against a stored hash.
59 | ///
60 | /// The stored hashed password.
61 | /// The password to verify.
62 | /// True if the password matches, false otherwise.
63 | public bool VerifyHashedPassword(string hashedPassword, string providedPassword)
64 | {
65 | if (string.IsNullOrEmpty(providedPassword) || string.IsNullOrEmpty(hashedPassword))
66 | return false;
67 |
68 | try
69 | {
70 | // Decode the stored hash
71 | byte[] hashBytes = Convert.FromBase64String(hashedPassword);
72 |
73 | // Validate the stored hash length
74 | if (hashBytes.Length != SaltByteSize + HashByteSize)
75 | return false;
76 |
77 | // Extract the salt
78 | byte[] salt = new byte[SaltByteSize];
79 | Array.Copy(hashBytes, 0, salt, 0, SaltByteSize);
80 |
81 | // Extract the hash
82 | byte[] storedSubHash = new byte[HashByteSize];
83 | Array.Copy(hashBytes, SaltByteSize, storedSubHash, 0, HashByteSize);
84 |
85 | // Compute the hash of the provided password
86 | using var pbkdf2 = new Rfc2898DeriveBytes(providedPassword, salt, Iterations, HashAlgorithmName.SHA256);
87 | byte[] computedHash = pbkdf2.GetBytes(HashByteSize);
88 |
89 | // Compare the hashes in constant time
90 | return CryptographicOperations.FixedTimeEquals(computedHash, storedSubHash);
91 | }
92 | catch
93 | {
94 | // Handle invalid base64 or other errors
95 | return false;
96 | }
97 | }
98 | }
99 |
100 |
--------------------------------------------------------------------------------
/NpgsqlRest/RoutineCache.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Concurrent;
2 |
3 | namespace NpgsqlRest;
4 |
5 | public interface IRoutineCache
6 | {
7 | bool Get(RoutineEndpoint endpoint, string key, out object? result);
8 | void AddOrUpdate(RoutineEndpoint endpoint, string key, object? value);
9 | }
10 |
11 | public class RoutineCache : IRoutineCache
12 | {
13 | private class CacheEntry
14 | {
15 | public object? Value { get; set; }
16 | public DateTime? ExpirationTime { get; set; }
17 | public bool IsExpired => ExpirationTime.HasValue && DateTime.UtcNow > ExpirationTime.Value;
18 | }
19 |
20 | private static readonly ConcurrentDictionary _cache = new();
21 | private static readonly ConcurrentDictionary _originalKeys = new();
22 | private static Timer? _cleanupTimer;
23 |
24 | public static void Start(NpgsqlRestOptions options)
25 | {
26 | _cleanupTimer = new Timer(
27 | _ => CleanupExpiredEntriesInternal(),
28 | null,
29 | TimeSpan.FromMinutes(options.CachePruneIntervalMin),
30 | TimeSpan.FromMinutes(options.CachePruneIntervalMin));
31 | }
32 |
33 | public static void Shutdown()
34 | {
35 | _cleanupTimer?.Dispose();
36 | _cache.Clear();
37 | _originalKeys.Clear();
38 | }
39 |
40 | private static void CleanupExpiredEntriesInternal()
41 | {
42 | var expiredKeys = _cache
43 | .Where(kvp => kvp.Value.IsExpired)
44 | .Select(kvp => kvp.Key)
45 | .ToList();
46 |
47 | foreach (var key in expiredKeys)
48 | {
49 | _cache.TryRemove(key, out _);
50 | _originalKeys.TryRemove(key, out _);
51 | }
52 | }
53 |
54 | public bool Get(RoutineEndpoint endpoint, string key, out object? result)
55 | {
56 | var hashedKey = key.GetHashCode();
57 |
58 | if (_cache.TryGetValue(hashedKey, out var entry))
59 | {
60 | if (_originalKeys.TryGetValue(hashedKey, out var originalKey) && originalKey == key)
61 | {
62 | if (entry.IsExpired)
63 | {
64 | // Remove expired entry
65 | _cache.TryRemove(hashedKey, out _);
66 | _originalKeys.TryRemove(hashedKey, out _);
67 | result = null;
68 | return false;
69 | }
70 |
71 | result = entry.Value;
72 | return true;
73 | }
74 | }
75 |
76 | result = null;
77 | return false;
78 | }
79 |
80 | public void AddOrUpdate(RoutineEndpoint endpoint, string key, object? value)
81 | {
82 | var hashedKey = key.GetHashCode();
83 | var entry = new CacheEntry
84 | {
85 | Value = value,
86 | ExpirationTime = endpoint.CacheExpiresIn.HasValue ? DateTime.UtcNow + endpoint.CacheExpiresIn.Value : null
87 | };
88 |
89 | _cache[hashedKey] = entry;
90 | _originalKeys[hashedKey] = key;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/NpgsqlRest/RoutineEndpoint.cs:
--------------------------------------------------------------------------------
1 | using Microsoft.Extensions.Primitives;
2 |
3 | namespace NpgsqlRest;
4 |
5 | public class RoutineEndpoint(
6 | Routine routine,
7 | string url,
8 | Method method,
9 | RequestParamType requestParamType,
10 | bool requiresAuthorization,
11 | int? commandTimeout,
12 | string? responseContentType,
13 | Dictionary responseHeaders,
14 | RequestHeadersMode requestHeadersMode,
15 | string requestHeadersParameterName,
16 | string? bodyParameterName,
17 | TextResponseNullHandling textResponseNullHandling,
18 | QueryStringNullHandling queryStringNullHandling,
19 | HashSet? authorizeRoles = null,
20 | bool login = false,
21 | bool logout = false,
22 | bool securitySensitive = false,
23 | ulong? bufferRows = null,
24 | bool raw = false,
25 | string? rawValueSeparator = null,
26 | string? rawNewLineSeparator = null,
27 | bool rawColumnNames = false,
28 | bool cached = false,
29 | string[]? cachedParams = null,
30 | TimeSpan? cacheExpiresIn = null,
31 | bool parseResponse = false,
32 | string? connectionName = null,
33 | bool upload = false,
34 | string[]? uploadHandlers = null,
35 | Dictionary? customParameters = null,
36 | bool userContext = false,
37 | bool userParameters = false)
38 | {
39 | private string? _bodyParameterName = bodyParameterName;
40 |
41 | internal bool HasBodyParameter = !string.IsNullOrWhiteSpace(bodyParameterName);
42 | internal Action? LogCallback { get; set; }
43 | internal bool HeadersNeedParsing { get; set; } = false;
44 | internal bool CustomParamsNeedParsing { get; set; } = false;
45 |
46 | public Routine Routine { get; } = routine;
47 | public string Url { get; set; } = url;
48 | public Method Method { get; set; } = method;
49 | public RequestParamType RequestParamType { get; set; } = requestParamType;
50 | public bool RequiresAuthorization { get; set; } = requiresAuthorization;
51 | public int? CommandTimeout { get; set; } = commandTimeout;
52 | public string? ResponseContentType { get; set; } = responseContentType;
53 | public Dictionary ResponseHeaders { get; set; } = responseHeaders;
54 | public RequestHeadersMode RequestHeadersMode { get; set; } = requestHeadersMode;
55 | public string RequestHeadersParameterName { get; set; } = requestHeadersParameterName;
56 | public string? BodyParameterName
57 | {
58 | get => _bodyParameterName;
59 | set
60 | {
61 | HasBodyParameter = !string.IsNullOrWhiteSpace(value);
62 | _bodyParameterName = value;
63 | }
64 | }
65 | public TextResponseNullHandling TextResponseNullHandling { get; set; } = textResponseNullHandling;
66 | public QueryStringNullHandling QueryStringNullHandling { get; set; } = queryStringNullHandling;
67 | public HashSet? AuthorizeRoles { get; set; } = authorizeRoles;
68 | public bool Login { get; set; } = login;
69 | public bool Logout { get; set; } = logout;
70 | public bool SecuritySensitive { get; set; } = securitySensitive;
71 | public bool IsAuth => Login || Logout || SecuritySensitive;
72 | public ulong? BufferRows { get; set; } = bufferRows;
73 | public bool Raw { get; set; } = raw;
74 | public string? RawValueSeparator { get; set; } = rawValueSeparator;
75 | public string? RawNewLineSeparator { get; set; } = rawNewLineSeparator;
76 | public bool RawColumnNames { get; set; } = rawColumnNames;
77 | public string[][]? CommentWordLines { get; internal set; }
78 | public bool Cached { get; set; } = cached;
79 | public HashSet? CachedParams { get; set; } = cachedParams?.ToHashSet();
80 | public TimeSpan? CacheExpiresIn { get; set; } = cacheExpiresIn;
81 | public bool ParseResponse { get; set; } = parseResponse;
82 | public string? ConnectionName { get; set; } = connectionName;
83 | public bool Upload { get; set; } = upload;
84 | public string[]? UploadHandlers { get; set; } = uploadHandlers;
85 | public Dictionary? CustomParameters { get; set; } = customParameters;
86 | public bool UserContext { get; set; } = userContext;
87 | public bool UseUserParameters { get; set; } = userParameters;
88 | }
89 |
--------------------------------------------------------------------------------
/NpgsqlRest/RoutineSourceParameterFormatter.cs:
--------------------------------------------------------------------------------
1 | using System.Globalization;
2 | using System.Text;
3 | using Npgsql;
4 |
5 | namespace NpgsqlRest;
6 |
7 | public class RoutineSourceParameterFormatter : IRoutineSourceParameterFormatter
8 | {
9 | public bool IsFormattable { get; } = false;
10 |
11 | public string AppendCommandParameter(NpgsqlRestParameter parameter, int index)
12 | {
13 | var suffix = parameter.TypeDescriptor.IsCastToText() ?
14 | string.Concat(Consts.DoubleColon, parameter.TypeDescriptor.OriginalType) :
15 | string.Empty;
16 |
17 | if (index == 0)
18 | {
19 | return parameter.ActualName is null ?
20 | string.Concat(Consts.FirstParam, suffix) :
21 | string.Concat(parameter.ActualName, Consts.FirstNamedParam, suffix);
22 | }
23 |
24 | var indexStr = (index + 1).ToString(CultureInfo.InvariantCulture);
25 |
26 | return parameter.ActualName is null ?
27 | string.Concat(Consts.Comma, Consts.Dollar, indexStr, suffix) :
28 | string.Concat(Consts.Comma, parameter.ActualName, Consts.NamedParam, indexStr, suffix);
29 | }
30 |
31 | public string? AppendEmpty() => Consts.CloseParenthesisStr;
32 | }
33 |
34 | public class RoutineSourceCustomTypesParameterFormatter : IRoutineSourceParameterFormatter
35 | {
36 | public bool IsFormattable { get; } = true;
37 |
38 | public string FormatCommand(Routine routine, NpgsqlParameterCollection parameters)
39 | {
40 | var sb = new StringBuilder(routine.Expression, routine.Expression.Length + parameters.Count * 20);
41 | var count = parameters.Count;
42 |
43 | var culture = CultureInfo.InvariantCulture;
44 |
45 | for (var i = 0; i < count; i++)
46 | {
47 | var parameter = (NpgsqlRestParameter)parameters[i];
48 | var typeDescriptor = parameter.TypeDescriptor;
49 |
50 | var suffix = typeDescriptor.IsCastToText() ?
51 | string.Concat(Consts.DoubleColon, typeDescriptor.OriginalType) :
52 | string.Empty;
53 |
54 | if (i > 0)
55 | {
56 | sb.Append(Consts.Comma);
57 | }
58 |
59 | var indexStr = (i + 1).ToString(culture);
60 |
61 | if (typeDescriptor.CustomType is null)
62 | {
63 | if (parameter.ActualName is null)
64 | {
65 | sb.Append(Consts.Dollar)
66 | .Append(indexStr)
67 | .Append(suffix);
68 | }
69 | else
70 | {
71 | sb.Append(parameter.ActualName)
72 | .Append(Consts.NamedParam)
73 | .Append(indexStr);
74 | }
75 | }
76 | else
77 | {
78 | if (typeDescriptor.CustomTypePosition == 1)
79 | {
80 | sb.Append(typeDescriptor.OriginalParameterName)
81 | .Append(Consts.OpenRow);
82 | }
83 |
84 | sb.Append(Consts.Dollar)
85 | .Append(indexStr)
86 | .Append(suffix);
87 |
88 | if (i == count - 1 ||
89 | typeDescriptor.CustomTypePosition !=
90 | ((NpgsqlRestParameter)parameters[i + 1]).TypeDescriptor.CustomTypePosition - 1)
91 | {
92 | sb.Append(Consts.CloseRow)
93 | .Append(typeDescriptor.CustomType);
94 | }
95 | }
96 | }
97 |
98 | sb.Append(Consts.CloseParenthesis);
99 | return sb.ToString();
100 | }
101 | }
--------------------------------------------------------------------------------
/NpgsqlRest/UploadHandlers/DefaultUploadHandler.cs:
--------------------------------------------------------------------------------
1 | using Npgsql;
2 |
3 | namespace NpgsqlRest.UploadHandlers;
4 |
5 | public class DefaultUploadHandler(params IUploadHandler[] handlers) : IUploadHandler
6 | {
7 | private readonly IUploadHandler[] _handlers = handlers;
8 | private readonly bool _requiresTransaction = handlers.Any(h => h.RequiresTransaction);
9 |
10 | public bool RequiresTransaction => _requiresTransaction;
11 |
12 | public void OnError(NpgsqlConnection? connection, HttpContext context, Exception? exception)
13 | {
14 | for(int i = 0; i < _handlers.Length; i++)
15 | {
16 | _handlers[i].OnError(connection, context, exception);
17 | }
18 | }
19 |
20 | public async Task