├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE │ ├── BUG_REPORT.yml │ ├── FEATURE_REQUEST.yml │ └── SUPPORT.yml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── codeql-analysis.yml │ ├── linter.yml │ └── semantic.yml ├── .gitignore ├── .markdownlint.yml ├── Assets └── influxdata.jpg ├── CHANGELOG.md ├── Client.Test.Integration ├── Client.Test.Integration.csproj ├── IntegrationTest.cs ├── QueryWriteTest.cs └── WriteTest.cs ├── Client.Test ├── Client.Test.csproj ├── Config │ ├── ClientConfigTest.cs │ ├── QueryOptionsComparer.cs │ └── QueryOptionsTest.cs ├── InfluxDBApiExceptionTest.cs ├── InfluxDBClientHttpsTest.cs ├── InfluxDBClientQueryTest.cs ├── InfluxDBClientTest.cs ├── InfluxDBClientWriteTest.cs ├── Internal │ ├── ArgumentsDurationTest.cs │ ├── ArgumentsTest.cs │ ├── AssemblyHelperTest.cs │ ├── FlightSqlClientTest.cs │ ├── FlightSqlExtensionsTest.cs │ ├── RecordBatchConverterTest.cs │ ├── RestClientTest.cs │ ├── ServerCertificateCustomValidationsTest.cs │ ├── TimestampConverterTest.cs │ └── TypeCastTest.cs ├── MockHttpsServerTest.cs ├── MockServerTest.cs ├── TestData │ ├── OtherCerts │ │ ├── empty.pem │ │ ├── invalid.pem │ │ └── otherCA.pem │ └── ServerCert │ │ ├── generate-self-signed-cert.sh │ │ ├── rootCA.key │ │ ├── rootCA.pem │ │ ├── server.key │ │ ├── server.p12 │ │ └── server.pem ├── Usings.cs └── Write │ ├── PointDataTest.cs │ └── WritePrecisionConverterTest.cs ├── Client ├── Client.csproj ├── Config │ ├── ClientConfig.cs │ ├── QueryOptions.cs │ └── WriteOptions.cs ├── InfluxDBApiException.cs ├── InfluxDBClient.cs ├── Internal │ ├── Arguments.cs │ ├── AssemblyHelper.cs │ ├── FlightSqlClient.cs │ ├── FlightSqlExtensions.cs │ ├── GzipHandler.cs │ ├── RecordBatchConverter.cs │ ├── RestClient.cs │ ├── ServerCertificateCustomValidations.cs │ ├── TimestampConverter.cs │ └── TypeCasting.cs ├── Query │ └── QueryType.cs └── Write │ ├── PointData.cs │ ├── PointDataValues.cs │ └── WritePrecision.cs ├── Examples ├── CustomSslCerts │ ├── CustomSslCerts.csproj │ └── CustomSslCertsExample.cs ├── Downsampling │ ├── Downsampling.csproj │ └── DownsamplingExample.cs ├── General │ ├── General.csproj │ ├── HttpErrorHandled.cs │ └── Runner.cs ├── IOx │ ├── Examples.IOx.csproj │ └── IOxExample.cs └── README.md ├── InfluxDB3.sln ├── Keys ├── Key.public.snk ├── Key.snk └── README.md ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── api │ └── .gitignore ├── docfx.json ├── favicon.ico ├── images │ └── influxdata-logo--symbol--white.png ├── index.md ├── publish-site.sh └── toc.yml └── net_logo.svg /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | commands: 3 | client-test: 4 | parameters: 5 | project: 6 | type: string 7 | default: "Client.Test" 8 | steps: 9 | - checkout 10 | - run: 11 | name: Install Dependencies 12 | command: | 13 | dotnet restore 14 | dotnet build --no-restore 15 | - run: 16 | name: Create a temp directory for artifacts 17 | command: | 18 | mkdir -p /tmp/artifacts 19 | mkdir test-results 20 | - run: 21 | name: Run tests 22 | command: dotnet test << parameters.project >> --collect "Xplat Code Coverage" --logger "junit;LogFilePath=../test-results/test-result.xml" 23 | - run: 24 | name: Coverage Report 25 | command: | 26 | dotnet tool install --tool-path="./reportgenerator/" dotnet-reportgenerator-globaltool 27 | ./reportgenerator/reportgenerator -reports:"<< parameters.project >>/TestResults/*/coverage.cobertura.xml" -targetdir:"report" -reporttypes:HtmlSummary "-sourcedirs:Client/" 28 | mv report/summary.html /tmp/artifacts 29 | cp test-results/test-result.xml /tmp/artifacts 30 | when: always 31 | - run: 32 | name: Report test results to codecov 33 | command: | 34 | apt-get update 35 | apt-get install gpg --yes 36 | curl -Os https://uploader.codecov.io/latest/linux/codecov 37 | curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM 38 | curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig 39 | curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --no-default-keyring --keyring trustedkeys.gpg --import 40 | gpgv codecov.SHA256SUM.sig codecov.SHA256SUM 41 | shasum -a 256 -c codecov.SHA256SUM 42 | chmod +x ./codecov 43 | ./codecov 44 | - store_artifacts: 45 | path: /tmp/artifacts 46 | - store_test_results: 47 | path: test-results 48 | 49 | 50 | jobs: 51 | tests-unit: 52 | working_directory: ~/repo 53 | docker: 54 | - image: &default-dotnet-image "mcr.microsoft.com/dotnet/sdk:8.0" 55 | steps: 56 | - client-test: 57 | project: "Client.Test" 58 | tests-integration: 59 | working_directory: ~/repo 60 | docker: 61 | - image: *default-dotnet-image 62 | steps: 63 | - client-test: 64 | project: "Client.Test.Integration" 65 | 66 | check-compilation-warnings: 67 | docker: 68 | - image: *default-dotnet-image 69 | steps: 70 | - checkout 71 | - run: 72 | name: Check compilation warnings 73 | command: | 74 | dotnet clean --configuration Release 75 | dotnet build --configuration Release 76 | 77 | deploy-preview: 78 | docker: 79 | - image: *default-dotnet-image 80 | steps: 81 | - run: 82 | name: Early return if this build is from a forked repository 83 | command: | 84 | if [[ $CIRCLE_PROJECT_USERNAME != "InfluxCommunity" ]]; then 85 | echo "Nothing to do for forked repositories, so marking this step successful" 86 | circleci step halt 87 | fi 88 | - checkout 89 | - run: 90 | name: Deploying To Preview Repository 91 | command: | 92 | dotnet pack Client --version-suffix=dev.$CIRCLE_BUILD_NUM 93 | dotnet nuget push ./Client/bin/Release/InfluxDB3.Client.*-dev.$CIRCLE_BUILD_NUM.nupkg -k ${NUGET_KEY} -s https://api.nuget.org/v3/index.json 94 | 95 | workflows: 96 | version: 2 97 | build: 98 | jobs: 99 | - check-compilation-warnings 100 | - tests-unit 101 | - tests-integration: 102 | requires: 103 | - "tests-unit" 104 | - deploy-preview: 105 | requires: 106 | - tests-integration 107 | filters: 108 | branches: 109 | only: main 110 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a bug report to help us improve 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking time to fill out this bug report! We reserve this repository issues for bugs with reproducible problems. 9 | Please redirect any questions about the client usage to our [Community Slack](https://app.slack.com/huddle/TH8RGQX5Z/C02UDUPLQKA) or [Community Page](https://community.influxdata.com/) we have a lot of talented community members there who could help answer your question more quickly. 10 | 11 | * Please add a :+1: or comment on a similar existing bug report instead of opening a new one. 12 | * Please check whether the bug can be reproduced with the latest release. 13 | - type: textarea 14 | id: specifications 15 | attributes: 16 | label: Specifications 17 | description: Describe the steps to reproduce the bug. 18 | value: | 19 | * Client Version: 20 | * InfluxDB Version: 21 | * Platform: 22 | validations: 23 | required: true 24 | - type: textarea 25 | id: reproduce 26 | attributes: 27 | label: Code sample to reproduce problem 28 | description: Provide a code sample that reproduces the problem 29 | value: | 30 | ```csharp 31 | ``` 32 | validations: 33 | required: true 34 | - type: textarea 35 | id: expected-behavior 36 | attributes: 37 | label: Expected behavior 38 | description: Describe what you expected to happen when you performed the above steps. 39 | validations: 40 | required: true 41 | - type: textarea 42 | id: actual-behavior 43 | attributes: 44 | label: Actual behavior 45 | description: Describe what actually happened when you performed the above steps. 46 | validations: 47 | required: true 48 | - type: textarea 49 | id: additional-info 50 | attributes: 51 | label: Additional info 52 | description: Include gist of relevant config, logs, etc. 53 | validations: 54 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Create a feature request to make client more awesome 3 | labels: ["feature request"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking time to share with us this feature request! Please describe why you would like this feature to be added to the client, how you plan to use it to make your life better. 9 | - type: textarea 10 | id: use-case 11 | attributes: 12 | label: Use Case 13 | description: Describe how you plan to use this feature. 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: expected-behavior 18 | attributes: 19 | label: Expected behavior 20 | description: Describe what you expected to happen when you performed the above steps. 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: actual-behavior 25 | attributes: 26 | label: Actual behavior 27 | description: Describe what actually happened when you performed the above steps. 28 | validations: 29 | required: true 30 | - type: textarea 31 | id: additional-info 32 | attributes: 33 | label: Additional info 34 | description: Include gist of relevant config, logs, etc. 35 | validations: 36 | required: false 37 | 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/SUPPORT.yml: -------------------------------------------------------------------------------- 1 | name: Support request 2 | description: Open a support request 3 | labels: ["support"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | WOAHH, hold up. This isn't the best place for support questions. 9 | You can get a faster response on slack or forums: 10 | 11 | Please redirect any QUESTIONS about Client usage to 12 | - InfluxData Slack Channel: https://app.slack.com/huddle/TH8RGQX5Z/C02UDUPLQKA 13 | - InfluxData Community Site: https://community.influxdata.com 14 | 15 | - type: textarea 16 | attributes: 17 | label: "Please direct all support questions to slack or the forums. Thank you." 18 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Closes # 2 | 3 | ## Proposed Changes 4 | 5 | _Briefly describe your proposed changes:_ 6 | 7 | ## Checklist 8 | 9 | 10 | 11 | - [ ] CHANGELOG.md updated 12 | - [ ] Rebased/mergeable 13 | - [ ] A test has been added if appropriate 14 | - [ ] Tests pass 15 | - [ ] Commit messages are [conventional](https://www.conventionalcommits.org/en/v1.0.0/) 16 | - [ ] Sign [CLA](https://www.influxdata.com/legal/cla/) (if not already signed) 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "nuget" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | CodeQL-Build: 11 | runs-on: ubuntu-latest 12 | 13 | permissions: 14 | security-events: write 15 | actions: read 16 | contents: read 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup .NET 23 | uses: actions/setup-dotnet@v3 24 | with: 25 | dotnet-version: | 26 | 8.0.x 27 | 28 | - name: Initialize CodeQL 29 | uses: github/codeql-action/init@v2 30 | with: 31 | languages: csharp 32 | 33 | - name: Autobuild 34 | uses: github/codeql-action/autobuild@v2 35 | 36 | - name: Perform CodeQL Analysis 37 | uses: github/codeql-action/analyze@v2 -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: "Lint Code Base" 2 | 3 | on: 4 | push: 5 | branches-ignore: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | permissions: 12 | contents: read 13 | packages: read 14 | statuses: write 15 | 16 | name: Lint Code Base 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout Code 21 | uses: actions/checkout@v3 22 | with: 23 | fetch-depth: 0 24 | 25 | - name: Lint Code Base 26 | uses: github/super-linter@v4.9.2 27 | env: 28 | VALIDATE_ALL_CODEBASE: false 29 | DEFAULT_BRANCH: main 30 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 31 | LINTER_RULES_PATH: '.' 32 | MARKDOWN_CONFIG_FILE: .markdownlint.yml 33 | VALIDATE_MARKDOWN: true 34 | VALIDATE_BASH: true 35 | VALIDATE_CSHARP: true 36 | VALIDATE_GITHUB_ACTIONS: true -------------------------------------------------------------------------------- /.github/workflows/semantic.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: "Semantic PR and Commit Messages" 3 | 4 | on: 5 | pull_request: 6 | types: [opened, reopened, synchronize, edited] 7 | branches: 8 | - main 9 | 10 | jobs: 11 | semantic: 12 | uses: influxdata/validate-semantic-github-messages/.github/workflows/semantic.yml@main 13 | with: 14 | CHECK_PR_TITLE_OR_ONE_COMMIT: true 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea/ 2 | /.vscode/ 3 | /.vs/ 4 | 5 | # Build results 6 | [Dd]ebug/ 7 | [Dd]ebugPublic/ 8 | [Rr]elease/ 9 | [Rr]eleases/ 10 | x64/ 11 | x86/ 12 | bld/ 13 | [Bb]in/ 14 | [Oo]bj/ 15 | [Ll]og/ 16 | 17 | # Test results 18 | report/ 19 | reportgenerator/ 20 | TestResults/ 21 | InfluxDB3.sln.DotSettings.user -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | { 2 | "MD013": false, 3 | "MD024": { 4 | "siblings_only": true 5 | }, 6 | "MD033": { 7 | "allowed_elements": [ "a", "img", "p" ] 8 | }, 9 | "MD041": false, 10 | } 11 | -------------------------------------------------------------------------------- /Assets/influxdata.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-csharp/71d1c5919f9e1fa1cc798f8e8b397fb9c6de6e62/Assets/influxdata.jpg -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.3.0 [unreleased] 2 | 3 | ## 1.2.0 [2025-05-22] 4 | 5 | ### Features 6 | 7 | 1. [#155](https://github.com/InfluxCommunity/influxdb3-csharp/pull/155): Allows setting grpc options. 8 | 1. [#157](https://github.com/InfluxCommunity/influxdb3-csharp/pull/157): Fix: always clone `DefaultOptions` to keep it 9 | immutable. 10 | 1. [#158](https://github.com/InfluxCommunity/influxdb3-csharp/pull/158): Support fast writes without waiting for WAL 11 | persistence: 12 | - New write option (`WriteOptions.NoSync`) added: `true` value means faster write but without the confirmation that 13 | the data was persisted. Default value: `false`. 14 | - **Supported by self-managed InfluxDB 3 Core and Enterprise servers only!** 15 | - Also configurable via connection string query parameter (`writeNoSync`). 16 | - Also configurable via environment variable (`INFLUX_WRITE_NO_SYNC`). 17 | - Long precision string values added from v3 HTTP API: `"nanosecond"`, `"microsecond"`, `"millisecond"`, 18 | `"second"` ( 19 | in addition to the existing `"ns"`, `"us"`, `"ms"`, `"s"`). 20 | 21 | ## 1.1.0 [2025-03-26] 22 | 23 | ### Features 24 | 25 | 1. [#153](https://github.com/InfluxCommunity/influxdb3-csharp/pull/153): Add custom SSL root certificate support. 26 | - New configuration items: 27 | - `SslRootsFilePath` 28 | - `DisableCertificateRevocationListCheck` 29 | - **Disclaimer:** Using custom SSL root certificate configurations is recommended for development and testing 30 | purposes 31 | only. For production deployments, ensure custom certificates are added to the operating system's trusted 32 | certificate store. 33 | 34 | ## 1.0.0 [2025-01-22] 35 | 36 | ### Features 37 | 38 | 1. [#132](https://github.com/InfluxCommunity/influxdb3-csharp/pull/132): Respect iox::column_type::field metadata when 39 | mapping query results into values. 40 | - iox::column_type::field::integer: => Long 41 | - iox::column_type::field::uinteger: => Long 42 | - iox::column_type::field::float: => Double 43 | - iox::column_type::field::string: => String 44 | - iox::column_type::field::boolean: => Boolean 45 | 46 | ## 0.8.0 [2024-09-13] 47 | 48 | ### Features 49 | 50 | 1.[#118](https://github.com/InfluxCommunity/influxdb3-csharp/pull/118): Simplify getting response headers and status code from `InfluxDBApiException`. Includes example runnable through `Examples/General`. 51 | 52 | ## 0.7.0 [2024-08-12] 53 | 54 | ### Migration Notice 55 | 56 | - `InfluxDBClient` constructor with connection options has new option `authScheme` with `null` default value: 57 | 58 | ```diff 59 | - public InfluxDBClient(string host, string token, string? organization = null, string? database = null); 60 | + public InfluxDBClient(string host, string token, string? organization = null, string? database = null, string? authScheme = null) 61 | ``` 62 | 63 | This new option is used for Edge (OSS) authentication. 64 | 65 | ### Features 66 | 67 | 1. [#101](https://github.com/InfluxCommunity/influxdb3-csharp/pull/101): Add standard `user-agent` header to all calls. 68 | 1. [#111](https://github.com/InfluxCommunity/influxdb3-csharp/pull/111): Add InfluxDB Edge (OSS) authentication support. 69 | 70 | ### Bug Fixes 71 | 72 | 1. [#110](https://github.com/InfluxCommunity/influxdb3-csharp/pull/110): InfluxDB Edge (OSS) error handling. 73 | 74 | ## 0.6.0 [2024-04-16] 75 | 76 | ### Features 77 | 78 | 1. [#90](https://github.com/InfluxCommunity/influxdb3-csharp/pull/90): Custom `HTTP/gRPC` headers can be specified globally by config or per request 79 | 80 | ## 0.5.0 [2024-03-01] 81 | 82 | ### Features 83 | 84 | 1. [#71](https://github.com/InfluxCommunity/influxdb3-csharp/pull/71): Add support for named query parameters 85 | 86 | ### Others 87 | 88 | 1. [#80](https://github.com/InfluxCommunity/influxdb3-csharp/pull/80): Use net8.0 as a default target framework in Tests and Examples 89 | 90 | ## 0.4.0 [2023-12-08] 91 | 92 | ### Features 93 | 94 | 1. [#66](https://github.com/InfluxCommunity/influxdb3-csharp/pull/66): Default Tags for Writes 95 | 96 | ## 0.3.0 [2023-10-02] 97 | 98 | ### Features 99 | 100 | 1. [#36](https://github.com/InfluxCommunity/influxdb3-csharp/pull/46): Add client creation from connection string 101 | and environment variables. 102 | 1. [#52](https://github.com/InfluxCommunity/influxdb3-csharp/pull/52): Add structured query support 103 | 104 | ### Docs 105 | 106 | 1. [#52](https://github.com/InfluxCommunity/influxdb3-csharp/pull/52): Add downsampling example 107 | 108 | ## 0.2.0 [2023-08-11] 109 | 110 | ### Features 111 | 112 | 1. [#33](https://github.com/InfluxCommunity/influxdb3-csharp/pull/33): Add GZIP support 113 | 1. [#34](https://github.com/InfluxCommunity/influxdb3-csharp/pull/34): Add HTTP proxy and custom HTTP headers support 114 | 115 | ### Breaking Changes 116 | 117 | 1. [#35](https://github.com/InfluxCommunity/influxdb3-csharp/pull/35): Renamed config types and some options 118 | 119 | ## 0.1.0 [2023-06-09] 120 | 121 | - initial release of new client version 122 | -------------------------------------------------------------------------------- /Client.Test.Integration/Client.Test.Integration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 10 6 | enable 7 | 8 | false 9 | InfluxDB3.Client.Test.Integration 10 | InfluxDB3.Client.Test.Integration 11 | 12 | ../Keys/Key.snk 13 | true 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /Client.Test.Integration/IntegrationTest.cs: -------------------------------------------------------------------------------- 1 | 2 | 3 | using System; 4 | using System.Diagnostics; 5 | using NUnit.Framework; 6 | 7 | namespace InfluxDB3.Client.Test.Integration; 8 | 9 | public abstract class IntegrationTest 10 | { 11 | private static readonly TraceListener ConsoleOutListener = new TextWriterTraceListener(Console.Out); 12 | 13 | protected string Host { get; private set; } = Environment.GetEnvironmentVariable("TESTING_INFLUXDB_URL") ?? 14 | throw new InvalidOperationException( 15 | "TESTING_INFLUXDB_URL environment variable is not set."); 16 | 17 | protected string Token { get; private set; } = Environment.GetEnvironmentVariable("TESTING_INFLUXDB_TOKEN") ?? 18 | throw new InvalidOperationException( 19 | "TESTING_INFLUXDB_TOKEN environment variable is not set."); 20 | 21 | protected string Database { get; private set; } = Environment.GetEnvironmentVariable("TESTING_INFLUXDB_DATABASE") ?? 22 | throw new InvalidOperationException( 23 | "TESTING_INFLUXDB_DATABASE environment variable is not set."); 24 | 25 | [OneTimeSetUp] 26 | public void OneTimeSetUp() 27 | { 28 | if (!Trace.Listeners.Contains(ConsoleOutListener)) 29 | { 30 | Console.SetOut(TestContext.Progress); 31 | Trace.Listeners.Add(ConsoleOutListener); 32 | } 33 | } 34 | 35 | [OneTimeTearDownAttribute] 36 | public void OneTimeTearDownAttribute() 37 | { 38 | ConsoleOutListener.Dispose(); 39 | } 40 | } -------------------------------------------------------------------------------- /Client.Test.Integration/QueryWriteTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Grpc.Core; 6 | using InfluxDB3.Client.Config; 7 | using InfluxDB3.Client.Query; 8 | using InfluxDB3.Client.Write; 9 | using NUnit.Framework; 10 | using WriteOptions = InfluxDB3.Client.Config.WriteOptions; 11 | 12 | namespace InfluxDB3.Client.Test.Integration; 13 | 14 | public class QueryWriteTest : IntegrationTest 15 | { 16 | 17 | [Test] 18 | public async Task QueryWrite() 19 | { 20 | using var client = new InfluxDBClient(new ClientConfig 21 | { 22 | Host = Host, 23 | Token = Token, 24 | Database = Database 25 | }); 26 | 27 | const string measurement = "integration_test"; 28 | var testId = DateTime.UtcNow.Millisecond; 29 | await client.WriteRecordAsync($"{measurement},type=used value=123.0,testId={testId}"); 30 | 31 | var sql = $"SELECT value FROM {measurement} where \"testId\" = {testId}"; 32 | await foreach (var row in client.Query(sql)) 33 | { 34 | Assert.That(row, Has.Length.EqualTo(1)); 35 | Assert.That(row[0], Is.EqualTo(123.0)); 36 | } 37 | 38 | var results = await client.Query(sql).ToListAsync(); 39 | Assert.That(results, Has.Count.EqualTo(1)); 40 | 41 | var influxQL = $"select MEAN(value) from {measurement} where \"testId\" = {testId} group by time(1s) fill(none) order by time desc limit 1"; 42 | results = await client.Query(influxQL, queryType: QueryType.InfluxQL).ToListAsync(); 43 | Assert.That(results, Has.Count.EqualTo(1)); 44 | 45 | var points = await client.QueryPoints(sql).ToListAsync(); 46 | Assert.That(points, Has.Count.EqualTo(1)); 47 | Assert.That(points.First().GetField("value"), Is.EqualTo(123.0)); 48 | 49 | points = await client.QueryPoints($"SELECT * FROM {measurement} where \"testId\" = {testId}").ToListAsync(); 50 | Assert.That(points, Has.Count.EqualTo(1)); 51 | Assert.That(points.First().GetField("value"), Is.EqualTo(123.0)); 52 | Assert.That(points.First().GetTag("type"), Is.EqualTo("used")); 53 | } 54 | 55 | [Test] 56 | public void QueryNotAuthorized() 57 | { 58 | using var client = new InfluxDBClient(new ClientConfig 59 | { 60 | Host = Host, 61 | Database = Database 62 | }); 63 | 64 | var ae = Assert.ThrowsAsync(async () => 65 | { 66 | await foreach (var _ in client.Query("SELECT 1")) 67 | { 68 | } 69 | }); 70 | 71 | Assert.That(ae, Is.Not.Null); 72 | Assert.That(ae?.Message, Contains.Substring("Unauthenticated")); 73 | } 74 | 75 | [Test] 76 | public async Task WriteDontFailForEmptyData() 77 | { 78 | using var client = new InfluxDBClient(new ClientConfig 79 | { 80 | Host = Host, 81 | Database = Database, 82 | Token = Token 83 | }); 84 | 85 | await client.WritePointAsync(PointData.Measurement("cpu").SetTag("tag", "c")); 86 | } 87 | 88 | [Test] 89 | public async Task CanDisableCertificateValidation() 90 | { 91 | using var client = new InfluxDBClient(new ClientConfig 92 | { 93 | Host = Host, 94 | Database = Database, 95 | Token = Token, 96 | DisableServerCertificateValidation = true 97 | }); 98 | 99 | await client.WritePointAsync(PointData.Measurement("cpu").SetTag("tag", "c")); 100 | } 101 | 102 | [Test] 103 | public async Task WriteDataGzipped() 104 | { 105 | using var client = new InfluxDBClient(new ClientConfig 106 | { 107 | Host = Host, 108 | Database = Database, 109 | Token = Token, 110 | WriteOptions = new WriteOptions 111 | { 112 | GzipThreshold = 1 113 | } 114 | }); 115 | 116 | await client.WritePointAsync(PointData.Measurement("cpu").SetTag("tag", "c").SetField("user", 14.34)); 117 | } 118 | 119 | [Test] 120 | public async Task QueryWriteParameters() 121 | { 122 | using var client = new InfluxDBClient(new ClientConfig 123 | { 124 | Host = Host, 125 | Token = Token, 126 | Database = Database 127 | }); 128 | 129 | var testId = DateTime.UtcNow.Millisecond; 130 | await client.WriteRecordAsync($"integration_test,type=used value=1234.0,testId={testId}"); 131 | 132 | const string sql = "SELECT value FROM integration_test where \"testId\" = $testId"; 133 | await foreach (var row in client.Query(sql, namedParameters: new Dictionary 134 | { 135 | { "testId", testId }, 136 | })) 137 | { 138 | Assert.That(row, Has.Length.EqualTo(1)); 139 | Assert.That(row[0], Is.EqualTo(1234.0)); 140 | } 141 | } 142 | 143 | [Test] 144 | public void MaxReceiveMessageSize() 145 | { 146 | using var client = new InfluxDBClient(new ClientConfig 147 | { 148 | Host = Host, 149 | Token = Token, 150 | Database = Database, 151 | QueryOptions = new QueryOptions() 152 | { 153 | MaxReceiveMessageSize = 100 154 | } 155 | }); 156 | 157 | var ex = Assert.ThrowsAsync(async () => 158 | { 159 | await foreach (var _ in client.Query("SELECT value FROM integration_test")) 160 | { 161 | } 162 | }); 163 | Assert.That(ex?.StatusCode, Is.EqualTo(StatusCode.ResourceExhausted)); 164 | } 165 | 166 | [Test] 167 | public void GrpcDeadline() 168 | { 169 | using var client = new InfluxDBClient(new ClientConfig 170 | { 171 | Host = Host, 172 | Token = Token, 173 | Database = Database, 174 | QueryOptions = new QueryOptions() 175 | { 176 | Deadline = DateTime.UtcNow.AddMicroseconds(1) 177 | } 178 | }); 179 | 180 | var ex = Assert.ThrowsAsync(async () => 181 | { 182 | await foreach (var _ in client.Query("SELECT value FROM stat")) 183 | { 184 | } 185 | }); 186 | Assert.That(ex.StatusCode, Is.EqualTo(StatusCode.DeadlineExceeded)); 187 | } 188 | } -------------------------------------------------------------------------------- /Client.Test.Integration/WriteTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using InfluxDB3.Client.Config; 5 | using NUnit.Framework; 6 | 7 | namespace InfluxDB3.Client.Test.Integration; 8 | 9 | public class WriteTest : IntegrationTest 10 | { 11 | 12 | [Test] 13 | public async Task WriteWithError() 14 | { 15 | using var client = new InfluxDBClient(new ClientConfig 16 | { 17 | Host = Host, 18 | Token = Token, 19 | Database = Database, 20 | }); 21 | 22 | try 23 | { 24 | await client.WriteRecordAsync("vehicle,id=vwbus vel=0.0,distance=,status=\"STOPPED\""); 25 | } 26 | catch (Exception ex) 27 | { 28 | if (ex is InfluxDBApiException) 29 | { 30 | var iaex = (InfluxDBApiException)ex; 31 | Assert.Multiple(() => 32 | { 33 | Assert.That(iaex.Message, 34 | Does.Contain("Found trailing content") 35 | .Or.Contain("partial write of line protocol occurred") 36 | ); 37 | Assert.That(iaex.StatusCode.ToString(), Is.EqualTo("BadRequest")); 38 | Assert.That(iaex.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest)); 39 | }); 40 | } 41 | else 42 | { 43 | Assert.Fail($"Should catch InfluxDBApiException, but received {ex.GetType()}: {ex.Message}."); 44 | } 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /Client.Test/Client.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 10 6 | 7 | false 8 | InfluxDB3.Client.Test 9 | InfluxDB3.Client.Test 10 | 11 | ../Keys/Key.snk 12 | true 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | PreserveNewest 37 | 38 | 39 | PreserveNewest 40 | 41 | 42 | PreserveNewest 43 | 44 | 45 | PreserveNewest 46 | 47 | 48 | PreserveNewest 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /Client.Test/Config/QueryOptionsComparer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using InfluxDB3.Client.Config; 4 | 5 | namespace InfluxDB3.Client.Test.Config; 6 | 7 | internal class QueryOptionsComparer : IEqualityComparer 8 | { 9 | public bool Equals(QueryOptions x, QueryOptions y) 10 | { 11 | if (ReferenceEquals(x, y)) return true; 12 | if (x is null) return false; 13 | if (y is null) return false; 14 | if (x.GetType() != y.GetType()) return false; 15 | return Nullable.Equals(x.Deadline, y.Deadline) && x.MaxReceiveMessageSize == y.MaxReceiveMessageSize && 16 | x.MaxSendMessageSize == y.MaxSendMessageSize && Equals(x.CompressionProviders, y.CompressionProviders); 17 | } 18 | 19 | public int GetHashCode(QueryOptions obj) 20 | { 21 | return HashCode.Combine(obj.Deadline, obj.MaxReceiveMessageSize, obj.MaxSendMessageSize, 22 | obj.CompressionProviders); 23 | } 24 | } -------------------------------------------------------------------------------- /Client.Test/Config/QueryOptionsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Immutable; 3 | using Grpc.Net.Compression; 4 | using InfluxDB3.Client.Config; 5 | 6 | namespace InfluxDB3.Client.Test.Config; 7 | 8 | public class QueryOptionsTest 9 | { 10 | [TestFixture] 11 | public class QueryOptionsTests 12 | { 13 | [Test] 14 | public void Clone_CreatesExactCopy() 15 | { 16 | var options = new QueryOptions 17 | { 18 | Deadline = DateTime.Now, 19 | MaxReceiveMessageSize = 1000, 20 | MaxSendMessageSize = 1000, 21 | CompressionProviders = ImmutableArray.Empty 22 | }; 23 | 24 | var clone = (QueryOptions)options.Clone(); 25 | 26 | Assert.That(clone.Deadline, Is.EqualTo(options.Deadline)); 27 | Assert.That(clone.MaxReceiveMessageSize, Is.EqualTo(options.MaxReceiveMessageSize)); 28 | Assert.That(clone.MaxSendMessageSize, Is.EqualTo(options.MaxSendMessageSize)); 29 | Assert.That(clone.CompressionProviders, Is.EqualTo(options.CompressionProviders)); 30 | } 31 | 32 | [Test] 33 | public void Clone_CreatesIndependentCopy() 34 | { 35 | var options = new QueryOptions 36 | { 37 | Deadline = DateTime.Now, 38 | MaxReceiveMessageSize = 1000, 39 | MaxSendMessageSize = 1000, 40 | CompressionProviders = ImmutableArray.Empty 41 | }; 42 | 43 | var clone = (QueryOptions)options.Clone(); 44 | options.Deadline = DateTime.Now.AddDays(1); 45 | options.MaxReceiveMessageSize = 2000; 46 | options.MaxSendMessageSize = 2000; 47 | options.CompressionProviders = null; 48 | 49 | Assert.That(clone.Deadline, Is.Not.EqualTo(options.Deadline)); 50 | Assert.That(clone.MaxReceiveMessageSize, Is.Not.EqualTo(options.MaxReceiveMessageSize)); 51 | Assert.That(clone.MaxSendMessageSize, Is.Not.EqualTo(options.MaxSendMessageSize)); 52 | Assert.That(clone.CompressionProviders, Is.Not.EqualTo(options.CompressionProviders)); 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /Client.Test/InfluxDBApiExceptionTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Frozen; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using WireMock.RequestBuilders; 7 | using WireMock.ResponseBuilders; 8 | 9 | namespace InfluxDB3.Client.Test; 10 | 11 | public class InfluxDBApiExceptionTest : MockServerTest 12 | { 13 | 14 | private InfluxDBClient _client; 15 | 16 | [TearDown] 17 | public new void TearDown() 18 | { 19 | base.TearDown(); 20 | _client?.Dispose(); 21 | } 22 | 23 | [Test] 24 | public void NullValuesTest() 25 | { 26 | var exception = new InfluxDBApiException("Testing exception", null); 27 | Assert.That(exception.StatusCode.ToString(), Is.EqualTo("0")); 28 | var headers = exception.Headers; 29 | Assert.That(exception.Headers, Is.Null); 30 | } 31 | 32 | [Test] 33 | public async Task GeneratedInfluxDbException() 34 | { 35 | var requestId = Guid.NewGuid().ToString(); 36 | 37 | MockServer 38 | .Given(Request.Create().WithPath("/api/v2/write").UsingPost()) 39 | .RespondWith(Response.Create() 40 | .WithStatusCode(400) 41 | .WithBody("{ \"message\": \"just testing\", \"statusCode\": \"bad request\" }") 42 | .WithHeaders(new Dictionary() 43 | { 44 | {"Content-Type", "application/json"}, 45 | {"Trace-Id", "123456789ABCDEF0"}, 46 | {"Trace-Sampled", "false"}, 47 | {"X-Influxdb-Request-ID", requestId}, 48 | {"X-Influxdb-Build", "Cloud"} 49 | }) 50 | ); 51 | 52 | _client = new InfluxDBClient(MockServerUrl, 53 | "my-token", 54 | "my-org", 55 | "my-database"); 56 | try 57 | { 58 | await _client.WriteRecordAsync("wetbulb,location=prg val=20.1"); 59 | } 60 | catch (Exception ex) 61 | { 62 | if (ex is InfluxDBApiException) 63 | { 64 | var idbae = (InfluxDBApiException)ex; 65 | Assert.Multiple(() => 66 | { 67 | Assert.That(idbae.Message, Is.EqualTo("just testing")); 68 | Assert.That(idbae.StatusCode.ToString(), Is.EqualTo("BadRequest")); 69 | Assert.That(idbae.Headers.Count() == 7); 70 | }); 71 | var headersDix = idbae.Headers.ToFrozenDictionary(); 72 | Assert.Multiple(() => 73 | { 74 | Assert.That(headersDix["Trace-Id"].First(), Is.EqualTo("123456789ABCDEF0")); 75 | Assert.That(headersDix["Trace-Sampled"].First(), Is.EqualTo("false")); 76 | Assert.That(headersDix["X-Influxdb-Request-ID"].First(), Is.EqualTo(requestId)); 77 | Assert.That(headersDix["X-Influxdb-Build"].First(), Is.EqualTo("Cloud")); 78 | }); 79 | } 80 | else 81 | { 82 | Assert.Fail($"Should have thrown InfluxdbApiException. Not - {ex}"); 83 | } 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /Client.Test/InfluxDBClientHttpsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Threading.Tasks; 5 | using Grpc.Core; 6 | using InfluxDB3.Client.Config; 7 | using WireMock.RequestBuilders; 8 | using WireMock.ResponseBuilders; 9 | 10 | namespace InfluxDB3.Client.Test; 11 | 12 | public class InfluxDBClientHttpsTest : MockHttpsServerTest 13 | { 14 | private InfluxDBClient _client; 15 | 16 | [TearDown] 17 | public new void TearDown() 18 | { 19 | _client?.Dispose(); 20 | } 21 | 22 | [Test] 23 | public Task NonExistingCertificateFile() 24 | { 25 | var ae = Assert.ThrowsAsync(() => 26 | { 27 | _client = new InfluxDBClient(new ClientConfig 28 | { 29 | Host = MockHttpsServerUrl, 30 | Token = "my-token", 31 | Organization = "my-org", 32 | Database = "my-database", 33 | DisableServerCertificateValidation = false, 34 | DisableCertificateRevocationListCheck = true, 35 | SslRootsFilePath = "./not-existing.pem" 36 | }); 37 | return null; 38 | }); 39 | 40 | Assert.That(ae, Is.Not.Null); 41 | Assert.That(ae.Message, Does.Match("Certificate file '.*' not found")); 42 | return Task.CompletedTask; 43 | } 44 | 45 | [Test] 46 | public Task EmptyCertificateFile() 47 | { 48 | var ae = Assert.ThrowsAsync(() => 49 | { 50 | _client = new InfluxDBClient(new ClientConfig 51 | { 52 | Host = MockHttpsServerUrl, 53 | Token = "my-token", 54 | Organization = "my-org", 55 | Database = "my-database", 56 | DisableServerCertificateValidation = false, 57 | DisableCertificateRevocationListCheck = true, 58 | SslRootsFilePath = "./TestData/OtherCerts/empty.pem" 59 | }); 60 | return null; 61 | }); 62 | 63 | Assert.That(ae, Is.Not.Null); 64 | Assert.That(ae.Message, Does.Match("Certificate file '.*' is empty")); 65 | return Task.CompletedTask; 66 | } 67 | 68 | [Test] 69 | public Task InvalidCertificateFile() 70 | { 71 | try 72 | { 73 | _client = new InfluxDBClient(new ClientConfig 74 | { 75 | Host = MockHttpsServerUrl, 76 | Token = "my-token", 77 | Organization = "my-org", 78 | Database = "my-database", 79 | DisableServerCertificateValidation = false, 80 | DisableCertificateRevocationListCheck = true, 81 | SslRootsFilePath = "./TestData/OtherCerts/invalid.pem" 82 | }); 83 | } 84 | catch (Exception e) 85 | { 86 | Assert.That(e, Is.Not.Null); 87 | Assert.That(e.Message, Does.Match("Failed to import custom certificates")); 88 | } 89 | 90 | return Task.CompletedTask; 91 | } 92 | 93 | [Test] 94 | public async Task WriteWithValidSslRootCertificate() 95 | { 96 | _client = new InfluxDBClient(new ClientConfig 97 | { 98 | Host = MockHttpsServerUrl, 99 | Token = "my-token", 100 | Organization = "my-org", 101 | Database = "my-database", 102 | DisableServerCertificateValidation = false, 103 | DisableCertificateRevocationListCheck = true, 104 | SslRootsFilePath = "./TestData/ServerCert/rootCA.pem" 105 | }); 106 | 107 | await WriteData(); 108 | 109 | var requests = MockHttpsServer.LogEntries.ToList(); 110 | Assert.That(requests[0].RequestMessage.BodyData?.BodyAsString, Is.EqualTo("mem,tag=a field=1")); 111 | } 112 | 113 | [Test] 114 | public async Task WriteWithDisabledCertificates() 115 | { 116 | _client = new InfluxDBClient(new ClientConfig 117 | { 118 | Host = MockHttpsServerUrl, 119 | Token = "my-token", 120 | Organization = "my-org", 121 | Database = "my-database", 122 | DisableServerCertificateValidation = true, 123 | }); 124 | 125 | await WriteData(); 126 | 127 | var requests = MockHttpsServer.LogEntries.ToList(); 128 | Assert.That(requests[0].RequestMessage.BodyData?.BodyAsString, Is.EqualTo("mem,tag=a field=1")); 129 | } 130 | 131 | [Test] 132 | public Task WriteWithOtherSslRootCertificate() 133 | { 134 | _client = new InfluxDBClient(new ClientConfig 135 | { 136 | Host = MockHttpsServerUrl, 137 | Token = "my-token", 138 | Organization = "my-org", 139 | Database = "my-database", 140 | DisableServerCertificateValidation = false, 141 | DisableCertificateRevocationListCheck = true, 142 | SslRootsFilePath = "./TestData/OtherCerts/otherCA.pem" 143 | }); 144 | 145 | var ae = Assert.ThrowsAsync(async () => 146 | { 147 | await _client.WriteRecordAsync("mem,tag=a field=1"); 148 | }); 149 | 150 | Assert.That(ae, Is.Not.Null); 151 | Assert.That(ae.Message, Does.Contain("The SSL connection could not be established")); 152 | Assert.That(ae.InnerException?.Message, 153 | Does.Contain("The remote certificate was rejected by the provided RemoteCertificateValidationCallback")); 154 | return Task.CompletedTask; 155 | } 156 | 157 | [Test] 158 | public Task QueryWithValidSslRootCertificate() 159 | { 160 | _client = new InfluxDBClient(new ClientConfig 161 | { 162 | Host = MockHttpsServerUrl, 163 | Token = "my-token", 164 | Organization = "my-org", 165 | Database = "my-database", 166 | DisableServerCertificateValidation = false, 167 | DisableCertificateRevocationListCheck = true, 168 | SslRootsFilePath = "./TestData/ServerCert/rootCA.pem" 169 | }); 170 | 171 | var ae = Assert.ThrowsAsync(async () => { await QueryData(); }); 172 | 173 | // Verify: server successfully sent back the configured 404 status 174 | Assert.That(ae, Is.Not.Null); 175 | Assert.That(ae.Message, Does.Contain("Bad gRPC response. HTTP status code: 404")); 176 | 177 | // Verify: the request reached the server 178 | var requests = MockHttpsServer.LogEntries.ToList(); 179 | Assert.That(requests[0].RequestMessage.BodyData?.BodyAsString, Does.Contain("SELECT 1")); 180 | return Task.CompletedTask; 181 | } 182 | 183 | [Test] 184 | public Task QueryWithDisabledCertificates() 185 | { 186 | _client = new InfluxDBClient(new ClientConfig 187 | { 188 | Host = MockHttpsServerUrl, 189 | Token = "my-token", 190 | Organization = "my-org", 191 | Database = "my-database", 192 | DisableServerCertificateValidation = true, 193 | }); 194 | 195 | var ae = Assert.ThrowsAsync(async () => { await QueryData(); }); 196 | 197 | // Verify: server successfully sent back the configured 404 status 198 | Assert.That(ae, Is.Not.Null); 199 | Assert.That(ae.Message, Does.Contain("Bad gRPC response. HTTP status code: 404")); 200 | 201 | // Verify: the request reached the server 202 | var requests = MockHttpsServer.LogEntries.ToList(); 203 | Assert.That(requests[0].RequestMessage.BodyData?.BodyAsString, Does.Contain("SELECT 1")); 204 | return Task.CompletedTask; 205 | } 206 | 207 | [Test] 208 | public Task QueryWithOtherSslRootCertificate() 209 | { 210 | _client = new InfluxDBClient(new ClientConfig 211 | { 212 | Host = MockHttpsServerUrl, 213 | Token = "my-token", 214 | Organization = "my-org", 215 | Database = "my-database", 216 | DisableServerCertificateValidation = false, 217 | DisableCertificateRevocationListCheck = true, 218 | SslRootsFilePath = "./TestData/OtherCerts/otherCA.pem" 219 | }); 220 | 221 | var ae = Assert.ThrowsAsync(async () => { await QueryData(); }); 222 | 223 | // Verify: the SSL connection was not established 224 | Assert.That(ae, Is.Not.Null); 225 | Assert.That(ae.Message, Does.Contain("The SSL connection could not be established")); 226 | 227 | // Verify: the request did not reach the server 228 | var requests = MockHttpsServer.LogEntries.ToList(); 229 | Assert.That(requests, Is.Empty); 230 | return Task.CompletedTask; 231 | } 232 | 233 | private async Task WriteData() 234 | { 235 | MockHttpsServer 236 | .Given(Request.Create().WithPath("/api/v2/write").UsingPost()) 237 | .RespondWith(Response.Create().WithStatusCode(204)); 238 | 239 | await _client.WriteRecordAsync("mem,tag=a field=1"); 240 | } 241 | 242 | 243 | private async Task QueryData() 244 | { 245 | // Setup mock server: return 404 for simplicity, so we don't have to implement a valid response. 246 | MockHttpsServer 247 | .Given(Request.Create().WithPath("/arrow.flight.protocol.FlightService/DoGet").UsingPost()) 248 | .RespondWith(Response.Create() 249 | .WithStatusCode(404) 250 | ); 251 | 252 | // Query data. 253 | var query = "SELECT 1"; 254 | await _client.Query(query).ToListAsync(); 255 | } 256 | } -------------------------------------------------------------------------------- /Client.Test/InfluxDBClientQueryTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Apache.Arrow; 6 | using InfluxDB3.Client.Internal; 7 | using InfluxDB3.Client.Query; 8 | using Moq; 9 | 10 | namespace InfluxDB3.Client.Test; 11 | 12 | public class InfluxDBClientQueryTest : MockServerTest 13 | { 14 | private InfluxDBClient _client; 15 | 16 | [TearDown] 17 | public new void TearDown() 18 | { 19 | _client?.Dispose(); 20 | } 21 | 22 | [Test] 23 | public void AlreadyDisposed() 24 | { 25 | _client = new InfluxDBClient(MockServerUrl); 26 | _client.Dispose(); 27 | var ae = Assert.ThrowsAsync(async () => 28 | { 29 | await foreach (var unused in _client.Query("SELECT 1")) 30 | { 31 | } 32 | }); 33 | 34 | Assert.That(ae, Is.Not.Null); 35 | Assert.That(ae.Message, Is.EqualTo($"Cannot access a disposed object.{Environment.NewLine}Object name: 'InfluxDBClient'.")); 36 | } 37 | 38 | [Test] 39 | public void NotSpecifiedDatabase() 40 | { 41 | _client = new InfluxDBClient(MockServerUrl); 42 | var ae = Assert.Throws(() => { _client.QueryBatches("SELECT 1"); }); 43 | 44 | Assert.That(ae, Is.Not.Null); 45 | Assert.That(ae.Message, Is.EqualTo("Please specify the 'database' as a method parameter or use default configuration at 'ClientConfig.Database'.")); 46 | } 47 | 48 | [Test] 49 | public async Task PassNamedParametersToFlightClient() 50 | { 51 | // 52 | // Mock the FlightSqlClient 53 | // 54 | var mockFlightSqlClient = new Mock(); 55 | mockFlightSqlClient 56 | .Setup(m => m.Execute(It.IsAny(), It.IsAny(), It.IsAny(), 57 | It.IsAny>(), It.IsAny>())) 58 | .Returns(new List().ToAsyncEnumerable()); 59 | 60 | // 61 | // Setup the client with the mocked FlightSqlClient 62 | // 63 | _client = new InfluxDBClient(MockServerUrl); 64 | _client.FlightSqlClient.Dispose(); 65 | _client.FlightSqlClient = mockFlightSqlClient.Object; 66 | 67 | const string query = "select * from cpu where location = $location and core_count = $core-count and production = $production and max_frequency > $max-frequency"; 68 | const QueryType queryType = QueryType.SQL; 69 | var namedParameters = new Dictionary 70 | { 71 | { "location", "us" }, 72 | { "core-count", 4 }, 73 | { "production", true }, 74 | { "max-frequency", 3.5 } 75 | }; 76 | 77 | _ = await _client.QueryPoints(query, database: "my-db", queryType: queryType, namedParameters: namedParameters) 78 | .ToListAsync(); 79 | mockFlightSqlClient.Verify(m => m.Execute(query, "my-db", queryType, namedParameters, new Dictionary()), Times.Exactly(1)); 80 | } 81 | 82 | [Test] 83 | public void NotSupportedQueryParameterType() 84 | { 85 | _client = new InfluxDBClient(MockServerUrl); 86 | var ae = Assert.ThrowsAsync(async () => 87 | { 88 | _ = await _client 89 | .Query("select * from cpu where location = $location", database: "my-db", 90 | queryType: QueryType.SQL, namedParameters: new Dictionary 91 | { 92 | { "location", DateTime.UtcNow } 93 | }) 94 | .ToListAsync(); 95 | }); 96 | 97 | Assert.That(ae, Is.Not.Null); 98 | Assert.That(ae.Message, 99 | Is.EqualTo( 100 | "The parameter 'location' has unsupported type 'System.DateTime'. The supported types are 'string', 'bool', 'int' and 'float'.")); 101 | } 102 | 103 | [Test] 104 | public async Task PassHeadersToFlightClient() 105 | { 106 | // 107 | // Mock the FlightSqlClient 108 | // 109 | var mockFlightSqlClient = new Mock(); 110 | mockFlightSqlClient 111 | .Setup(m => m.Execute(It.IsAny(), It.IsAny(), It.IsAny(), 112 | It.IsAny>(), It.IsAny>())) 113 | .Returns(new List().ToAsyncEnumerable()); 114 | 115 | // 116 | // Setup the client with the mocked FlightSqlClient 117 | // 118 | _client = new InfluxDBClient(MockServerUrl); 119 | _client.FlightSqlClient.Dispose(); 120 | _client.FlightSqlClient = mockFlightSqlClient.Object; 121 | 122 | const string query = "select * from cpu"; 123 | const QueryType queryType = QueryType.SQL; 124 | 125 | var headers = new Dictionary{ 126 | { 127 | "X-Tracing-Id", "123" 128 | }}; 129 | _ = await _client.QueryPoints(query, database: "my-db", queryType: queryType, headers: headers) 130 | .ToListAsync(); 131 | mockFlightSqlClient.Verify(m => m.Execute(query, "my-db", queryType, new Dictionary(), headers), Times.Exactly(1)); 132 | } 133 | } -------------------------------------------------------------------------------- /Client.Test/InfluxDBClientTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | // ReSharper disable ObjectCreationAsStatement 5 | // ReSharper disable AssignNullToNotNullAttribute 6 | 7 | namespace InfluxDB3.Client.Test; 8 | 9 | public class InfluxDBClientTest 10 | { 11 | [Test] 12 | public void Create() 13 | { 14 | using var client = new InfluxDBClient("http://localhost:8086", token: "my-token", organization: "my-org", database: "my-database"); 15 | 16 | Assert.That(client, Is.Not.Null); 17 | } 18 | 19 | [Test] 20 | public void CreateFromConnectionString() 21 | { 22 | using var client = new InfluxDBClient("http://localhost:8086?token=my-token&org=my-org&database=my-db"); 23 | 24 | Assert.That(client, Is.Not.Null); 25 | } 26 | 27 | [Test] 28 | public void CreateFromEnv() 29 | { 30 | var env = new Dictionary 31 | { 32 | {"INFLUX_HOST", "http://localhost:8086"}, 33 | {"INFLUX_TOKEN", "my-token"}, 34 | {"INFLUX_ORG", "my-org"}, 35 | {"INFLUX_DATABASE", "my-database"}, 36 | }; 37 | SetEnv(env); 38 | 39 | using var client = new InfluxDBClient(); 40 | 41 | Assert.That(client, Is.Not.Null); 42 | } 43 | 44 | [Test] 45 | public void RequiredHost() 46 | { 47 | var ae = Assert.Throws(() => { new InfluxDBClient(host: null, token: ""); }); 48 | 49 | Assert.That(ae, Is.Not.Null); 50 | Assert.That(ae.Message, Is.EqualTo("The URL of the InfluxDB server has to be defined")); 51 | } 52 | 53 | private static void SetEnv(IDictionary dict) 54 | { 55 | foreach (var entry in dict) 56 | { 57 | Environment.SetEnvironmentVariable(entry.Key, entry.Value, EnvironmentVariableTarget.Process); 58 | } 59 | } 60 | 61 | [TearDown] 62 | public void Cleanup() 63 | { 64 | var env = Environment.GetEnvironmentVariables(EnvironmentVariableTarget.Process); 65 | foreach (var key in env.Keys) 66 | { 67 | if (((string)key).StartsWith("INFLUX_")) 68 | { 69 | Environment.SetEnvironmentVariable((string)key, null, EnvironmentVariableTarget.Process); 70 | } 71 | } 72 | } 73 | } -------------------------------------------------------------------------------- /Client.Test/Internal/ArgumentsDurationTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using InfluxDB3.Client.Internal; 3 | 4 | namespace InfluxDB3.Client.Test.Internal 5 | { 6 | [TestFixture] 7 | public class ArgumentsDurationTest 8 | { 9 | [SetUp] 10 | public void SetUp() 11 | { 12 | } 13 | 14 | [Test] 15 | public void Literals() 16 | { 17 | Arguments.CheckDuration("1s", "duration"); 18 | Arguments.CheckDuration("10d", "duration"); 19 | Arguments.CheckDuration("1h15m", "duration"); 20 | Arguments.CheckDuration("5w", "duration"); 21 | Arguments.CheckDuration("1mo5d", "duration"); 22 | Arguments.CheckDuration("-1mo5d", "duration"); 23 | Arguments.CheckDuration("inf", "duration"); 24 | Arguments.CheckDuration("-inf", "duration"); 25 | } 26 | 27 | [Test] 28 | public void LiteralNull() 29 | { 30 | try 31 | { 32 | Arguments.CheckDuration(null, "duration"); 33 | 34 | Assert.Fail(); 35 | } 36 | catch (ArgumentException e) 37 | { 38 | Assert.That(e.Message.Equals("Expecting a duration string for duration. But got: ")); 39 | } 40 | } 41 | 42 | [Test] 43 | public void LiteralEmpty() 44 | { 45 | try 46 | { 47 | Arguments.CheckDuration("", "duration"); 48 | 49 | Assert.Fail(); 50 | } 51 | catch (ArgumentException e) 52 | { 53 | Assert.That(e.Message.Equals("Expecting a duration string for duration. But got: ")); 54 | } 55 | } 56 | 57 | [Test] 58 | public void LiteralNotDuration() 59 | { 60 | try 61 | { 62 | Arguments.CheckDuration("x", "duration"); 63 | 64 | Assert.Fail(); 65 | } 66 | catch (ArgumentException e) 67 | { 68 | Assert.That(e.Message.Equals("Expecting a duration string for duration. But got: x")); 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /Client.Test/Internal/ArgumentsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using InfluxDB3.Client.Internal; 3 | 4 | namespace InfluxDB3.Client.Test.Internal 5 | { 6 | [TestFixture] 7 | public class ArgumentsTest 8 | { 9 | [SetUp] 10 | public void SetUp() 11 | { 12 | } 13 | 14 | [Test] 15 | public void CheckNonNullString() 16 | { 17 | Arguments.CheckNotNull("valid", "property"); 18 | } 19 | 20 | [Test] 21 | public void CheckNonNullStringNull() 22 | { 23 | try 24 | { 25 | Arguments.CheckNotNull(null!, "property"); 26 | 27 | Assert.Fail(); 28 | } 29 | catch (NullReferenceException e) 30 | { 31 | Assert.That(e.Message.Equals("Expecting a not null reference for property")); 32 | } 33 | } 34 | 35 | [Test] 36 | public void CheckNonEmptyString() 37 | { 38 | Arguments.CheckNonEmptyString("valid", "property"); 39 | } 40 | 41 | [Test] 42 | public void CheckNonEmptyStringNull() 43 | { 44 | try 45 | { 46 | Arguments.CheckNonEmptyString(null!, "property"); 47 | 48 | Assert.Fail(); 49 | } 50 | catch (ArgumentException e) 51 | { 52 | Assert.That(e.Message.Equals("Expecting a non-empty string for property")); 53 | } 54 | } 55 | 56 | [Test] 57 | public void CheckNonEmptyStringEmpty() 58 | { 59 | try 60 | { 61 | Arguments.CheckNonEmptyString("", "property"); 62 | 63 | Assert.Fail(); 64 | } 65 | catch (ArgumentException e) 66 | { 67 | Assert.That(e.Message.Equals("Expecting a non-empty string for property")); 68 | } 69 | } 70 | 71 | [Test] 72 | public void CheckPositiveNumber() 73 | { 74 | Arguments.CheckPositiveNumber(10, "property"); 75 | } 76 | 77 | [Test] 78 | public void CheckPositiveNumberZero() 79 | { 80 | try 81 | { 82 | Arguments.CheckPositiveNumber(0, "property"); 83 | 84 | Assert.Fail(); 85 | } 86 | catch (ArgumentException e) 87 | { 88 | Assert.That(e.Message.Equals("Expecting a positive number for property")); 89 | } 90 | } 91 | 92 | [Test] 93 | public void CheckPositiveNumberNegative() 94 | { 95 | try 96 | { 97 | Arguments.CheckPositiveNumber(-12, "property"); 98 | 99 | Assert.Fail(); 100 | } 101 | catch (ArgumentException e) 102 | { 103 | Assert.That(e.Message.Equals("Expecting a positive number for property")); 104 | } 105 | } 106 | 107 | [Test] 108 | public void CheckNotNegativeNumber() 109 | { 110 | Arguments.CheckNotNegativeNumber(0, "valid"); 111 | } 112 | 113 | [Test] 114 | public void CheckNotNegativeNumberNegative() 115 | { 116 | try 117 | { 118 | Arguments.CheckNotNegativeNumber(-12, "property"); 119 | 120 | Assert.Fail(); 121 | } 122 | catch (ArgumentException e) 123 | { 124 | Assert.That(e.Message.Equals("Expecting a positive or zero number for property")); 125 | } 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /Client.Test/Internal/AssemblyHelperTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using InfluxDB3.Client.Internal; 3 | 4 | namespace InfluxDB3.Client.Test.Internal 5 | { 6 | [TestFixture] 7 | public class AssemblyHelperTest 8 | { 9 | [Test] 10 | public void GetAssemblyVersion() 11 | { 12 | var version = AssemblyHelper.GetVersion(); 13 | Assert.Multiple(() => 14 | { 15 | Assert.That(Version.Parse(version).Major, Is.EqualTo(1)); 16 | Assert.That(Version.Parse(version).Minor, Is.GreaterThanOrEqualTo(0)); 17 | Assert.That(Version.Parse(version).Build, Is.EqualTo(0)); 18 | Assert.That(Version.Parse(version).Revision, Is.EqualTo(0)); 19 | }); 20 | } 21 | 22 | [Test] 23 | public void GetUserAgent() 24 | { 25 | var version = AssemblyHelper.GetVersion(); 26 | var userAgent = AssemblyHelper.GetUserAgent(); 27 | Assert.That(userAgent, Is.EqualTo($"influxdb3-csharp/{version}")); 28 | } 29 | } 30 | } -------------------------------------------------------------------------------- /Client.Test/Internal/FlightSqlClientTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.Immutable; 4 | using System.Text; 5 | using Grpc.Net.Compression; 6 | using InfluxDB3.Client.Config; 7 | using InfluxDB3.Client.Internal; 8 | using InfluxDB3.Client.Query; 9 | 10 | namespace InfluxDB3.Client.Test.Internal; 11 | 12 | public class FlightSqlClientTest : MockServerTest 13 | { 14 | private IFlightSqlClient _flightSqlClient; 15 | 16 | [SetUp] 17 | public void SetUp() 18 | { 19 | var config = new ClientConfig 20 | { 21 | Host = MockServerUrl, 22 | Timeout = TimeSpan.FromSeconds(45) 23 | }; 24 | 25 | _flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateAndConfigureHttpClient(config)); 26 | } 27 | 28 | [TearDown] 29 | public new void TearDown() 30 | { 31 | _flightSqlClient.Dispose(); 32 | } 33 | 34 | [Test] 35 | public void PrepareFlightTicket() 36 | { 37 | var prepareFlightTicket = _flightSqlClient.PrepareFlightTicket( 38 | "select * from mem", 39 | "my-db", 40 | QueryType.InfluxQL, new Dictionary()); 41 | 42 | const string ticket = 43 | @"{""database"":""my-db"",""sql_query"":""select * from mem"",""query_type"":""influxql""}"; 44 | Assert.Multiple(() => 45 | { 46 | Assert.That(prepareFlightTicket, Is.Not.Null); 47 | var actual = Encoding.UTF8.GetString(prepareFlightTicket.Ticket.ToByteArray()); 48 | Console.WriteLine(actual); 49 | Assert.That(actual, Is.EqualTo(ticket)); 50 | }); 51 | } 52 | 53 | [Test] 54 | public void PrepareFlightTicketNamedParameters() 55 | { 56 | var prepareFlightTicket = _flightSqlClient.PrepareFlightTicket( 57 | "select * from cpu where location = $location and production = $production and count = $count and temperature = $temperature", 58 | "my-db", 59 | QueryType.SQL, 60 | new Dictionary 61 | { 62 | { "location", "us" }, 63 | { "production", true }, 64 | { "count", 10 }, 65 | { "temperature", 23.5 } 66 | }); 67 | 68 | var ticket = "{\"database\":\"my-db\"," + 69 | "\"sql_query\":\"select * from cpu where location = $location and production = $production and count = $count and temperature = $temperature\"," + 70 | "\"query_type\":\"sql\"," + 71 | "\"params\": {\"location\":\"us\",\"production\":true,\"count\":10,\"temperature\":23.5}}"; 72 | Assert.Multiple(() => 73 | { 74 | Assert.That(prepareFlightTicket, Is.Not.Null); 75 | Assert.That(Encoding.UTF8.GetString(prepareFlightTicket.Ticket.ToByteArray()), Is.EqualTo(ticket)); 76 | }); 77 | } 78 | 79 | [Test] 80 | public void HeadersMetadataFromRequest() 81 | { 82 | var prepareHeadersMetadata = 83 | _flightSqlClient.PrepareHeadersMetadata(new Dictionary { { "X-Tracing-Id", "987" } }); 84 | 85 | Assert.Multiple(() => 86 | { 87 | Assert.That(prepareHeadersMetadata, Is.Not.Null); 88 | Assert.That(prepareHeadersMetadata, Has.Count.EqualTo(2)); 89 | Assert.That(prepareHeadersMetadata[0].Key, Is.EqualTo("user-agent")); 90 | Assert.That(prepareHeadersMetadata[0].Value, Is.EqualTo(AssemblyHelper.GetUserAgent())); 91 | Assert.That(prepareHeadersMetadata[1].Key, Is.EqualTo("x-tracing-id")); 92 | Assert.That(prepareHeadersMetadata[1].Value, Is.EqualTo("987")); 93 | }); 94 | } 95 | 96 | [Test] 97 | public void HeadersMetadataFromConfig() 98 | { 99 | _flightSqlClient.Dispose(); 100 | 101 | var config = new ClientConfig 102 | { 103 | Host = MockServerUrl, 104 | Timeout = TimeSpan.FromSeconds(45), 105 | Headers = new Dictionary 106 | { 107 | { "X-Global-Tracing-Id", "123" } 108 | } 109 | }; 110 | 111 | _flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateAndConfigureHttpClient(config)); 112 | 113 | var prepareHeadersMetadata = 114 | _flightSqlClient.PrepareHeadersMetadata(new Dictionary()); 115 | 116 | Assert.Multiple(() => 117 | { 118 | Assert.That(prepareHeadersMetadata, Is.Not.Null); 119 | Assert.That(prepareHeadersMetadata, Has.Count.EqualTo(2)); 120 | Assert.That(prepareHeadersMetadata[0].Key, Is.EqualTo("user-agent")); 121 | Assert.That(prepareHeadersMetadata[0].Value, Is.EqualTo(AssemblyHelper.GetUserAgent())); 122 | Assert.That(prepareHeadersMetadata[1].Key, Is.EqualTo("x-global-tracing-id")); 123 | Assert.That(prepareHeadersMetadata[1].Value, Is.EqualTo("123")); 124 | }); 125 | } 126 | 127 | [Test] 128 | public void HeadersMetadataFromRequestArePreferred() 129 | { 130 | _flightSqlClient.Dispose(); 131 | 132 | var config = new ClientConfig 133 | { 134 | Host = MockServerUrl, 135 | Timeout = TimeSpan.FromSeconds(45), 136 | Headers = new Dictionary 137 | { 138 | { "X-Tracing-Id", "ABC" } 139 | } 140 | }; 141 | 142 | _flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateAndConfigureHttpClient(config)); 143 | 144 | var prepareHeadersMetadata = 145 | _flightSqlClient.PrepareHeadersMetadata(new Dictionary { { "X-Tracing-Id", "258" } }); 146 | 147 | Assert.Multiple(() => 148 | { 149 | Assert.That(prepareHeadersMetadata, Is.Not.Null); 150 | Assert.That(prepareHeadersMetadata, Has.Count.EqualTo(2)); 151 | Assert.That(prepareHeadersMetadata[0].Key, Is.EqualTo("user-agent")); 152 | Assert.That(prepareHeadersMetadata[0].Value, Is.EqualTo(AssemblyHelper.GetUserAgent())); 153 | Assert.That(prepareHeadersMetadata[1].Key, Is.EqualTo("x-tracing-id")); 154 | Assert.That(prepareHeadersMetadata[1].Value, Is.EqualTo("258")); 155 | }); 156 | } 157 | 158 | [Test] 159 | public void UserAgentHeaderNotChanged() 160 | { 161 | _flightSqlClient.Dispose(); 162 | 163 | var config = new ClientConfig 164 | { 165 | Host = MockServerUrl, 166 | Timeout = TimeSpan.FromSeconds(45), 167 | Headers = new Dictionary 168 | { 169 | { "User-Agent", "some/user-agent" } 170 | } 171 | }; 172 | 173 | _flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateAndConfigureHttpClient(config)); 174 | 175 | var prepareHeadersMetadata = 176 | _flightSqlClient.PrepareHeadersMetadata(new Dictionary { { "user-agent", "another/user-agent" } }); 177 | 178 | Assert.Multiple(() => 179 | { 180 | Assert.That(prepareHeadersMetadata, Is.Not.Null); 181 | Assert.That(prepareHeadersMetadata, Has.Count.EqualTo(1)); 182 | Assert.That(prepareHeadersMetadata[0].Key, Is.EqualTo("user-agent")); 183 | Assert.That(prepareHeadersMetadata[0].Value, Is.EqualTo(AssemblyHelper.GetUserAgent())); 184 | }); 185 | } 186 | 187 | [Test] 188 | public void TestGrpcCallOptions() 189 | { 190 | var config = new ClientConfig 191 | { 192 | Host = MockServerUrl, 193 | QueryOptions = { 194 | Deadline = DateTime.Now.AddMinutes(5), 195 | MaxReceiveMessageSize = 8_388_608, 196 | MaxSendMessageSize = 10000, 197 | CompressionProviders = ImmutableArray.Empty 198 | } 199 | }; 200 | 201 | Assert.DoesNotThrow(() => 202 | _flightSqlClient = new FlightSqlClient(config, InfluxDBClient.CreateAndConfigureHttpClient(config))); 203 | } 204 | 205 | } -------------------------------------------------------------------------------- /Client.Test/Internal/FlightSqlExtensionsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Apache.Arrow; 4 | using Apache.Arrow.Types; 5 | using InfluxDB3.Client.Internal; 6 | using Array = Apache.Arrow.Array; 7 | 8 | namespace InfluxDB3.Client.Test.Internal; 9 | 10 | [TestFixture] 11 | public class FlightSqlExtensionsTest 12 | { 13 | [Test] 14 | public void BooleanArray() 15 | { 16 | var array = new BooleanArray.Builder().Append(true).Build(); 17 | 18 | Assert.That(array.Length, Is.EqualTo(1)); 19 | Assert.That(array.GetObjectValue(0), Is.True); 20 | } 21 | 22 | [Test] 23 | public void UInt8ArrayArray() 24 | { 25 | var array = new UInt8Array.Builder().Append(5).Build(); 26 | 27 | Assert.That(array.Length, Is.EqualTo(1)); 28 | Assert.That(array.GetObjectValue(0), Is.EqualTo(5)); 29 | } 30 | 31 | [Test] 32 | public void Int8ArrayArray() 33 | { 34 | var array = new Int8Array.Builder().Append(13).Build(); 35 | 36 | Assert.That(array.Length, Is.EqualTo(1)); 37 | Assert.That(array.GetObjectValue(0), Is.EqualTo(13)); 38 | } 39 | 40 | [Test] 41 | public void UInt16ArrayArray() 42 | { 43 | var array = new UInt16Array.Builder().Append(23).Build(); 44 | 45 | Assert.That(array.Length, Is.EqualTo(1)); 46 | Assert.That(array.GetObjectValue(0), Is.EqualTo(23)); 47 | } 48 | 49 | [Test] 50 | public void Int16ArrayArray() 51 | { 52 | var array = new Int16Array.Builder().Append(25).Build(); 53 | 54 | Assert.That(array.Length, Is.EqualTo(1)); 55 | Assert.That(array.GetObjectValue(0), Is.EqualTo(25)); 56 | } 57 | 58 | [Test] 59 | public void UInt32Array() 60 | { 61 | var array = new UInt32Array.Builder().Append(35).Build(); 62 | 63 | Assert.That(array.Length, Is.EqualTo(1)); 64 | Assert.That(array.GetObjectValue(0), Is.EqualTo(35)); 65 | } 66 | 67 | [Test] 68 | public void Int32Array() 69 | { 70 | var array = new Int32Array.Builder().Append(37).Build(); 71 | 72 | Assert.That(array.Length, Is.EqualTo(1)); 73 | Assert.That(array.GetObjectValue(0), Is.EqualTo(37)); 74 | } 75 | 76 | [Test] 77 | public void UInt64Array() 78 | { 79 | var array = new UInt64Array.Builder().Append(55).Build(); 80 | 81 | Assert.That(array.Length, Is.EqualTo(1)); 82 | Assert.That(array.GetObjectValue(0), Is.EqualTo(55)); 83 | } 84 | 85 | [Test] 86 | public void Int64Array() 87 | { 88 | var array = new Int64Array.Builder().Append(552).Build(); 89 | 90 | Assert.That(array.Length, Is.EqualTo(1)); 91 | Assert.That(array.GetObjectValue(0), Is.EqualTo(552)); 92 | } 93 | 94 | [Test] 95 | public void FloatArray() 96 | { 97 | var array = new FloatArray.Builder().Append(552.34F).Build(); 98 | 99 | Assert.That(array.Length, Is.EqualTo(1)); 100 | Assert.That(array.GetObjectValue(0), Is.EqualTo(552.34F)); 101 | } 102 | 103 | [Test] 104 | public void DoubleArray() 105 | { 106 | var array = new DoubleArray.Builder().Append(552.322).Build(); 107 | 108 | Assert.That(array.Length, Is.EqualTo(1)); 109 | Assert.That(array.GetObjectValue(0), Is.EqualTo(552.322)); 110 | } 111 | 112 | [Test] 113 | public void StringArray() 114 | { 115 | var array = new StringArray.Builder().Append("abc").Build(); 116 | 117 | Assert.That(array.Length, Is.EqualTo(1)); 118 | Assert.That(array.GetObjectValue(0), Is.EqualTo("abc")); 119 | } 120 | 121 | [Test] 122 | public void BinaryArray() 123 | { 124 | var array = new BinaryArray.Builder().Append(1).Build(); 125 | 126 | Assert.That(array.Length, Is.EqualTo(1)); 127 | Assert.That(array.GetObjectValue(0), Is.EqualTo(new object[] { 1 })); 128 | } 129 | 130 | [Test] 131 | public void TimestampArray() 132 | { 133 | var dateTimeOffset = DateTimeOffset.Now; 134 | var array = new TimestampArray.Builder(TimeUnit.Nanosecond, TimeZoneInfo.Utc).Append(dateTimeOffset).Build(); 135 | 136 | Assert.That(array.Length, Is.EqualTo(1)); 137 | Assert.That(array.GetObjectValue(0), Is.EqualTo(dateTimeOffset)); 138 | } 139 | 140 | [Test] 141 | public void Date64Array() 142 | { 143 | var array = new Date64Array.Builder().Append(DateTime.Now.Date).Build(); 144 | 145 | Assert.That(array.Length, Is.EqualTo(1)); 146 | Assert.That(array.GetObjectValue(0), Is.EqualTo(DateTime.Now.Date)); 147 | } 148 | 149 | [Test] 150 | public void Date32Array() 151 | { 152 | var array = new Date32Array.Builder().Append(DateTime.Now.Date).Build(); 153 | 154 | Assert.That(array.Length, Is.EqualTo(1)); 155 | Assert.That(array.GetObjectValue(0), Is.EqualTo(DateTime.Now.Date)); 156 | } 157 | 158 | [Test] 159 | public void Time32Array() 160 | { 161 | var array = new Time32Array.Builder().Append(431).Build(); 162 | 163 | Assert.That(array.Length, Is.EqualTo(1)); 164 | Assert.That(array.GetObjectValue(0), Is.EqualTo(431)); 165 | } 166 | 167 | [Test] 168 | public void Time64Array() 169 | { 170 | var array = new Time64Array.Builder().Append(1431).Build(); 171 | 172 | Assert.That(array.Length, Is.EqualTo(1)); 173 | Assert.That(array.GetObjectValue(0), Is.EqualTo(1431)); 174 | } 175 | 176 | [Test] 177 | public void Decimal128Array() 178 | { 179 | var array = new Decimal128Array.Builder(new Decimal128Type(8, 2)).Append(1431).Build(); 180 | 181 | Assert.That(array.Length, Is.EqualTo(1)); 182 | Assert.That(array.GetObjectValue(0), Is.EqualTo(1431)); 183 | } 184 | 185 | [Test] 186 | public void Decimal256Array() 187 | { 188 | var array = new Decimal256Array.Builder(new Decimal256Type(8, 2)).Append(1551).Build(); 189 | 190 | Assert.That(array.Length, Is.EqualTo(1)); 191 | Assert.That(array.GetObjectValue(0), Is.EqualTo(1551)); 192 | } 193 | 194 | [Test] 195 | public void NotSupported() 196 | { 197 | var array = new StructArray( 198 | new StructType( 199 | new List 200 | { 201 | new Field.Builder().Name("Strings").DataType(StringType.Default).Nullable(false).Build() 202 | }), 203 | 0, 204 | new List(), 205 | new ArrowBuffer.BitmapBuilder().Append(false).Build(), 0 206 | ); 207 | 208 | var ae = Assert.Throws(() => { array.GetObjectValue(0); }); 209 | 210 | Assert.That(ae, Is.Not.Null); 211 | Assert.That(ae.Message, Is.EqualTo("The datatype Apache.Arrow.Types.StructType is not supported.")); 212 | } 213 | } -------------------------------------------------------------------------------- /Client.Test/Internal/RecordBatchConverterTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Apache.Arrow; 3 | using Apache.Arrow.Types; 4 | using InfluxDB3.Client.Internal; 5 | using InfluxDB3.Client.Write; 6 | 7 | namespace InfluxDB3.Client.Test.Internal; 8 | 9 | public class RecordBatchConverterTest 10 | { 11 | [Test] 12 | public void ConvertToPointDataValue() 13 | { 14 | Schema schema; 15 | RecordBatch recordBatch; 16 | PointDataValues point; 17 | 18 | var stringField = new Field("measurement", StringType.Default, true); 19 | var stringArray = new StringArray.Builder().Append("host").Build(); 20 | schema = new Schema(new[] { stringField, }, null); 21 | recordBatch = new RecordBatch(schema, new[] { stringArray }, 1); 22 | point = RecordBatchConverter.ConvertToPointDataValue(recordBatch, 0); 23 | Assert.That(point.GetMeasurement(), Is.EqualTo("host")); 24 | 25 | Assert.Multiple(() => 26 | { 27 | var now = new DateTimeOffset(); 28 | 29 | var timeField = new Field("time", TimestampType.Default, true); 30 | var timeArray = new TimestampArray.Builder().Append(now).Build(); 31 | schema = new Schema(new[] { timeField, }, null); 32 | recordBatch = new RecordBatch(schema, new[] { timeArray }, 1); 33 | 34 | point = RecordBatchConverter.ConvertToPointDataValue(recordBatch, 0); 35 | Assert.That(point.GetTimestamp(), Is.EqualTo(TimestampConverter.GetNanoTime(now.UtcDateTime))); 36 | 37 | timeField = new Field("test", TimestampType.Default, true); 38 | schema = new Schema(new[] { timeField, }, null); 39 | recordBatch = new RecordBatch(schema, new[] { timeArray }, 1); 40 | point = RecordBatchConverter.ConvertToPointDataValue(recordBatch, 0); 41 | Assert.That(point.GetField("test"), Is.EqualTo(now)); 42 | }); 43 | } 44 | } -------------------------------------------------------------------------------- /Client.Test/Internal/RestClientTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net.Http; 4 | using System.Reflection; 5 | using System.Threading.Tasks; 6 | using InfluxDB3.Client.Config; 7 | using InfluxDB3.Client.Internal; 8 | using WireMock.RequestBuilders; 9 | using WireMock.ResponseBuilders; 10 | 11 | namespace InfluxDB3.Client.Test.Internal; 12 | 13 | public class RestClientTest : MockServerTest 14 | { 15 | private RestClient _client; 16 | private HttpClient _httpClient; 17 | 18 | [TearDown] 19 | public new void TearDown() 20 | { 21 | _httpClient?.Dispose(); 22 | } 23 | 24 | [Test] 25 | public async Task Authorization() 26 | { 27 | CreateAndConfigureRestClient(new ClientConfig 28 | { 29 | Host = MockServerUrl, 30 | Token = "my-token" 31 | }); 32 | await DoRequest(); 33 | 34 | var requests = MockServer.LogEntries.ToList(); 35 | 36 | Assert.That(requests[0].RequestMessage.Headers?["Authorization"][0], Is.EqualTo("Token my-token")); 37 | } 38 | 39 | [Test] 40 | public async Task AuthorizationCustomScheme() 41 | { 42 | CreateAndConfigureRestClient(new ClientConfig 43 | { 44 | Host = MockServerUrl, 45 | Token = "my-token", 46 | AuthScheme = "my-scheme" 47 | }); 48 | await DoRequest(); 49 | 50 | var requests = MockServer.LogEntries.ToList(); 51 | 52 | Assert.That(requests[0].RequestMessage.Headers?["Authorization"][0], Is.EqualTo("my-scheme my-token")); 53 | } 54 | 55 | [Test] 56 | public async Task UserAgent() 57 | { 58 | CreateAndConfigureRestClient(new ClientConfig 59 | { 60 | Host = MockServerUrl, 61 | }); 62 | await DoRequest(); 63 | 64 | var requests = MockServer.LogEntries.ToList(); 65 | 66 | Assert.That(requests[0].RequestMessage.Headers?["User-Agent"][0], Does.StartWith("influxdb3-csharp/1.")); 67 | Assert.That(requests[0].RequestMessage.Headers?["User-Agent"][0], Does.EndWith(".0.0")); 68 | } 69 | 70 | [Test] 71 | public async Task Url() 72 | { 73 | CreateAndConfigureRestClient(new ClientConfig 74 | { 75 | Host = MockServerUrl, 76 | }); 77 | await DoRequest(); 78 | 79 | var requests = MockServer.LogEntries.ToList(); 80 | Assert.That(requests[0].RequestMessage.Url, Is.EqualTo($"{MockServerUrl}/api")); 81 | } 82 | 83 | [Test] 84 | public async Task UrlWithBackslash() 85 | { 86 | CreateAndConfigureRestClient(new ClientConfig 87 | { 88 | Host = $"{MockServerUrl}/", 89 | }); 90 | await DoRequest(); 91 | 92 | var requests = MockServer.LogEntries.ToList(); 93 | Assert.That(requests[0].RequestMessage.Url, Is.EqualTo($"{MockServerUrl}/api")); 94 | } 95 | 96 | private async Task DoRequest() 97 | { 98 | MockServer 99 | .Given(Request.Create().WithPath("/api").UsingGet()) 100 | .RespondWith(Response.Create().WithStatusCode(204)); 101 | 102 | await _client.Request("api", HttpMethod.Get); 103 | } 104 | 105 | [Test] 106 | public void ErrorHeader() 107 | { 108 | CreateAndConfigureRestClient(new ClientConfig 109 | { 110 | Host = MockServerUrl, 111 | }); 112 | 113 | MockServer 114 | .Given(Request.Create().WithPath("/api").UsingPost()) 115 | .RespondWith(Response.Create() 116 | .WithHeader("X-Influx-Error", "line protocol poorly formed and no points were written") 117 | .WithStatusCode(400)); 118 | 119 | var ae = Assert.ThrowsAsync(async () => 120 | { 121 | await _client.Request("api", HttpMethod.Post); 122 | }); 123 | 124 | Assert.Multiple(() => 125 | { 126 | Assert.That(ae, Is.Not.Null); 127 | Assert.That(ae.HttpResponseMessage, Is.Not.Null); 128 | Assert.That(ae.Message, Is.EqualTo("line protocol poorly formed and no points were written")); 129 | }); 130 | } 131 | 132 | [Test] 133 | public void ErrorBody() 134 | { 135 | CreateAndConfigureRestClient(new ClientConfig 136 | { 137 | Host = MockServerUrl, 138 | }); 139 | 140 | MockServer 141 | .Given(Request.Create().WithPath("/api").UsingPost()) 142 | .RespondWith(Response.Create() 143 | .WithBody("no token was sent and they are required") 144 | .WithStatusCode(403)); 145 | 146 | var ae = Assert.ThrowsAsync(async () => 147 | { 148 | await _client.Request("api", HttpMethod.Post); 149 | }); 150 | 151 | Assert.Multiple(() => 152 | { 153 | Assert.That(ae, Is.Not.Null); 154 | Assert.That(ae.HttpResponseMessage, Is.Not.Null); 155 | Assert.That(ae.Message, Is.EqualTo("no token was sent and they are required")); 156 | }); 157 | } 158 | 159 | [Test] 160 | public void ErrorJsonBodyCloud() 161 | { 162 | CreateAndConfigureRestClient(new ClientConfig 163 | { 164 | Host = MockServerUrl, 165 | }); 166 | 167 | MockServer 168 | .Given(Request.Create().WithPath("/api").UsingPost()) 169 | .RespondWith(Response.Create() 170 | .WithHeader("X-Influx-Error", "not used") 171 | .WithBody("{\"message\":\"token does not have sufficient permissions\"}") 172 | .WithStatusCode(401)); 173 | 174 | var ae = Assert.ThrowsAsync(async () => 175 | { 176 | await _client.Request("api", HttpMethod.Post); 177 | }); 178 | 179 | Assert.Multiple(() => 180 | { 181 | Assert.That(ae, Is.Not.Null); 182 | Assert.That(ae.HttpResponseMessage, Is.Not.Null); 183 | Assert.That(ae.Message, Is.EqualTo("token does not have sufficient permissions")); 184 | }); 185 | } 186 | 187 | [Test] 188 | public void ErrorJsonBodyEdgeWithData() 189 | { 190 | CreateAndConfigureRestClient(new ClientConfig 191 | { 192 | Host = MockServerUrl, 193 | }); 194 | 195 | MockServer 196 | .Given(Request.Create().WithPath("/api").UsingPost()) 197 | .RespondWith(Response.Create() 198 | .WithHeader("X-Influx-Error", "not used") 199 | .WithBody("{\"error\":\"parsing failed\", \"data\":{\"error_message\":\"invalid field value in line protocol for field 'value' on line 0\"}}") 200 | .WithStatusCode(401)); 201 | 202 | var ae = Assert.ThrowsAsync(async () => 203 | { 204 | await _client.Request("api", HttpMethod.Post); 205 | }); 206 | 207 | Assert.Multiple(() => 208 | { 209 | Assert.That(ae, Is.Not.Null); 210 | Assert.That(ae.HttpResponseMessage, Is.Not.Null); 211 | Assert.That(ae.Message, Is.EqualTo("invalid field value in line protocol for field 'value' on line 0")); 212 | }); 213 | } 214 | 215 | [Test] 216 | public void ErrorJsonBodyEdgeWithoutData() 217 | { 218 | CreateAndConfigureRestClient(new ClientConfig 219 | { 220 | Host = MockServerUrl, 221 | }); 222 | 223 | MockServer 224 | .Given(Request.Create().WithPath("/api").UsingPost()) 225 | .RespondWith(Response.Create() 226 | .WithHeader("X-Influx-Error", "not used") 227 | .WithBody("{\"error\":\"token does not have sufficient permissions\"}") 228 | .WithStatusCode(401)); 229 | 230 | var ae = Assert.ThrowsAsync(async () => 231 | { 232 | await _client.Request("api", HttpMethod.Post); 233 | }); 234 | 235 | Assert.Multiple(() => 236 | { 237 | Assert.That(ae, Is.Not.Null); 238 | Assert.That(ae.HttpResponseMessage, Is.Not.Null); 239 | Assert.That(ae.Message, Is.EqualTo("token does not have sufficient permissions")); 240 | }); 241 | } 242 | 243 | 244 | [Test] 245 | public void ErrorReason() 246 | { 247 | CreateAndConfigureRestClient(new ClientConfig 248 | { 249 | Host = MockServerUrl, 250 | }); 251 | 252 | MockServer 253 | .Given(Request.Create().WithPath("/api").UsingPost()) 254 | .RespondWith(Response.Create() 255 | .WithStatusCode(409)); 256 | 257 | var ae = Assert.ThrowsAsync(async () => 258 | { 259 | await _client.Request("api", HttpMethod.Post); 260 | }); 261 | 262 | Assert.Multiple(() => 263 | { 264 | Assert.That(ae, Is.Not.Null); 265 | Assert.That(ae.HttpResponseMessage, Is.Not.Null); 266 | Assert.That(ae.Message, Is.EqualTo("Conflict")); 267 | }); 268 | } 269 | 270 | [Test] 271 | public void AllowHttpRedirects() 272 | { 273 | CreateAndConfigureRestClient(new ClientConfig 274 | { 275 | Host = MockServerUrl, 276 | AllowHttpRedirects = true 277 | }); 278 | 279 | Assert.That(_client, Is.Not.Null); 280 | } 281 | 282 | [Test] 283 | public void Timeout() 284 | { 285 | CreateAndConfigureRestClient(new ClientConfig 286 | { 287 | Host = MockServerUrl, 288 | Timeout = TimeSpan.FromSeconds(45) 289 | }); 290 | 291 | var httpClient = GetDeclaredField(_client.GetType(), _client, "_httpClient"); 292 | Assert.That(httpClient.Timeout, Is.EqualTo(TimeSpan.FromSeconds(45))); 293 | } 294 | 295 | private void CreateAndConfigureRestClient(ClientConfig config) 296 | { 297 | _httpClient = InfluxDBClient.CreateAndConfigureHttpClient(config); 298 | _client = new RestClient(config, _httpClient); 299 | } 300 | 301 | private static T GetDeclaredField(IReflect type, object instance, string fieldName) 302 | { 303 | const BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic 304 | | BindingFlags.Static | BindingFlags.DeclaredOnly; 305 | var field = type.GetField(fieldName, bindFlags); 306 | return (T)field?.GetValue(instance); 307 | } 308 | } -------------------------------------------------------------------------------- /Client.Test/Internal/ServerCertificateCustomValidationsTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Http; 3 | using System.Net.Security; 4 | using System.Security.Cryptography.X509Certificates; 5 | using InfluxDB3.Client.Internal; 6 | using Moq; 7 | 8 | namespace InfluxDB3.Client.Test.Internal; 9 | 10 | public class ServerCertificateCustomValidationsTest 11 | { 12 | private HttpRequestMessage _message; 13 | private X509Certificate2 _certificate; 14 | private X509Chain _chain; 15 | private Func _callback; 16 | private X509Certificate2Collection _certificates; 17 | 18 | 19 | [SetUp] 20 | public void SetUp() 21 | { 22 | _message = new Mock().Object; 23 | _certificate = new X509Certificate2("./TestData/ServerCert/server.pem"); 24 | _certificates = new X509Certificate2Collection(_certificate); 25 | _chain = new X509Chain(); 26 | _chain.Build(_certificate); 27 | 28 | _callback = ServerCertificateCustomValidations.CreateCustomCertificatesValidationCallback( 29 | "./TestData/ServerCert/rootCA.pem", true); 30 | } 31 | 32 | [TearDown] 33 | public void Cleanup() 34 | { 35 | _certificate.Dispose(); 36 | _chain.Dispose(); 37 | } 38 | 39 | [Test] 40 | public void NoErrors() 41 | { 42 | var isValid = _callback(_message, _certificate, _chain, SslPolicyErrors.None); 43 | Assert.That(isValid, Is.True); 44 | } 45 | 46 | [Test] 47 | public void Error_RemoteCertificateNotAvailable() 48 | { 49 | var isValid = _callback(_message, _certificate, _chain, SslPolicyErrors.RemoteCertificateNotAvailable); 50 | Assert.That(isValid, Is.False); 51 | } 52 | 53 | [Test] 54 | public void Error_RemoteCertificateNameMismatch() 55 | { 56 | var isValid = _callback(_message, _certificate, _chain, SslPolicyErrors.RemoteCertificateNameMismatch); 57 | Assert.That(isValid, Is.False); 58 | } 59 | 60 | [Test] 61 | public void MissingCertificate() 62 | { 63 | var isValid = _callback(_message, null, _chain, SslPolicyErrors.RemoteCertificateChainErrors); 64 | Assert.That(isValid, Is.False); 65 | } 66 | 67 | [Test] 68 | public void MissingChain() 69 | { 70 | var isValid = _callback(_message, _certificate, null, SslPolicyErrors.RemoteCertificateChainErrors); 71 | Assert.That(isValid, Is.False); 72 | } 73 | 74 | [Test] 75 | public void ContainsCertificateWithThumbprint_NullThumbprint() 76 | { 77 | var contains = ServerCertificateCustomValidations.ContainsCertificateWithThumbprint(_certificates, null); 78 | Assert.That(contains, Is.False); 79 | } 80 | 81 | [Test] 82 | public void ContainsCertificateWithThumbprint_NotFoundThumbprint() 83 | { 84 | var contains = 85 | ServerCertificateCustomValidations.ContainsCertificateWithThumbprint(_certificates, "not-found-thumbprint"); 86 | Assert.That(contains, Is.False); 87 | } 88 | 89 | [Test] 90 | public void ContainsCertificateWithThumbprint_EmptyCollection() 91 | { 92 | var contains = 93 | ServerCertificateCustomValidations.ContainsCertificateWithThumbprint(new X509Certificate2Collection(), 94 | "thumbprint"); 95 | Assert.That(contains, Is.False); 96 | } 97 | 98 | [Test] 99 | public void IsRootCertificateSelfSigned_SelfSignedCertificate() 100 | { 101 | var chain = new X509Chain(); 102 | chain.ChainPolicy.ExtraStore.Add(new X509Certificate2("./TestData/ServerCert/rootCA.pem")); 103 | chain.Build(_certificate); 104 | var isSelfSigned = ServerCertificateCustomValidations.IsRootCertificateSelfSigned(chain); 105 | Assert.That(isSelfSigned, Is.True); 106 | } 107 | 108 | [Test] 109 | public void IsRootCertificateSelfSigned_ServerPemWithoutRootCA() 110 | { 111 | var partialChain = new X509Chain(); 112 | partialChain.Build(new X509Certificate2("./TestData/ServerCert/server.pem")); 113 | var isSelfSigned = ServerCertificateCustomValidations.IsRootCertificateSelfSigned(partialChain); 114 | Assert.That(isSelfSigned, Is.False); 115 | } 116 | 117 | [Test] 118 | public void IsRootCertificateSelfSigned_EmptyChain() 119 | { 120 | var isSelfSigned = 121 | ServerCertificateCustomValidations.IsRootCertificateSelfSigned(new X509Chain()); 122 | Assert.That(isSelfSigned, Is.False); 123 | } 124 | } -------------------------------------------------------------------------------- /Client.Test/Internal/TimestampConverterTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using InfluxDB3.Client.Internal; 4 | 5 | namespace InfluxDB3.Client.Test.Internal; 6 | 7 | public class TimestampConverterTest 8 | { 9 | private static readonly DateTime EpochStart = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 10 | 11 | [Test] 12 | public void GetNanoTime() 13 | { 14 | var now = DateTime.Now; 15 | 16 | var localTime = DateTime.SpecifyKind(now, DateTimeKind.Local); 17 | var timestamp = TimestampConverter.GetNanoTime(localTime); 18 | BigInteger nanoTime = now.ToUniversalTime().Subtract(EpochStart).Ticks * 100; 19 | Assert.That(nanoTime, Is.EqualTo(timestamp)); 20 | 21 | var unspecifiedTime = DateTime.SpecifyKind(now, DateTimeKind.Unspecified); 22 | timestamp = TimestampConverter.GetNanoTime(unspecifiedTime); 23 | nanoTime = DateTime.SpecifyKind(now, DateTimeKind.Utc).Subtract(EpochStart).Ticks * 100; 24 | Assert.That(nanoTime, Is.EqualTo(timestamp)); 25 | } 26 | } -------------------------------------------------------------------------------- /Client.Test/Internal/TypeCastTest.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using Apache.Arrow; 3 | using Apache.Arrow.Types; 4 | using InfluxDB3.Client.Internal; 5 | 6 | namespace InfluxDB3.Client.Test.Internal; 7 | 8 | public class TypeCastTest 9 | { 10 | [Test] 11 | public void IsNumber() 12 | { 13 | Assert.Multiple(() => 14 | { 15 | Assert.That(TypeCasting.IsNumber(1), Is.True); 16 | Assert.That(TypeCasting.IsNumber(1.2), Is.True); 17 | Assert.That(TypeCasting.IsNumber(-1.2), Is.True); 18 | }); 19 | 20 | Assert.Multiple(() => 21 | { 22 | Assert.That(TypeCasting.IsNumber('1'), Is.False); 23 | Assert.That(TypeCasting.IsNumber('a'), Is.False); 24 | Assert.That(TypeCasting.IsNumber(true), Is.False); 25 | Assert.That(TypeCasting.IsNumber(null), Is.False); 26 | }); 27 | } 28 | 29 | [Test] 30 | public void GetMappedValue() 31 | { 32 | // If pass the correct value type to GetMappedValue() it will return the value with a correct type 33 | // If pass the incorrect value type to GetMappedValue() it will NOT throws any error but return the passed value 34 | const string fieldName = "test"; 35 | 36 | var field = GenerateIntField(fieldName); 37 | Assert.Multiple(() => 38 | { 39 | Assert.That(TypeCasting.GetMappedValue(field, 1)!, Is.EqualTo(1)); 40 | Assert.That(TypeCasting.GetMappedValue(field, "a")!, Is.EqualTo("a")); 41 | }); 42 | 43 | field = GenerateUIntField(fieldName); 44 | Assert.Multiple(() => 45 | { 46 | Assert.That(TypeCasting.GetMappedValue(field, 1)!, Is.EqualTo(1)); 47 | Assert.That(TypeCasting.GetMappedValue(field, -1)!, Is.EqualTo(-1)); 48 | Assert.That(TypeCasting.GetMappedValue(field, "a")!, Is.EqualTo("a")); 49 | }); 50 | 51 | field = GenerateDoubleField(fieldName); 52 | Assert.Multiple(() => 53 | { 54 | Assert.That(TypeCasting.GetMappedValue(field, 1.2)!, Is.EqualTo(1.2)); 55 | Assert.That(TypeCasting.GetMappedValue(field, "a")!, Is.EqualTo("a")); 56 | }); 57 | 58 | field = GenerateBooleanField(fieldName); 59 | Assert.Multiple(() => 60 | { 61 | Assert.That(TypeCasting.GetMappedValue(field, true)!, Is.EqualTo(true)); 62 | Assert.That(TypeCasting.GetMappedValue(field, 10)!, Is.EqualTo(10)); 63 | }); 64 | 65 | field = GenerateStringField(fieldName); 66 | Assert.Multiple(() => 67 | { 68 | Assert.That(TypeCasting.GetMappedValue(field, "a")!, Is.EqualTo("a")); 69 | Assert.That(TypeCasting.GetMappedValue(field, 10)!, Is.EqualTo(10)); 70 | }); 71 | 72 | 73 | field = GenerateIntFieldTestTypeMeta(fieldName); 74 | Assert.That(TypeCasting.GetMappedValue(field, 1)!, Is.EqualTo(1)); 75 | } 76 | 77 | private static Field GenerateIntFieldTestTypeMeta(string fieldName) 78 | { 79 | var meta = new Dictionary 80 | { 81 | { 82 | "iox::column::type", "iox::column_type::field::test" 83 | } 84 | }; 85 | return new Field(fieldName, Int64Type.Default, true, meta); 86 | } 87 | 88 | private static Field GenerateIntField(string fieldName) 89 | { 90 | var meta = new Dictionary 91 | { 92 | { 93 | "iox::column::type", "iox::column_type::field::integer" 94 | } 95 | }; 96 | return new Field(fieldName, Int64Type.Default, true, meta); 97 | } 98 | 99 | private static Field GenerateUIntField(string fieldName) 100 | { 101 | var meta = new Dictionary 102 | { 103 | { 104 | "iox::column::type", "iox::column_type::field::uinteger" 105 | } 106 | }; 107 | return new Field(fieldName, UInt64Type.Default, true, meta); 108 | } 109 | 110 | private static Field GenerateDoubleField(string fieldName) 111 | { 112 | var meta = new Dictionary 113 | { 114 | { 115 | "iox::column::type", "iox::column_type::field::float" 116 | } 117 | }; 118 | return new Field(fieldName, DoubleType.Default, true, meta); 119 | } 120 | 121 | private static Field GenerateBooleanField(string fieldName) 122 | { 123 | var meta = new Dictionary 124 | { 125 | { 126 | "iox::column::type", "iox::column_type::field::boolean" 127 | } 128 | }; 129 | return new Field(fieldName, BooleanType.Default, true, meta); 130 | } 131 | 132 | private static Field GenerateStringField(string fieldName) 133 | { 134 | var meta = new Dictionary 135 | { 136 | { 137 | "iox::column::type", "iox::column_type::field::string" 138 | } 139 | }; 140 | return new Field(fieldName, StringType.Default, true, meta); 141 | } 142 | } -------------------------------------------------------------------------------- /Client.Test/MockHttpsServerTest.cs: -------------------------------------------------------------------------------- 1 | using WireMock.Server; 2 | using WireMock.Settings; 3 | using WireMock.Types; 4 | 5 | namespace InfluxDB3.Client.Test; 6 | 7 | public class MockHttpsServerTest 8 | { 9 | internal WireMockServer MockHttpsServer; 10 | internal string MockHttpsServerUrl; 11 | 12 | [OneTimeSetUp] 13 | public void OneTimeSetUp() 14 | { 15 | if (MockHttpsServer is { IsStarted: true }) 16 | { 17 | return; 18 | } 19 | 20 | MockHttpsServer = WireMockServer.Start(new WireMockServerSettings 21 | { 22 | UseSSL = true, 23 | HostingScheme = HostingScheme.Https, 24 | CertificateSettings = new WireMockCertificateSettings 25 | { 26 | X509CertificateFilePath = "./TestData/ServerCert/server.p12", 27 | X509CertificatePassword = "password12" 28 | } 29 | }); 30 | 31 | MockHttpsServerUrl = MockHttpsServer.Urls[0]; 32 | } 33 | 34 | [OneTimeTearDown] 35 | public void OneTimeTearDownAttribute() 36 | { 37 | MockHttpsServer.Dispose(); 38 | } 39 | 40 | [TearDown] 41 | public void TearDown() 42 | { 43 | MockHttpsServer.Reset(); 44 | } 45 | 46 | [OneTimeTearDown] 47 | public void OneTimeTearDown() 48 | { 49 | MockHttpsServer?.Stop(); 50 | } 51 | } -------------------------------------------------------------------------------- /Client.Test/MockServerTest.cs: -------------------------------------------------------------------------------- 1 | using WireMock.Server; 2 | using WireMock.Settings; 3 | 4 | namespace InfluxDB3.Client.Test; 5 | 6 | public class MockServerTest 7 | { 8 | internal WireMockServer MockServer, MockProxy; 9 | internal string MockServerUrl, MockProxyUrl; 10 | 11 | [OneTimeSetUp] 12 | public void OneTimeSetUp() 13 | { 14 | if (MockServer is { IsStarted: true }) 15 | { 16 | return; 17 | } 18 | 19 | MockServer = WireMockServer.Start(new WireMockServerSettings 20 | { 21 | UseSSL = false 22 | }); 23 | 24 | MockServerUrl = MockServer.Urls[0]; 25 | 26 | MockProxy = WireMockServer.Start(new WireMockServerSettings 27 | { 28 | UseSSL = false, 29 | Port = 8888, 30 | ProxyAndRecordSettings = new ProxyAndRecordSettings 31 | { 32 | Url = MockServerUrl 33 | } 34 | }); 35 | 36 | MockProxyUrl = MockProxy.Urls[0]; 37 | } 38 | 39 | [OneTimeTearDown] 40 | public void OneTimeTearDownAttribute() 41 | { 42 | MockServer.Dispose(); 43 | MockProxy.Dispose(); 44 | } 45 | 46 | [TearDown] 47 | public void TearDown() 48 | { 49 | MockServer.Reset(); 50 | MockProxy.Reset(); 51 | } 52 | 53 | [OneTimeTearDown] 54 | public void OneTimeTearDown() 55 | { 56 | MockServer?.Stop(); 57 | MockProxy?.Stop(); 58 | } 59 | } -------------------------------------------------------------------------------- /Client.Test/TestData/OtherCerts/empty.pem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-csharp/71d1c5919f9e1fa1cc798f8e8b397fb9c6de6e62/Client.Test/TestData/OtherCerts/empty.pem -------------------------------------------------------------------------------- /Client.Test/TestData/OtherCerts/invalid.pem: -------------------------------------------------------------------------------- 1 | invalid pem file with 2 | -----BEGIN CERTIFICATE----- 3 | invalid certificate 4 | -----END CERTIFICATE----- 5 | -------------------------------------------------------------------------------- /Client.Test/TestData/OtherCerts/otherCA.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDFTCCAf2gAwIBAgIUGiAZNoQhUnhsAAQkNwjP6pBIsQgwDQYJKoZIhvcNAQEL 3 | BQAwGjEYMBYGA1UEAwwPT3RoZXJUZXN0Um9vdENBMB4XDTI1MDMwNTE1MTkyN1oX 4 | DTM1MDMwMzE1MTkyN1owGjEYMBYGA1UEAwwPT3RoZXJUZXN0Um9vdENBMIIBIjAN 5 | BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2bf1RXmI/DhgMJEe/PLEsCCu64rh 6 | H1bP7INBzHc8p571LaZVegFwjh3Q9A6KOXZqeY/Uge8x1uBH/W+t/OUQB9ccCJ5V 7 | Lzt03Lwv8iwyB3VsOlrZf/fcsuO18tTnsqqEvS+lKU/hAyXQq3O3XL/KHHjA+k6K 8 | lqiZkUmHoEbevEva2GN5pAeF8m8ihzr8G7x0wEkwZwCcu6NCKZCJrmHXX8wu1c2F 9 | KAAYRzwpGjS3k3kEhzFkVKpiBVIUXkLUWL9aljnM3+T8g4g+yXkna/w6vo6OdY1r 10 | mPIG7Ma+OiyNWeT0BQPk2AAORNiaj/vPVAr8lwhTIfMklNhpFWUAufoxlwIDAQAB 11 | o1MwUTAdBgNVHQ4EFgQUYXcUTkFydrhT+6uRAD+7/fy35ZAwHwYDVR0jBBgwFoAU 12 | YXcUTkFydrhT+6uRAD+7/fy35ZAwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B 13 | AQsFAAOCAQEACkOv+hh3juvI97ShN9eBFIWbqe1opPuda5kElE6/LPpgnYR1F+/G 14 | 2n6StGycnDPeo3rmcRMbn9351nBA1oqxe9bt3q7aHEaGcKeNzQhuaUKHJ2dI4EDs 15 | RLl61fl2SEaHQmUPqdoEVYMpf3DTkAbd3XimfQwxWBlLGtUyr1g73JfQua6GaKdT 16 | crL77MuJf+h4mx3xX5dtoey58xgkCXqN/oLNNotNnlWv9XNBKcpV6zydYNDpf3k1 17 | 3TfrG7TMwLO1MCjr7LLPdQSjBx22rKtBn0ognHcWYrx5JhoahVi09QjGaptNfzal 18 | xxGF9Z/1PwmK7GlE4+pNcoQsRvpxZs2ncw== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /Client.Test/TestData/ServerCert/generate-self-signed-cert.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | PKCS12_PASSWORD="password12" 3 | 4 | echo "### Generate root CA key..." 5 | openssl genrsa -out rootCA.key 2048 6 | 7 | echo "### Generate root CA certificate..." 8 | openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 3650 -out rootCA.pem -subj "/CN=MyTestRootCA" 9 | 10 | echo "### Generate server key..." 11 | openssl genrsa -out server.key 2048 12 | 13 | echo "### Generate server certificate request..." 14 | openssl req -new -nodes -newkey rsa:4096 \ 15 | -keyout server.key \ 16 | -out server.csr \ 17 | -subj "/CN=localhost" \ 18 | -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" \ 19 | -addext "extendedKeyUsage=serverAuth" 20 | 21 | echo "### Sign the request with the root CA..." 22 | openssl x509 -req -in server.csr -CA rootCA.pem -CAkey rootCA.key -CAcreateserial -out server.pem -days 365 -sha256 -copy_extensions copy 23 | 24 | echo "### Add server certificates into an encrypted PKCS#12 file for WireMock..." 25 | openssl pkcs12 -export -out server.p12 -inkey server.key -in server.pem -certfile rootCA.pem -password pass:${PKCS12_PASSWORD} 26 | 27 | echo 28 | echo "### Print rootCA.pem..." 29 | openssl x509 -in rootCA.pem -text -noout 30 | 31 | echo 32 | echo "### Print server.pem..." 33 | openssl x509 -in server.pem -text -noout 34 | 35 | 36 | ## Check certificate on MacOS: 37 | # security verify-cert -r rootCA.pem -c server.pem -p ssl -v 38 | 39 | echo 40 | echo "### Cleanup..." 41 | rm server.csr 42 | rm rootCA.srl 43 | -------------------------------------------------------------------------------- /Client.Test/TestData/ServerCert/rootCA.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQD1imOcyGY759yv 3 | 7PjTtXqD9I5XGBjrL19kcXJDIUow7yZuQaWC3vAT++LtFnq6S4GbFL1OVxF6VF17 4 | sogF8EFjbQUHR5j3Hd46mGE3MKX8RFSDG50xi+JeEXw7LEbp6tLIBJ0jjk5FPkiF 5 | C2jJZfbPxy6/B2z2MtuqAvqu6StSm5PsHNVMqpnenGiTqT26GFyot87oyzBX1pKU 6 | FrhwpecXRS7QS76MupCzrDZV+OPkTqza8TbJUu3yOpgI1UGENhAlL/nbgke5v7h9 7 | wPesY2h5050A5qHwjQaM/s8phBTPCXYXVWA3Aa/HittipOXKGXxLWMPLh6GhWI+w 8 | n2lF7x25AgMBAAECggEAASM9Qp1BSTVFKT6Clggg+dc9lcs4VryfNBN8SUtwV2lm 9 | pu7TofEMMTeC52c2tduPZx6YB7+KoFbsOo+K83+xU3d0y47jt0wyetiFmMnqdvcB 10 | 4jw6RMZZmLOuVoYMi3bP6ucHxShA/Hw+10XEgqxf6207lJCBZLAH79HenSyNshU6 11 | oTIDtwOekRMCmk2PTVUrA+frGaBPCNFX4XkP71vZj0I7uC/etq2aq/3pArKDKRIk 12 | zrOwFJ96RihGd2SvVEQ+azYHrZs6q5O53dct2wdDWNpxw1qKsyjtEmNltFMFgqJD 13 | jvbs+Y04HWvxn/3RG9aLTOF+df6FKFpcgkiQcQSx5QKBgQD8mtPzbSJ/AuSo3/Q+ 14 | pqgVPIRHzhjeJ3sJy2Z0wNjU+vvmhG4dqIjqWGQ85Q/ZTuZe4VMd2ijFlezeKmJ+ 15 | RJDFdyqGgk067TO24BS5/mUov46P1rzENMj3IyRgsa8LGRBXGt6UX0//k/TZchuA 16 | o58paUY3rHx2V06h29D9N2yGzQKBgQD410EeC21PrqzEHfdXmvIy36HW0lpr4WFP 17 | hoNSYXMu77xWp1kuRtxqNNoenUdqyvBv/y+Vvhc7rf+NjLYFbcjhwT6T/KxoOa+s 18 | flBkHhtFfSdnSoiVL3TIyzZgCIY5K+Yoh/beUzWzM79Ha+K4BUIZs5Tk0qJ/6h91 19 | p7lzXS46nQKBgQDNhQbXMj7zyZ3SzEuDQcLVbGRPq55N+R9A681TRqfkOMQEXFDf 20 | LoUe7YW4icGoU2pZXchrLFkp0P5kD2YNR6nmDzt5LsC5Jc6ChrQ2U4VobtoFq3fv 21 | xuaPNHdfeJFrXRwPUpwvaZDD51Q6Kn233ugbIDzyBRNKkWQ3ionxy0swLQKBgQDG 22 | 4mQtOzzW3uk+piS2ZCUH/C1BoUabyrsX8I6tHS4OaZXCPGbO0dwBFtTuew5FkIWz 23 | PeYubIvtKxiBbeFdXW2c4fK+HMk/VDvQRfTC0D1DwM/pgmy/r/pLMf/3qVh+AVQn 24 | OORatW7KF6Xd06CVK761BA8RBDjUu68BPli+l88HEQKBgQD6qYwoCGm0d1cB/FRY 25 | tXZknyD8EDQ2ftjZw36MGyp/XC01EuwXxYed5lYqnNWmzzjQheb7vKv1wYTXN5lx 26 | 3NJnj7AAcmMrcX547Kk9hWayelaeOAGzRP1taO/5WQ1O60ttq4N7WHYLsL8utcEj 27 | iCVtq5kqsqLH0/LtYwmGl6lwBw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /Client.Test/TestData/ServerCert/rootCA.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDDzCCAfegAwIBAgIUGS4YEVtD+fgxe5SXXML+Qp/OwaEwDQYJKoZIhvcNAQEL 3 | BQAwFzEVMBMGA1UEAwwMTXlUZXN0Um9vdENBMB4XDTI1MDMwNTE1MTUzMVoXDTM1 4 | MDMwMzE1MTUzMVowFzEVMBMGA1UEAwwMTXlUZXN0Um9vdENBMIIBIjANBgkqhkiG 5 | 9w0BAQEFAAOCAQ8AMIIBCgKCAQEA9YpjnMhmO+fcr+z407V6g/SOVxgY6y9fZHFy 6 | QyFKMO8mbkGlgt7wE/vi7RZ6ukuBmxS9TlcRelRde7KIBfBBY20FB0eY9x3eOphh 7 | NzCl/ERUgxudMYviXhF8OyxG6erSyASdI45ORT5IhQtoyWX2z8cuvwds9jLbqgL6 8 | rukrUpuT7BzVTKqZ3pxok6k9uhhcqLfO6MswV9aSlBa4cKXnF0Uu0Eu+jLqQs6w2 9 | Vfjj5E6s2vE2yVLt8jqYCNVBhDYQJS/524JHub+4fcD3rGNoedOdAOah8I0GjP7P 10 | KYQUzwl2F1VgNwGvx4rbYqTlyhl8S1jDy4ehoViPsJ9pRe8duQIDAQABo1MwUTAd 11 | BgNVHQ4EFgQU7Khaitr/dXKiFcg2XnzFZraeF30wHwYDVR0jBBgwFoAU7Khaitr/ 12 | dXKiFcg2XnzFZraeF30wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC 13 | AQEAWBhBc0XZtbbPwResBi5UQEGT7wzRmd6g3kb8sHihlfZqNMVgfTb9I3BN7ofq 14 | nYhdXoGkKyKcSlCLY8p9hjItJRLxL715CzGhtUYi1kW+XKrQZryWTb0L2txub7As 15 | eIE6Mgy2WLTrCgLgWhLV1U1LOkjDsRRRgg6NanPd2TzDWB2YxXEcrQwqH4Cebl12 16 | 6r3MOCWBq4skniCDlLB4A+0Xox5SkvWiquD5VKOQlDJppKZtrI3JiDxCybognS6R 17 | G+YFCNOpLdxsz9gw9qfRZBxX86dZkSbW8x86/9ORdi+PcswHuy3sV/dcgqUo//wJ 18 | /J77nNA2dZRwVVpcEkWDrmoG1A== 19 | -----END CERTIFICATE----- 20 | -------------------------------------------------------------------------------- /Client.Test/TestData/ServerCert/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIJQQIBADANBgkqhkiG9w0BAQEFAASCCSswggknAgEAAoICAQCqcQuSG8ZWMR/k 3 | ITWPThdYE+9TpZqDNSFocFkZxJcRVi00Ab7pMdoL2TR24qztCNTn50stlu/jxn8K 4 | JFI0R21Os/OmV4rsLJCrUan2P13DE5ttL6fkEAK4kz90Ay/wTyWIdvC6ZHHhvvHC 5 | 8Kc2bZmye8Xq27Y/Qz5BjrdNWNbT3N6CYo6FDMUN7bf9VB3kfmwLBXzHSqqOIXlQ 6 | +pm9IIl1fHoo/2J0AhDF9N1M9fj55fPaAgg0D78buki3k0Me/EgM9pK+SqgzWbRF 7 | ux3IsGreXbmbuivIq1qxTxi1PjxG9T0GwbKceIv10sHhTS3kzuek3bP+rdxCZie+ 8 | HQq1ZIsXV+burP9BRsK5TPRzWMrPBTgXOIpRGGanJPyO28DYhQgsCC62LetcdoSi 9 | z1EEQAod+7COp+DbPDQ6yazsK7Xp5pbpmUDBVYtzmqLlgS5n2y/dMMobx3uTtUGR 10 | Njj7X0IADDWU4jEwnsNtEgzx740E1wUIST1npy0MGspK240fJQywSoJpYHQ6g0rJ 11 | yf4x733m+fI9zxVI4XU3r7QmOd/KSS76GJndkRIDyNpLVX9Skp5tWvTEay/3rZ1k 12 | qWdoRU8XOnJizFdH4/xYfHqiJ6W0qc5cl/Pb9o24kk5BuzyN2SJSDBqIN44l3rzZ 13 | Va8uk0+4glNurCotYB6YGuPZPPRSiwIDAQABAoIB/ynYZ9jpHrW+wh8jzztpPJ3V 14 | BL20Bztez/rdrWFHXghoWNauQbWWtxfSyiGoaN9Ecb62tTAm3oXButc92rfxofeB 15 | QEcbXQmobBUx2xbJbmsXsRqiMFOxYbdEgn8MEGOjopNjN+/ggGEInJ5lpoILu9/T 16 | xemg/jCJ/HyNs1a718mUefAzqpES0+YcR4sQjZuVImzUqbFzkH8by8AfZ8VJlojs 17 | XAgmhVFLVnQUEmcCIr+pmNYcxgy/vfxcmgKK/zuJTIYIKXov46CR6j7CsdkiDrwE 18 | pZapHoiONAtUJyUV8x7hA9g8hp2FDgcdAzpW3nJhiWFGFqiM131i2aFrDiMI9JgK 19 | rF+6092hj90iZN468oGBgPwPLq0TdgsTfxUykXh4xFTEBYhoc2yZznZlwX+fSpuo 20 | VB1vBP9F6g5jKnWp/vmftLQEIIf8jGerVu+MlNM2jQDr1qtQjG6OC2kITN92gml1 21 | Wy9Q+TA6aCOzKKQjqRxnwzB5CNOLmLI+oHLRASP4Za52RT4smXHxk7AcQbKmKh5B 22 | o5AU7ecGFsT89eSnYh41VafB0DmR5dojOvfobB/rI3sI9Mg2bQVN6AIo6DBbdEoi 23 | j8HYrXGMPmRajn3LYjtZzcPMtGFstx//2WHxx6ArVZ2BhkeBDQSFT7Azc7WG60rS 24 | TFtz9miycW/nR8jnGFkCggEBANLRj4RNqKXu4ASXJBPQyDLw684OMDvSkK2JCFje 25 | vQpg9pS1jFOXfDiB0o7I00dHKPZFUnerYk26rXD+e/y5u5HJ0l6DrhWgYXqzI7q6 26 | yhgcUB2K7Ldg8RRah7pAKuASQF7FN66OfceSIAbs1VEirBDIVSJLxKIFDCc63aVr 27 | MDw2f//ATfVGOMaT0oVnSvT7MeVJQVo1behcuQMhGp2ThSaAsCZbtvJkihYdZKes 28 | VpBwABSL9478ssCo8XMz9acl+cDvu+Z75Rid4gFNECq6IVa4Irxh5ylfs0LXEuIj 29 | rQZtzxxNGAp8EUMUzIakL7qP+elRxmOKWNStUqFgywRIqtcCggEBAM74OSk7Azlo 30 | 1pjtNwDwZAORWnOcSD0E5MJW+EuVUQU541Qq3F+/0Rjdv0ejUOvjqOWLihes2oyh 31 | F6h76/k8QWD2TLVSAIibfArzx4Ie0gpogTI2JC5LCT2pLYbONN/V5YWnEZmSdm6j 32 | z40A2omTQ9a+RnveUNtOii2HxqxeJhL/brdEcM0fkRmNK9UIOIW01XWXxVNXZX9k 33 | KSCgDLqMtYaqBFSm6tQu//cKdgfzvOuwC2ryhWwF0RzFd5578jht/EzcPvT7ZJeK 34 | weN6FLTeMXIEjr6nsxsVJ1vKQaPwPTPd+AvlPiBpaXlHjpGtYSNtgrWGvMHQ0Rqa 35 | NbthBolDc20CggEAPXxVKTclGtAikfQq14Sq/wB5Ja3Jr4joHVS8FH9SDzbcc97H 36 | QJIL7sceS/qSdFVywvDQ7ooTr6vkbfIq4zigVhLQwjQRj8ko5QpeP1W/H20SiKs+ 37 | AvXJGjCVKWMRnix0ja9jYAu673vUz3A/ftxn5rUm70u926sxMjfe8SHj/Yu/pS3H 38 | DuQeJvxyB+pqWCzzDnZhExhGJ+DzLNB43MBrsyERPv68ytE73NtRwf1nedTQkS2J 39 | VDCrSbl8QidDXC6dOwIwgTZdSOKzScHaFzwWAR7PMrYzH0QEGlhz73SMQDXCk/SA 40 | MH6i4jestxAnw3e+Yagx/lIZOl4anWqHi4ZOHQKCAQAENN9UizKBlDPoX8niZwBh 41 | Yi7ocqC1PMg5evMI9jvzUz3flA9uksN5MeiVWCho4vOn1rIz0PuQKCYG8p3VLvhV 42 | NCxS8xu3qxFWV6YSavXx6DXYA2mw1y2U5z9Zog9JDVvg34hoCqYFkabCQuYuLaQP 43 | VUaxdc9G5c3BJyVOk4tKj458gxOX17BnSIs/hS9A6G/+N9t22ivh7vM3Com3sOxv 44 | lD0KS2oybdId/0Ru3PFWCPnyboNvsp6RayIHQpWlt/aMyV0uNX3R5qwSJ4UMOAfh 45 | PV55WYc2YbOvFelHn7Zeu4AzHbGpjvrp0B6Br3ht8+fYMKjdd5UIaWH2MP6lA43x 46 | AoIBAQCBOM3TjPl0s11mhVnwtOUN49Li2lP1uDlNb6QGaetqmqd2RSrYxeBYpYBI 47 | Udo/QIE8mxCF/Sc33MaymS3OG6fNeiNee81eI3NJlygOU60MBoGsjXeUX26B9zGx 48 | znAfluRAoVI1wVj06tEnaic/oTASEELhVjbLDxL2v9yordFisLaAouaSItWTlSiE 49 | PC9+SXv2IYH96kzf33ILc3DckNdG87z4x0dnO/qJrmsKORPvvAY20CQR6uelIq+a 50 | A1p2iC8d1vs7xiP1JXVkvNZRhEnWIQyDar7EKGnBeBeytNOUgITeibrjAxSgnHlI 51 | aJ/DJFrvYmzVt2mksIWcP/mYU7uI 52 | -----END PRIVATE KEY----- 53 | -------------------------------------------------------------------------------- /Client.Test/TestData/ServerCert/server.p12: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-csharp/71d1c5919f9e1fa1cc798f8e8b397fb9c6de6e62/Client.Test/TestData/ServerCert/server.p12 -------------------------------------------------------------------------------- /Client.Test/TestData/ServerCert/server.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIELDCCAxSgAwIBAgIUJP6+5nwHFbf6bmaErshA7m5JcOowDQYJKoZIhvcNAQEL 3 | BQAwFzEVMBMGA1UEAwwMTXlUZXN0Um9vdENBMB4XDTI1MDMwNTE1MTUzMVoXDTI2 4 | MDMwNTE1MTUzMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0B 5 | AQEFAAOCAg8AMIICCgKCAgEAqnELkhvGVjEf5CE1j04XWBPvU6WagzUhaHBZGcSX 6 | EVYtNAG+6THaC9k0duKs7QjU5+dLLZbv48Z/CiRSNEdtTrPzpleK7CyQq1Gp9j9d 7 | wxObbS+n5BACuJM/dAMv8E8liHbwumRx4b7xwvCnNm2ZsnvF6tu2P0M+QY63TVjW 8 | 09zegmKOhQzFDe23/VQd5H5sCwV8x0qqjiF5UPqZvSCJdXx6KP9idAIQxfTdTPX4 9 | +eXz2gIINA+/G7pIt5NDHvxIDPaSvkqoM1m0RbsdyLBq3l25m7oryKtasU8YtT48 10 | RvU9BsGynHiL9dLB4U0t5M7npN2z/q3cQmYnvh0KtWSLF1fm7qz/QUbCuUz0c1jK 11 | zwU4FziKURhmpyT8jtvA2IUILAguti3rXHaEos9RBEAKHfuwjqfg2zw0Osms7Cu1 12 | 6eaW6ZlAwVWLc5qi5YEuZ9sv3TDKG8d7k7VBkTY4+19CAAw1lOIxMJ7DbRIM8e+N 13 | BNcFCEk9Z6ctDBrKStuNHyUMsEqCaWB0OoNKycn+Me995vnyPc8VSOF1N6+0Jjnf 14 | ykku+hiZ3ZESA8jaS1V/UpKebVr0xGsv962dZKlnaEVPFzpyYsxXR+P8WHx6oiel 15 | tKnOXJfz2/aNuJJOQbs8jdkiUgwaiDeOJd682VWvLpNPuIJTbqwqLWAemBrj2Tz0 16 | UosCAwEAAaNzMHEwGgYDVR0RBBMwEYIJbG9jYWxob3N0hwR/AAABMBMGA1UdJQQM 17 | MAoGCCsGAQUFBwMBMB0GA1UdDgQWBBS3LUDBo131k//IR3N2NTXeoR4rYzAfBgNV 18 | HSMEGDAWgBTsqFqK2v91cqIVyDZefMVmtp4XfTANBgkqhkiG9w0BAQsFAAOCAQEA 19 | BIImYrBwBjQQ8cOY/auUiETwwG6Ypj8KD96FpKJsJcHNxja+H6vG9XqvolWxtZ54 20 | c7vgTA63O4eGOAmkeIy5N+FRk6L5lT7zUZf/VXnsUHKPUDMLlKaoGgvCbhscXXLR 21 | PyqFrm/5IBYJFLGggaeLHDNc8tLAAEREr/FS5v7ufe9pSnxBA0QlVerfWiB7mLtt 22 | 21lMzZojgHu6X/i72VGeg1sOwB0+n7M21aEU84cL7vtMkLgHgWGyAkQ3+zqYAIb9 23 | 2Oq5LIB2z9zK98I7KjRvcotZDBWRKw0/jdawS6y3j+YVPz7GX+2MXdX/XukSWIlf 24 | ZPmD8FbRNJlv+/O61wvvqQ== 25 | -----END CERTIFICATE----- 26 | -------------------------------------------------------------------------------- /Client.Test/Usings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; -------------------------------------------------------------------------------- /Client.Test/Write/WritePrecisionConverterTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using InfluxDB3.Client.Write; 3 | 4 | namespace InfluxDB3.Client.Test.Write 5 | { 6 | public class WritePrecisionConverterTest 7 | { 8 | [TestCase(WritePrecision.Ns, "ns")] 9 | [TestCase(WritePrecision.Us, "us")] 10 | [TestCase(WritePrecision.Ms, "ms")] 11 | [TestCase(WritePrecision.S, "s")] 12 | public void ToV2ApiString_ValidPrecision(WritePrecision precision, string expectedString) 13 | { 14 | string result = WritePrecisionConverter.ToV2ApiString(precision); 15 | Assert.That(expectedString, Is.EqualTo(result)); 16 | } 17 | 18 | [Test] 19 | public void ToV2ApiString_InvalidPrecision() 20 | { 21 | var ae = Assert.Throws(() => 22 | { 23 | WritePrecisionConverter.ToV2ApiString((WritePrecision)999); 24 | }); 25 | Assert.That(ae, Is.Not.Null); 26 | Assert.That(ae.Message, Does.Contain("Unsupported precision")); 27 | } 28 | 29 | [TestCase(WritePrecision.Ns, "nanosecond")] 30 | [TestCase(WritePrecision.Us, "microsecond")] 31 | [TestCase(WritePrecision.Ms, "millisecond")] 32 | [TestCase(WritePrecision.S, "second")] 33 | public void ToV3ApiString_ValidPrecision(WritePrecision precision, string expectedString) 34 | { 35 | string result = WritePrecisionConverter.ToV3ApiString(precision); 36 | Assert.That(expectedString, Is.EqualTo(result)); 37 | } 38 | 39 | [Test] 40 | public void ToV3ApiString_InvalidPrecision() 41 | { 42 | var ae = Assert.Throws(() => 43 | { 44 | WritePrecisionConverter.ToV3ApiString((WritePrecision)999); 45 | }); 46 | Assert.That(ae, Is.Not.Null); 47 | Assert.That(ae.Message, Does.Contain("Unsupported precision")); 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /Client/Client.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;netstandard2.1 5 | 10 6 | enable 7 | 8 | 9 | The C# .NET client that provides an easy and convenient way to interact with InfluxDB 3. 10 | This package supports both writing data to InfluxDB and querying data using the FlightSQL client, 11 | which allows you to execute SQL queries against InfluxDB IOx. 12 | 13 | InfluxDB3.Client Contributors 14 | InfluxDB3.Client 15 | 1.3.0 16 | dev 17 | 18 | InfluxDB3.Client 19 | influxdata;timeseries;influxdb 20 | influxdata.jpg 21 | https://raw.githubusercontent.com/InfluxCommunity/influxdb3-csharp/main/Assets/influxdata.jpg 22 | https://github.com/InfluxCommunity/influxdb3-csharp/tree/main/Client 23 | MIT 24 | https://github.com/InfluxCommunity/influxdb3-csharp 25 | git 26 | 27 | https://raw.githubusercontent.com/InfluxCommunity/influxdb3-csharp/main/CHANGELOG.md 28 | true 29 | README.md 30 | InfluxDB3.Client 31 | 32 | ../Keys/Key.snk 33 | true 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | <_Parameter1>InfluxDB3.Client.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100054b3efef02968d05c3dd8481e23fb40ade1fae377f18cf5fa48c673694140f7c00dc0b38d43be297256824dc8489c5224647e77f861ef600514607159b151cf71b094a0ef5736c420cbaa14100acc3b3694e3815597a5e89cf8090ed22bfdad2d5eec49250d88da1345d670b5e131ed9611eed141e04c31d79f166db39cb4a5 49 | 50 | 51 | <_Parameter1>DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7 52 | 53 | 54 | <_Parameter1>InfluxDB3.Client.Test.Integration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100054b3efef02968d05c3dd8481e23fb40ade1fae377f18cf5fa48c673694140f7c00dc0b38d43be297256824dc8489c5224647e77f861ef600514607159b151cf71b094a0ef5736c420cbaa14100acc3b3694e3815597a5e89cf8090ed22bfdad2d5eec49250d88da1345d670b5e131ed9611eed141e04c31d79f166db39cb4a5 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /Client/Config/ClientConfig.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Net; 5 | using System.Web; 6 | using InfluxDB3.Client.Write; 7 | 8 | namespace InfluxDB3.Client.Config; 9 | 10 | /// 11 | /// The ClientConfig class holds the configuration for the InfluxDB client. 12 | /// 13 | /// You can configure following options: 14 | /// 15 | /// - Host: The URL of the InfluxDB server. 16 | /// - Token: The authentication token for accessing the InfluxDB server. 17 | /// - AuthScheme: Token authentication scheme. Default is 'null' for Cloud access. Set to 'Bearer' for Edge access. 18 | /// - Organization: The organization to be used for operations. 19 | /// - Database: The database to be used for InfluxDB operations. 20 | /// - Headers: The set of HTTP headers to be included in requests. 21 | /// - Timeout: Timeout to wait before the HTTP request times out. Default to '10 seconds'. 22 | /// - AllowHttpRedirects: Automatically following HTTP 3xx redirects. Default to 'false'. 23 | /// - DisableServerCertificateValidation: Disable server SSL certificate validation. Default to 'false'. 24 | /// - DisableCertificateRevocationListCheck: Disable SSL certificate revocation list (CRL) checking. Default to 'false'. 25 | /// - SslRootsFilePath: SSL root certificates file path. 26 | /// - Proxy: The HTTP proxy URL. Default is not set. 27 | /// - WriteOptions: Write options. 28 | /// - QueryOptions Query options. 29 | /// 30 | /// 31 | /// If you want create client with custom options, you can use the following code: 32 | /// 33 | /// using var client = new InfluxDBClient(new ClientConfig{ 34 | /// Host = "https://us-east-1-1.aws.cloud2.influxdata.com", 35 | /// Token = "my-token", 36 | /// Organization = "my-org", 37 | /// Database = "my-database", 38 | /// AllowHttpRedirects = true, 39 | /// DisableServerCertificateValidation = true, 40 | /// WriteOptions = new WriteOptions 41 | /// { 42 | /// Precision = WritePrecision.S, 43 | /// GzipThreshold = 4096, 44 | /// NoSync = false 45 | /// }, 46 | /// QueryOptions = new QueryOptions 47 | /// { 48 | /// Deadline = DateTime.UtcNow.AddSeconds(10), 49 | /// MaxReceiveMessageSize = 4096, 50 | /// MaxSendMessageSize = 4096, 51 | /// CompressionProviders = new List<ICompressionProvider> 52 | /// { 53 | /// Grpc.Net.Compression.GzipCompressionProvider.Default 54 | /// } 55 | /// } 56 | /// }); 57 | /// 58 | /// 59 | public class ClientConfig 60 | { 61 | internal const string EnvInfluxHost = "INFLUX_HOST"; 62 | internal const string EnvInfluxToken = "INFLUX_TOKEN"; 63 | internal const string EnvInfluxAuthScheme = "INFLUX_AUTH_SCHEME"; 64 | internal const string EnvInfluxOrg = "INFLUX_ORG"; 65 | internal const string EnvInfluxDatabase = "INFLUX_DATABASE"; 66 | internal const string EnvInfluxPrecision = "INFLUX_PRECISION"; 67 | internal const string EnvInfluxGzipThreshold = "INFLUX_GZIP_THRESHOLD"; 68 | internal const string EnvInfluxWriteNoSync = "INFLUX_WRITE_NO_SYNC"; 69 | 70 | private string _host = ""; 71 | 72 | /// 73 | /// Initializes a new instance of client configuration. 74 | /// 75 | public ClientConfig() 76 | { 77 | QueryOptions = (QueryOptions)QueryOptions.DefaultOptions.Clone(); 78 | } 79 | 80 | /// 81 | /// Initializes a new instance of client configuration from connection string. 82 | /// 83 | internal ClientConfig(string connectionString) 84 | { 85 | var uri = new Uri(connectionString); 86 | Host = uri.GetLeftPart(UriPartial.Path); 87 | var values = HttpUtility.ParseQueryString(uri.Query); 88 | Token = values.Get("token"); 89 | AuthScheme = values.Get("authScheme"); 90 | Organization = values.Get("org"); 91 | Database = values.Get("database"); 92 | QueryOptions = (QueryOptions)QueryOptions.DefaultOptions.Clone(); 93 | ParsePrecision(values.Get("precision")); 94 | ParseGzipThreshold(values.Get("gzipThreshold")); 95 | ParseWriteNoSync(values.Get("writeNoSync")); 96 | } 97 | 98 | /// 99 | /// Initializes a new instance of client configuration from environment variables. 100 | /// 101 | internal ClientConfig(IDictionary env) 102 | { 103 | Host = (string)env[EnvInfluxHost]; 104 | Token = env[EnvInfluxToken] as string; 105 | AuthScheme = env[EnvInfluxAuthScheme] as string; 106 | Organization = env[EnvInfluxOrg] as string; 107 | Database = env[EnvInfluxDatabase] as string; 108 | QueryOptions = (QueryOptions)QueryOptions.DefaultOptions.Clone(); 109 | ParsePrecision(env[EnvInfluxPrecision] as string); 110 | ParseGzipThreshold(env[EnvInfluxGzipThreshold] as string); 111 | ParseWriteNoSync(env[EnvInfluxWriteNoSync] as string); 112 | } 113 | 114 | /// 115 | /// The URL of the InfluxDB server. 116 | /// 117 | public string Host 118 | { 119 | get => _host; 120 | set => _host = string.IsNullOrEmpty(value) ? value : value.EndsWith("/") ? value : $"{value}/"; 121 | } 122 | 123 | /// 124 | /// The authentication token for accessing the InfluxDB server. 125 | /// 126 | public string? Token { get; set; } 127 | 128 | /// 129 | /// Token authentication scheme. 130 | /// 131 | public string? AuthScheme { get; set; } 132 | 133 | /// 134 | /// The organization to be used for operations. 135 | /// 136 | public string? Organization { get; set; } 137 | 138 | /// 139 | /// The database to be used for InfluxDB operations. 140 | /// 141 | public string? Database { get; set; } 142 | 143 | /// 144 | /// The custom headers that will be added to requests. This is useful for adding custom headers to requests, 145 | /// such as tracing headers. To add custom headers use following code: 146 | /// 147 | /// 148 | /// using var client = new InfluxDBClient(new ClientConfig 149 | /// { 150 | /// Host = "https://us-east-1-1.aws.cloud2.influxdata.com", 151 | /// Token = "my-token", 152 | /// Organization = "my-org", 153 | /// Database = "my-database", 154 | /// Headers = new Dictionary<string, string> 155 | /// { 156 | /// { "X-Tracing-Id", "123" }, 157 | /// } 158 | /// }); 159 | /// 160 | /// 161 | public Dictionary? Headers { get; set; } 162 | 163 | /// 164 | /// Timeout to wait before the HTTP request times out. Default to '10 seconds'. 165 | /// 166 | public TimeSpan Timeout { get; set; } = TimeSpan.FromSeconds(10); 167 | 168 | /// 169 | /// Automatically following HTTP 3xx redirects. Default to 'false'. 170 | /// 171 | public bool AllowHttpRedirects { get; set; } 172 | 173 | /// 174 | /// Disable server SSL certificate validation. Default to 'false'. 175 | /// 176 | public bool DisableServerCertificateValidation { get; set; } 177 | 178 | /// 179 | /// Disable SSL certificate revocation list (CRL) checking. Default to 'false'. 180 | /// 181 | public bool DisableCertificateRevocationListCheck { get; set; } 182 | 183 | /// 184 | /// SSL root certificates file path. 185 | /// 186 | public string? SslRootsFilePath { get; set; } 187 | 188 | /// 189 | /// The HTTP proxy URL. Default is not set. 190 | /// 191 | public WebProxy? Proxy { get; set; } 192 | 193 | /// 194 | /// Write options. 195 | /// 196 | public WriteOptions? WriteOptions { get; set; } 197 | 198 | /// 199 | /// Configuration options for query behavior in the InfluxDB client. 200 | /// 201 | public QueryOptions QueryOptions { get; set; } 202 | 203 | internal void Validate() 204 | { 205 | if (string.IsNullOrEmpty(Host)) 206 | { 207 | throw new ArgumentException("The URL of the InfluxDB server has to be defined"); 208 | } 209 | } 210 | 211 | internal WritePrecision WritePrecision 212 | { 213 | get => WriteOptions != null ? WriteOptions.Precision ?? WritePrecision.Ns : WritePrecision.Ns; 214 | } 215 | 216 | internal bool WriteNoSync 217 | { 218 | get => (WriteOptions ?? WriteOptions.DefaultOptions).NoSync; 219 | } 220 | 221 | private void ParsePrecision(string? precision) 222 | { 223 | if (precision != null) 224 | { 225 | var writePrecision = precision switch 226 | { 227 | "ns" => WritePrecision.Ns, 228 | "nanosecond" => WritePrecision.Ns, 229 | "us" => WritePrecision.Us, 230 | "microsecond" => WritePrecision.Us, 231 | "ms" => WritePrecision.Ms, 232 | "millisecond" => WritePrecision.Ms, 233 | "s" => WritePrecision.S, 234 | "second" => WritePrecision.S, 235 | _ => throw new ArgumentException($"Unsupported precision '{precision}'"), 236 | }; 237 | WriteOptions ??= (WriteOptions)WriteOptions.DefaultOptions.Clone(); 238 | WriteOptions.Precision = writePrecision; 239 | } 240 | } 241 | 242 | private void ParseGzipThreshold(string? threshold) 243 | { 244 | if (threshold != null) 245 | { 246 | var gzipThreshold = int.Parse(threshold); 247 | WriteOptions ??= (WriteOptions)WriteOptions.DefaultOptions.Clone(); 248 | WriteOptions.GzipThreshold = gzipThreshold; 249 | } 250 | } 251 | 252 | private void ParseWriteNoSync(string? strVal) 253 | { 254 | if (strVal != null) 255 | { 256 | var noSync = bool.Parse(strVal); 257 | WriteOptions ??= (WriteOptions)WriteOptions.DefaultOptions.Clone(); 258 | WriteOptions.NoSync = noSync; 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /Client/Config/QueryOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Grpc.Net.Compression; 4 | 5 | namespace InfluxDB3.Client.Config; 6 | 7 | /// 8 | /// Represents the options for configuring query behavior in the client. 9 | /// 10 | public class QueryOptions : ICloneable 11 | { 12 | /// 13 | /// Gets or sets the optional deadline for query execution. 14 | /// 15 | /// 16 | /// This property specifies the maximum time allowed for query execution before a timeout. 17 | /// If set to null, no deadline is applied. The value is represented as a nullable . 18 | /// 19 | public DateTime? Deadline { get; set; } 20 | 21 | /// 22 | /// Gets or sets the maximum size, in bytes, of a single message that can be received. 23 | /// 24 | /// 25 | /// This property defines an optional limit for the size of incoming messages to avoid excessive memory allocation. 26 | /// A value of null specifies that no maximum size is enforced. The default value is 4,194,304 bytes (4 MB). 27 | /// 28 | public int? MaxReceiveMessageSize { get; set; } 29 | 30 | /// 31 | /// Gets or sets the maximum size of a message that can be sent. 32 | /// 33 | /// 34 | /// This property defines the maximum allowable size, in bytes, for messages sent by the client. 35 | /// If set to null, there is no limit on the size of the sent messages. 36 | /// 37 | public int? MaxSendMessageSize { get; set; } 38 | 39 | /// 40 | /// Gets or sets the collection of compression providers used for gRPC message compression. 41 | /// 42 | /// 43 | /// This property specifies the list of compression algorithms available for compressing gRPC messages. 44 | /// The value is represented as a nullable list of . 45 | /// If set to null, Gzip will be used 46 | /// 47 | public IList? CompressionProviders { get; set; } 48 | 49 | /// 50 | /// Represents the default query options used throughout the client configuration. 51 | /// 52 | /// 53 | /// This variable is a static, pre-configured instance of with default values. 54 | /// It specifies parameters such as deadlines, message sizes, and compression for query execution. 55 | /// 56 | internal static readonly QueryOptions DefaultOptions = new() 57 | { 58 | Deadline = null, 59 | MaxReceiveMessageSize = 4_194_304, 60 | MaxSendMessageSize = null, 61 | CompressionProviders = null 62 | }; 63 | 64 | /// 65 | /// Creates a shallow copy of the current QueryOptions instance. 66 | /// 67 | /// 68 | /// A new object that is a shallow copy of the current QueryOptions instance. 69 | /// 70 | public object Clone() 71 | { 72 | return MemberwiseClone(); 73 | } 74 | } -------------------------------------------------------------------------------- /Client/Config/WriteOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using InfluxDB3.Client.Write; 4 | 5 | namespace InfluxDB3.Client.Config; 6 | 7 | /// 8 | /// The WriteOptions class holds the configuration for writing data to InfluxDB. 9 | /// 10 | /// You can configure following options: 11 | /// - Precision: The default precision to use for the timestamp of points if no precision is specified in the write API call. 12 | /// - GzipThreshold: The threshold in bytes for gzipping the body. The default value is 1000. 13 | /// - NoSync: Bool value whether to skip waiting for WAL persistence on write. The default value is false. 14 | /// 15 | /// If you want create client with custom options, you can use the following code: 16 | /// 17 | /// using var client = new InfluxDBClient(new ClientConfig 18 | /// { 19 | /// Host = "https://us-east-1-1.aws.cloud2.influxdata.com", 20 | /// Token = "my-token", 21 | /// Organization = "my-org", 22 | /// Database = "my-database", 23 | /// WriteOptions = new WriteOptions 24 | /// { 25 | /// Precision = WritePrecision.S, 26 | /// GzipThreshold = 4096, 27 | /// NoSync = false 28 | /// } 29 | /// }); 30 | /// 31 | /// 32 | public class WriteOptions : ICloneable 33 | { 34 | /// 35 | /// The default precision to use for the timestamp of points if no precision is specified in the write API call. 36 | /// 37 | public WritePrecision? Precision { get; set; } 38 | 39 | /// 40 | /// Tags added to each point during writing. If a point already has a tag with the same key, it is left unchanged. 41 | /// 42 | /// 43 | /// 61 | /// 62 | /// 63 | /// 64 | public Dictionary? DefaultTags { get; set; } 65 | 66 | /// 67 | /// The threshold in bytes for gzipping the body. 68 | /// 69 | public int GzipThreshold { get; set; } 70 | 71 | /// 72 | /// Instructs the server whether to wait with the response until WAL persistence completes. 73 | /// NoSync=true means faster write but without the confirmation that the data was persisted. 74 | /// 75 | /// Note: This option is supported by InfluxDB 3 Core and Enterprise servers only. 76 | /// For other InfluxDB 3 server types (InfluxDB Clustered, InfluxDB Clould Serverless/Dedicated) 77 | /// the write operation will fail with an error. 78 | /// 79 | /// Default value: false. 80 | /// 81 | public bool NoSync { get; set; } 82 | 83 | public object Clone() 84 | { 85 | return this.MemberwiseClone(); 86 | } 87 | 88 | internal static readonly WriteOptions DefaultOptions = new() 89 | { 90 | Precision = WritePrecision.Ns, 91 | GzipThreshold = 1000, 92 | NoSync = false, 93 | }; 94 | } 95 | -------------------------------------------------------------------------------- /Client/InfluxDBApiException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Net.Http; 4 | using System.Net.Http.Headers; 5 | 6 | namespace InfluxDB3.Client; 7 | 8 | /// 9 | /// The generic API exception. 10 | /// 11 | public class InfluxDBApiException : Exception 12 | { 13 | internal InfluxDBApiException(string message, HttpResponseMessage httpResponseMessage) : base(message) 14 | { 15 | HttpResponseMessage = httpResponseMessage; 16 | } 17 | 18 | public HttpResponseMessage? HttpResponseMessage { get; private set; } 19 | 20 | public HttpResponseHeaders? Headers 21 | { 22 | get 23 | { 24 | return HttpResponseMessage?.Headers; 25 | } 26 | } 27 | 28 | public HttpStatusCode StatusCode 29 | { 30 | get 31 | { 32 | return HttpResponseMessage?.StatusCode ?? 0; 33 | } 34 | } 35 | 36 | } -------------------------------------------------------------------------------- /Client/Internal/Arguments.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace InfluxDB3.Client.Internal 5 | { 6 | /// 7 | /// Functions for parameter validation. 8 | /// 9 | /// 10 | /// Inspiration from InfluxDB java - thanks 11 | /// 12 | /// 13 | internal static class Arguments 14 | { 15 | private const string DurationPattern = @"([-+]?)([0-9]+(\\.[0-9]*)?[a-z]+)+|inf|-inf"; 16 | 17 | /// 18 | /// Enforces that the string is not empty. 19 | /// 20 | /// the string to test 21 | /// the variable name for reporting 22 | /// if the string is empty 23 | internal static void CheckNonEmptyString(string value, string name) 24 | { 25 | if (string.IsNullOrEmpty(value)) 26 | { 27 | throw new ArgumentException("Expecting a non-empty string for " + name); 28 | } 29 | } 30 | 31 | /// 32 | /// Enforces that the string is duration literal. 33 | /// 34 | /// the string to test 35 | /// the variable name for reporting 36 | /// if the string is not duration literal 37 | internal static void CheckDuration(string value, string name) 38 | { 39 | if (string.IsNullOrEmpty(value) || !Regex.Match(value, DurationPattern).Success) 40 | { 41 | throw new ArgumentException("Expecting a duration string for " + name + ". But got: " + value); 42 | } 43 | } 44 | 45 | /// 46 | /// Enforces that the number is larger than 0. 47 | /// 48 | /// the number to test 49 | /// the variable name for reporting 50 | /// if the number is less or equal to 0 51 | internal static void CheckPositiveNumber(int number, string name) 52 | { 53 | if (number <= 0) 54 | { 55 | throw new ArgumentException("Expecting a positive number for " + name); 56 | } 57 | } 58 | 59 | /// 60 | /// Enforces that the number is not negative. 61 | /// 62 | /// the number to test 63 | /// the variable name for reporting 64 | /// if the number is less or equal to 0 65 | internal static void CheckNotNegativeNumber(int number, string name) 66 | { 67 | if (number < 0) 68 | { 69 | throw new ArgumentException("Expecting a positive or zero number for " + name); 70 | } 71 | } 72 | 73 | /// 74 | /// Checks that the specified object reference is not null. 75 | /// 76 | /// the object to test 77 | /// the variable name for reporting 78 | /// if the object is null 79 | internal static void CheckNotNull(object obj, string name) 80 | { 81 | if (obj == null) 82 | { 83 | throw new NullReferenceException("Expecting a not null reference for " + name); 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /Client/Internal/AssemblyHelper.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | namespace InfluxDB3.Client.Internal 4 | { 5 | internal static class AssemblyHelper 6 | { 7 | internal static string GetVersion() 8 | { 9 | return typeof(InfluxDBClient) 10 | .GetTypeInfo() 11 | .Assembly 12 | .GetCustomAttribute() 13 | .Version; 14 | } 15 | 16 | internal static string GetUserAgent() 17 | { 18 | return $"influxdb3-csharp/{GetVersion()}"; 19 | } 20 | 21 | } 22 | } -------------------------------------------------------------------------------- /Client/Internal/FlightSqlClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Runtime.Serialization; 7 | using System.Runtime.Serialization.Json; 8 | using System.Text; 9 | using Apache.Arrow; 10 | using Apache.Arrow.Flight; 11 | using Apache.Arrow.Flight.Client; 12 | using Grpc.Core; 13 | using Grpc.Net.Client; 14 | using InfluxDB3.Client.Config; 15 | using InfluxDB3.Client.Query; 16 | 17 | namespace InfluxDB3.Client.Internal; 18 | 19 | /// 20 | /// Simple "FlightSQL" client implementation. 21 | /// 22 | internal interface IFlightSqlClient : IDisposable 23 | { 24 | /// 25 | /// Execute the query and return the result as a sequence of record batches. 26 | /// 27 | internal IAsyncEnumerable Execute(string query, string database, QueryType queryType, 28 | Dictionary namedParameters, Dictionary headers); 29 | 30 | /// 31 | /// Prepare the FlightTicket for the query. 32 | /// 33 | internal FlightTicket PrepareFlightTicket(string query, string database, QueryType queryType, 34 | Dictionary namedParameters); 35 | 36 | /// 37 | /// Prepare the headers metadata. 38 | /// 39 | /// The invocation headers 40 | /// 41 | internal Metadata PrepareHeadersMetadata(Dictionary headers); 42 | } 43 | 44 | internal class FlightSqlClient : IFlightSqlClient 45 | { 46 | private readonly GrpcChannel _channel; 47 | private readonly FlightClient _flightClient; 48 | 49 | private readonly ClientConfig _config; 50 | private readonly DataContractJsonSerializer _serializer; 51 | 52 | internal FlightSqlClient(ClientConfig config, HttpClient httpClient) 53 | { 54 | _config = config; 55 | _channel = GrpcChannel.ForAddress( 56 | _config.Host, 57 | new GrpcChannelOptions 58 | { 59 | HttpClient = httpClient, 60 | DisposeHttpClient = false, 61 | Credentials = _config.Host.StartsWith("https", StringComparison.OrdinalIgnoreCase) 62 | ? ChannelCredentials.SecureSsl 63 | : ChannelCredentials.Insecure, 64 | MaxReceiveMessageSize = _config.QueryOptions.MaxReceiveMessageSize, 65 | MaxSendMessageSize = _config.QueryOptions.MaxSendMessageSize, 66 | CompressionProviders = _config.QueryOptions.CompressionProviders, 67 | }); 68 | _flightClient = new FlightClient(_channel); 69 | var knownTypes = new List { typeof(string), typeof(int), typeof(float), typeof(bool) }; 70 | _serializer = new DataContractJsonSerializer(typeof(Dictionary), 71 | new DataContractJsonSerializerSettings 72 | { 73 | UseSimpleDictionaryFormat = true, 74 | KnownTypes = knownTypes, 75 | EmitTypeInformation = EmitTypeInformation.Never 76 | }); 77 | } 78 | 79 | async IAsyncEnumerable IFlightSqlClient.Execute(string query, string database, QueryType queryType, 80 | Dictionary namedParameters, Dictionary headers) 81 | { 82 | // 83 | // verify that values of namedParameters is supported type 84 | // 85 | foreach (var keyValuePair in namedParameters) 86 | { 87 | var key = keyValuePair.Key; 88 | var value = keyValuePair.Value; 89 | if (value is not string and not int and not float and not bool) 90 | { 91 | throw new ArgumentException($"The parameter '{key}' has unsupported type '{value.GetType()}'. " + 92 | $"The supported types are 'string', 'bool', 'int' and 'float'."); 93 | } 94 | } 95 | 96 | var metadata = ((IFlightSqlClient)this).PrepareHeadersMetadata(headers); 97 | 98 | var ticket = ((IFlightSqlClient)this).PrepareFlightTicket(query, database, queryType, namedParameters); 99 | 100 | using var stream = _flightClient.GetStream(ticket, metadata, _config.QueryOptions.Deadline); 101 | while (await stream.ResponseStream.MoveNext().ConfigureAwait(false)) 102 | { 103 | yield return stream.ResponseStream.Current; 104 | } 105 | } 106 | 107 | FlightTicket IFlightSqlClient.PrepareFlightTicket(string query, string database, QueryType queryType, 108 | Dictionary namedParameters) 109 | { 110 | // set query parameters 111 | var ticketData = new Dictionary 112 | { 113 | { "database", database }, 114 | { "sql_query", query }, 115 | { "query_type", Enum.GetName(typeof(QueryType), queryType)!.ToLowerInvariant() }, 116 | }; 117 | 118 | // 119 | // serialize to json 120 | // 121 | var json = SerializeDictionary(ticketData); 122 | // 123 | // serialize named parameters 124 | // 125 | if (namedParameters.Count > 0) 126 | { 127 | json = json.TrimEnd('}') + $",\"params\": {SerializeDictionary(namedParameters)}}}"; 128 | } 129 | 130 | var flightTicket = new FlightTicket(json); 131 | return flightTicket; 132 | } 133 | 134 | Metadata IFlightSqlClient.PrepareHeadersMetadata(Dictionary headers) 135 | { 136 | var metadata = new Metadata(); 137 | 138 | // user-agent 139 | metadata.Add("user-agent", AssemblyHelper.GetUserAgent()); 140 | 141 | // authorization by token 142 | if (!string.IsNullOrEmpty(_config.Token)) 143 | { 144 | metadata.Add("Authorization", $"Bearer {_config.Token}"); 145 | } 146 | 147 | // add request headers 148 | foreach (var header in headers.Where(header => !header.Key.ToLower().Equals("user-agent"))) 149 | { 150 | metadata.Add(header.Key, header.Value); 151 | } 152 | // add config headers 153 | if (_config.Headers != null) 154 | { 155 | foreach (var header in _config.Headers.Where(header => !headers.ContainsKey(header.Key))) 156 | { 157 | if (!header.Key.ToLower().Equals("user-agent")) 158 | { 159 | metadata.Add(header.Key, header.Value); 160 | } 161 | } 162 | } 163 | return metadata; 164 | } 165 | 166 | private string SerializeDictionary(Dictionary ticketData) 167 | { 168 | using var memoryStream = new MemoryStream(); 169 | _serializer.WriteObject(memoryStream, ticketData); 170 | return Encoding.UTF8.GetString(memoryStream.ToArray()); 171 | } 172 | 173 | public void Dispose() 174 | { 175 | _channel.Dispose(); 176 | } 177 | } -------------------------------------------------------------------------------- /Client/Internal/FlightSqlExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Apache.Arrow; 3 | using Array = Apache.Arrow.Array; 4 | 5 | namespace InfluxDB3.Client.Internal; 6 | 7 | internal static class FlightSqlExtensions 8 | { 9 | /// 10 | /// Get row value from array. The implementation is based on the 11 | /// ArrowArrayFactory. 12 | /// 13 | /// Array 14 | /// Row index 15 | /// 16 | /// Type of array is not supported 17 | internal static object? GetObjectValue(this Array array, int index) 18 | { 19 | return array switch 20 | { 21 | BooleanArray booleanArray => booleanArray.GetValue(index), 22 | UInt8Array uInt8Array => uInt8Array.GetValue(index), 23 | Int8Array int8Array => int8Array.GetValue(index), 24 | UInt16Array uInt16Array => uInt16Array.GetValue(index), 25 | Int16Array int16Array => int16Array.GetValue(index), 26 | UInt32Array uInt32Array => uInt32Array.GetValue(index), 27 | Int32Array int32Array => int32Array.GetValue(index), 28 | UInt64Array uInt64Array => uInt64Array.GetValue(index), 29 | Int64Array int64Array => int64Array.GetValue(index), 30 | FloatArray floatArray => floatArray.GetValue(index), 31 | DoubleArray doubleArray => doubleArray.GetValue(index), 32 | StringArray stringArray => stringArray.GetString(index), 33 | BinaryArray binaryArray => binaryArray.GetBytes(index).ToArray(), 34 | TimestampArray timestampArray => timestampArray.GetTimestamp(index), 35 | Date64Array date64Array => date64Array.GetDateTime(index), 36 | Date32Array date32Array => date32Array.GetDateTime(index), 37 | Time32Array time32Array => time32Array.GetValue(index), 38 | Time64Array time64Array => time64Array.GetValue(index), 39 | Decimal128Array decimal128Array => decimal128Array.GetValue(index), 40 | Decimal256Array decimal256Array => decimal256Array.GetValue(index), 41 | _ => throw new NotSupportedException($"The datatype {array.Data.DataType} is not supported.") 42 | }; 43 | } 44 | } -------------------------------------------------------------------------------- /Client/Internal/GzipHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Linq; 5 | using System.Net.Http; 6 | using System.Text; 7 | 8 | namespace InfluxDB3.Client.Internal; 9 | 10 | internal class GzipHandler 11 | { 12 | private readonly int _threshold; 13 | 14 | public GzipHandler(int threshold) 15 | { 16 | _threshold = threshold; 17 | } 18 | 19 | public HttpContent? Process(string body) 20 | { 21 | if (_threshold > 0 && body.Length < _threshold) 22 | { 23 | return null; 24 | } 25 | 26 | using (var msi = new MemoryStream(Encoding.UTF8.GetBytes(body))) 27 | using (var mso = new MemoryStream()) 28 | { 29 | using (var gs = new GZipStream(mso, CompressionMode.Compress)) 30 | { 31 | msi.CopyTo(gs); 32 | gs.Flush(); 33 | } 34 | 35 | var content = new ByteArrayContent(mso.ToArray()); 36 | content.Headers.Add("Content-Type", "text/plain; charset=utf-8"); 37 | content.Headers.Add("Content-Encoding", "gzip"); 38 | return content; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /Client/Internal/RecordBatchConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | using Apache.Arrow; 4 | using InfluxDB3.Client.Write; 5 | using ArrowArray = Apache.Arrow.Array; 6 | 7 | namespace InfluxDB3.Client.Internal; 8 | 9 | internal static class RecordBatchConverter 10 | { 11 | /// 12 | /// Convert a given row of data from RecordBatch to PointDataValues 13 | /// 14 | /// The RecordBatch to get data from 15 | /// The row number 16 | /// The PointDataValues 17 | internal static PointDataValues ConvertToPointDataValue(RecordBatch recordBatch, int rowNumber) 18 | { 19 | PointDataValues point = new(); 20 | for (var columnIndex = 0; columnIndex < recordBatch.ColumnCount; columnIndex++) 21 | { 22 | var schema = recordBatch.Schema.FieldsList[columnIndex]; 23 | var fullName = schema.Name; 24 | 25 | if (recordBatch.Column(columnIndex) is not ArrowArray array) 26 | { 27 | continue; 28 | } 29 | 30 | var objectValue = array.GetObjectValue(rowNumber); 31 | if (fullName is "measurement" or "iox::measurement" && 32 | objectValue is string value) 33 | { 34 | point = point.SetMeasurement(value); 35 | continue; 36 | } 37 | 38 | if (!schema.HasMetadata) 39 | { 40 | if (fullName == "time" && objectValue is DateTimeOffset timestamp) 41 | { 42 | point = point.SetTimestamp(timestamp); 43 | } 44 | else if (objectValue != null) 45 | { 46 | // just push as field If you don't know what type is it 47 | point = point.SetField(fullName, objectValue); 48 | } 49 | 50 | 51 | continue; 52 | } 53 | 54 | var type = schema.Metadata["iox::column::type"]; 55 | var parts = type.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); 56 | var valueType = parts[2]; 57 | // string fieldType = parts.Length > 3 ? parts[3] : ""; 58 | var mappedValue = TypeCasting.GetMappedValue(schema, objectValue); 59 | if (valueType == "field" && mappedValue != null) 60 | { 61 | point = point.SetField(fullName, mappedValue); 62 | } 63 | else if (valueType == "tag" && mappedValue != null) 64 | { 65 | point = point.SetTag(fullName, (string)mappedValue); 66 | } 67 | else if (valueType == "timestamp" && mappedValue != null) 68 | { 69 | point = point.SetTimestamp((BigInteger)mappedValue); 70 | } 71 | } 72 | 73 | return point; 74 | } 75 | } -------------------------------------------------------------------------------- /Client/Internal/RestClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Net.Http; 7 | using System.Runtime.Serialization; 8 | using System.Runtime.Serialization.Json; 9 | using System.Text; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using System.Web; 13 | using InfluxDB3.Client.Config; 14 | 15 | namespace InfluxDB3.Client.Internal; 16 | 17 | internal class RestClient 18 | { 19 | private static readonly string[] ErrorHeaders = 20 | { "X-Platform-Error-Code", "X-Influx-Error", "X-InfluxDb-Error" }; 21 | 22 | private readonly ClientConfig _config; 23 | private readonly HttpClient _httpClient; 24 | 25 | internal RestClient(ClientConfig config, HttpClient httpClient) 26 | { 27 | _config = config; 28 | _httpClient = httpClient; 29 | } 30 | 31 | internal async Task Request(string path, HttpMethod method, HttpContent? content = null, 32 | Dictionary? queryParams = null, Dictionary? headers = null, 33 | CancellationToken cancellationToken = default) 34 | { 35 | var builder = new UriBuilder(new Uri($"{_config.Host}{path}")); 36 | if (queryParams is not null) 37 | { 38 | var query = queryParams 39 | .Select(param => 40 | { 41 | if (string.IsNullOrEmpty(param.Key) || string.IsNullOrEmpty(param.Value)) 42 | { 43 | return ""; 44 | } 45 | 46 | var key = HttpUtility.UrlEncode(param.Key); 47 | var value = HttpUtility.UrlEncode(param.Value ?? ""); 48 | 49 | return $"{key}={value}"; 50 | }) 51 | .Where(part => !string.IsNullOrEmpty(part)); 52 | builder.Query = string.Join("&", query); 53 | } 54 | 55 | var request = new HttpRequestMessage 56 | { 57 | Method = method, 58 | RequestUri = builder.Uri, 59 | Content = content, 60 | }; 61 | // add request headers 62 | if (headers is not null) 63 | { 64 | foreach (var header in headers) 65 | { 66 | request.Headers.Add(header.Key, header.Value); 67 | } 68 | } 69 | 70 | // add config headers 71 | if (_config.Headers != null) 72 | { 73 | foreach (var header in _config.Headers) 74 | { 75 | if (headers == null || !headers.ContainsKey(header.Key)) 76 | { 77 | request.Headers.Add(header.Key, header.Value); 78 | } 79 | } 80 | } 81 | 82 | var result = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false); 83 | if (!result.IsSuccessStatusCode) 84 | { 85 | string? message = null; 86 | var body = await result.Content.ReadAsStringAsync().ConfigureAwait(true); 87 | // error message in body 88 | if (!string.IsNullOrEmpty(body)) 89 | { 90 | using var memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(body)); 91 | try 92 | { 93 | if (new DataContractJsonSerializer(typeof(ErrorBody)).ReadObject(memoryStream) is ErrorBody 94 | errorBody) 95 | { 96 | if (!string.IsNullOrEmpty(errorBody.Message)) // Cloud 97 | { 98 | message = errorBody.Message; 99 | } 100 | else if ((errorBody.Data is not null) && !string.IsNullOrEmpty(errorBody.Data.ErrorMessage)) // Edge 101 | { 102 | message = errorBody.Data.ErrorMessage; 103 | } 104 | else if (!string.IsNullOrEmpty(errorBody.Error)) // Edge 105 | { 106 | message = errorBody.Error; 107 | } 108 | } 109 | } 110 | catch (SerializationException se) 111 | { 112 | Debug.WriteLine($"Cannot parse error response as JSON: {body}. {se}"); 113 | } 114 | } 115 | 116 | // from header 117 | if (string.IsNullOrEmpty(message)) 118 | { 119 | message = result.Headers? 120 | .Where(header => ErrorHeaders.Contains(header.Key, StringComparer.OrdinalIgnoreCase)) 121 | .Select(header => header.Value.FirstOrDefault()?.ToString()) 122 | .FirstOrDefault(); 123 | } 124 | 125 | // whole body 126 | if (string.IsNullOrEmpty(message)) 127 | { 128 | message = body; 129 | } 130 | 131 | // reason 132 | if (string.IsNullOrEmpty(message)) 133 | { 134 | message = result.ReasonPhrase; 135 | } 136 | 137 | throw new InfluxDBApiException(message ?? "Cannot write data to InfluxDB.", result); 138 | } 139 | } 140 | } 141 | 142 | [DataContract] 143 | internal class ErrorBody 144 | { 145 | [DataMember(Name = "message")] 146 | public string? Message { get; set; } 147 | 148 | [DataMember(Name = "error")] 149 | public string? Error { get; set; } 150 | 151 | [DataMember(Name = "data")] 152 | public ErrorData? Data { get; set; } 153 | 154 | [DataContract] 155 | internal class ErrorData 156 | { 157 | [DataMember(Name = "error_message")] 158 | public string? ErrorMessage { get; set; } 159 | } 160 | } -------------------------------------------------------------------------------- /Client/Internal/ServerCertificateCustomValidations.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Net.Http; 6 | using System.Net.Security; 7 | using System.Security.Cryptography.X509Certificates; 8 | 9 | namespace InfluxDB3.Client.Internal; 10 | 11 | using ValidationCallback = Func; 12 | 13 | internal static class ServerCertificateCustomValidations 14 | { 15 | /// 16 | /// Create a ServerCertificateCustomValidationCallback function that ignores certificate validation errors. 17 | /// 18 | internal static ValidationCallback CreateSkipValidationCallback() 19 | { 20 | return (_, _, _, _) => true; 21 | } 22 | 23 | /// 24 | /// Create a ServerCertificateCustomValidationCallback function that uses additional root certificates for validation. 25 | /// 26 | internal static ValidationCallback CreateCustomCertificatesValidationCallback(string customCertsFilePath, 27 | bool disableRevocationChecks) 28 | { 29 | // Check custom certificates file 30 | if (!File.Exists(customCertsFilePath)) 31 | { 32 | throw new ArgumentException($"Certificate file '{customCertsFilePath}' not found."); 33 | } 34 | 35 | var fileInfo = new FileInfo(customCertsFilePath); 36 | if (fileInfo.Length == 0) 37 | { 38 | throw new ArgumentException($"Certificate file '{customCertsFilePath}' is empty."); 39 | } 40 | 41 | // Load custom certificates 42 | var customCerts = new X509Certificate2Collection(); 43 | try 44 | { 45 | customCerts.Import(customCertsFilePath); 46 | } 47 | catch (Exception ex) 48 | { 49 | throw new Exception($"Failed to import custom certificates from '{customCertsFilePath}': {ex.Message}", ex); 50 | } 51 | 52 | return (_, certificate, chain, sslErrors) => 53 | { 54 | if (sslErrors == SslPolicyErrors.None) 55 | { 56 | // No errors, certificate is valid 57 | return true; 58 | } 59 | 60 | if ((sslErrors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0) 61 | { 62 | // Certificate is not valid due to RemoteCertificateNotAvailable or RemoteCertificateNameMismatch 63 | return false; 64 | } 65 | 66 | if (certificate == null || chain == null) 67 | { 68 | // Certificate missing 69 | return false; 70 | } 71 | 72 | // Certificate validation failed due to chain errors, revalidate the certificate with custom certificates. 73 | var newChain = new X509Chain(); 74 | newChain.ChainPolicy.ExtraStore.AddRange(customCerts); 75 | var isValid = newChain.Build(certificate); 76 | if (isValid) return true; 77 | 78 | // Collect relevant error statuses. 79 | var hasSelfSignedRoot = IsRootCertificateSelfSigned(newChain); 80 | var errorStatuses = GetFilteredChainStatuses(newChain, (element, status) => 81 | { 82 | // Ignore UntrustedRoot errors for root certificates from the user-provided custom 83 | // certificates file. These certificates are explicitly trusted by the user. 84 | if (status.Status == X509ChainStatusFlags.UntrustedRoot && 85 | ContainsCertificateWithThumbprint(customCerts, element.Certificate.Thumbprint)) 86 | return false; 87 | 88 | // Ignore RevocationStatusUnknown errors for certificates with self-signed roots. 89 | // Self-signed certificates typically don't publish revocation information. 90 | if (status.Status == X509ChainStatusFlags.RevocationStatusUnknown && hasSelfSignedRoot) 91 | { 92 | return false; 93 | } 94 | 95 | // Ignore revocation-related errors is certificate revocation list (CRL) checks are disabled. 96 | if (disableRevocationChecks && status.Status is X509ChainStatusFlags.OfflineRevocation 97 | or X509ChainStatusFlags.RevocationStatusUnknown) 98 | { 99 | return false; 100 | } 101 | 102 | // Ignore NoError statuses. 103 | return status.Status != X509ChainStatusFlags.NoError; 104 | }); 105 | if (errorStatuses.Count == 0) return true; 106 | 107 | // Log certificate validation errors and then return false. 108 | foreach (var status in errorStatuses) 109 | { 110 | Trace.TraceWarning($"Certificate chain validation failed: {status.Status}: {status.StatusInformation}"); 111 | } 112 | 113 | return false; 114 | }; 115 | } 116 | 117 | private static List GetFilteredChainStatuses(X509Chain chain, 118 | Func filter) 119 | { 120 | var filtered = new List(); 121 | foreach (var element in chain.ChainElements) 122 | { 123 | if (element.ChainElementStatus == null) continue; 124 | foreach (var status in element.ChainElementStatus) 125 | { 126 | if (filter(element, status)) 127 | { 128 | filtered.Add(status); 129 | } 130 | } 131 | } 132 | 133 | return filtered; 134 | } 135 | 136 | internal static bool ContainsCertificateWithThumbprint(X509Certificate2Collection certificates, 137 | string? certificateThumbprint) 138 | { 139 | if (string.IsNullOrEmpty(certificateThumbprint)) 140 | { 141 | return false; 142 | } 143 | 144 | foreach (var certificate in certificates) 145 | { 146 | if (certificate.Thumbprint != null && 147 | certificate.Thumbprint.Equals(certificateThumbprint, StringComparison.OrdinalIgnoreCase)) 148 | { 149 | return true; 150 | } 151 | } 152 | 153 | return false; 154 | } 155 | 156 | internal static bool IsRootCertificateSelfSigned(X509Chain chain) 157 | { 158 | if (chain.ChainElements == null || chain.ChainElements.Count == 0) 159 | { 160 | return false; 161 | } 162 | 163 | // The last certificate should be the root certificate 164 | var rootCertificate = chain.ChainElements[chain.ChainElements.Count - 1].Certificate; 165 | 166 | return rootCertificate != null && rootCertificate.Issuer == rootCertificate.Subject; 167 | } 168 | } -------------------------------------------------------------------------------- /Client/Internal/TimestampConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Numerics; 3 | 4 | namespace InfluxDB3.Client.Internal; 5 | 6 | internal static class TimestampConverter 7 | { 8 | private static readonly DateTime EpochStart = new(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); 9 | 10 | /// 11 | /// Get nano time from Datetime and EpochStart time . 12 | /// 13 | /// the Datetime object 14 | /// the time in nanosecond 15 | internal static BigInteger GetNanoTime(DateTime dateTime) 16 | { 17 | var utcTimestamp = dateTime.Kind switch 18 | { 19 | DateTimeKind.Local => dateTime.ToUniversalTime(), 20 | DateTimeKind.Unspecified => DateTime.SpecifyKind(dateTime, DateTimeKind.Utc), 21 | _ => dateTime 22 | }; 23 | return utcTimestamp.Subtract(EpochStart).Ticks * 100; 24 | } 25 | } -------------------------------------------------------------------------------- /Client/Internal/TypeCasting.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Numerics; 4 | using Apache.Arrow; 5 | 6 | namespace InfluxDB3.Client.Internal; 7 | 8 | public class TypeCasting 9 | { 10 | /// 11 | /// Function to cast value return based on metadata from InfluxDB. 12 | /// 13 | /// The Field object from Arrow 14 | /// The value to cast 15 | /// The value with the correct type 16 | public static object? GetMappedValue(Field field, object? value) 17 | { 18 | if (value == null) 19 | return null; 20 | 21 | var fieldName = field.Name; 22 | var metaType = field.HasMetadata ? field.Metadata["iox::column::type"] : null; 23 | if (metaType == null) 24 | { 25 | if (fieldName == "time" && value is DateTimeOffset timeOffset) 26 | { 27 | return TimestampConverter.GetNanoTime(timeOffset.UtcDateTime); 28 | } 29 | 30 | return value; 31 | } 32 | 33 | var parts = metaType.Split(new[] { ':' }, StringSplitOptions.RemoveEmptyEntries); 34 | var valueType = parts[2]; 35 | if (valueType == "field") 36 | { 37 | switch (metaType) 38 | { 39 | case "iox::column_type::field::integer": 40 | if (IsNumber(value)) 41 | { 42 | return Convert.ToInt64(value); 43 | } 44 | 45 | Trace.TraceWarning($"Value [{value}] is not a long"); 46 | return value; 47 | case "iox::column_type::field::uinteger": 48 | if (IsNumber(value) && Convert.ToInt64(value) >= 0) 49 | { 50 | return Convert.ToUInt64(value); 51 | } 52 | 53 | Trace.TraceWarning($"Value [{value}] is not an unsigned long"); 54 | return value; 55 | case "iox::column_type::field::float": 56 | if (IsNumber(value)) 57 | { 58 | return Convert.ToDouble(value); 59 | } 60 | 61 | Trace.TraceWarning($"Value [{value}] is not a double"); 62 | return value; 63 | case "iox::column_type::field::string": 64 | if (value is string) 65 | { 66 | return value; 67 | } 68 | 69 | Trace.TraceWarning($"Value [{value}] is not a string"); 70 | return value; 71 | case "iox::column_type::field::boolean": 72 | if (value is bool) 73 | { 74 | return Convert.ToBoolean(value); 75 | } 76 | 77 | Trace.TraceWarning($"Value [{value}] is not a boolean"); 78 | return value; 79 | default: 80 | return value; 81 | } 82 | } 83 | 84 | if (valueType == "timestamp" && value is DateTimeOffset dateTimeOffset) 85 | { 86 | return TimestampConverter.GetNanoTime(dateTimeOffset.UtcDateTime); 87 | } 88 | 89 | return value; 90 | } 91 | 92 | public static bool IsNumber(object? value) 93 | { 94 | return value is sbyte 95 | or byte 96 | or short 97 | or ushort 98 | or int 99 | or uint 100 | or long 101 | or ulong 102 | or float 103 | or double 104 | or decimal 105 | or BigInteger; 106 | } 107 | } -------------------------------------------------------------------------------- /Client/Query/QueryType.cs: -------------------------------------------------------------------------------- 1 | namespace InfluxDB3.Client.Query; 2 | 3 | /// 4 | /// Defines type of query sent to InfluxDB. 5 | /// 6 | public enum QueryType 7 | { 8 | /// 9 | /// Query by SQL. 10 | /// 11 | SQL = 1, 12 | 13 | /// 14 | /// Query by InfluxQL. 15 | /// 16 | InfluxQL = 2, 17 | } -------------------------------------------------------------------------------- /Client/Write/WritePrecision.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace InfluxDB3.Client.Write; 4 | 5 | /// 6 | /// Defines WritePrecision 7 | /// 8 | public enum WritePrecision 9 | { 10 | /// 11 | /// Enum Ms for value: ms 12 | /// 13 | Ms = 1, 14 | 15 | /// 16 | /// Enum S for value: s 17 | /// 18 | S = 2, 19 | 20 | /// 21 | /// Enum Us for value: us 22 | /// 23 | Us = 3, 24 | 25 | /// 26 | /// Enum Ns for value: ns 27 | /// 28 | Ns = 4 29 | } 30 | 31 | public static class WritePrecisionConverter 32 | { 33 | public static string ToV2ApiString(WritePrecision precision) 34 | { 35 | return precision switch 36 | { 37 | WritePrecision.Ns => "ns", 38 | WritePrecision.Us => "us", 39 | WritePrecision.Ms => "ms", 40 | WritePrecision.S => "s", 41 | _ => throw new ArgumentException($"Unsupported precision '{precision}'"), 42 | }; 43 | } 44 | 45 | public static string ToV3ApiString(WritePrecision precision) 46 | { 47 | return precision switch 48 | { 49 | WritePrecision.Ns => "nanosecond", 50 | WritePrecision.Us => "microsecond", 51 | WritePrecision.Ms => "millisecond", 52 | WritePrecision.S => "second", 53 | _ => throw new ArgumentException($"Unsupported precision '{precision}'"), 54 | }; 55 | } 56 | } -------------------------------------------------------------------------------- /Examples/CustomSslCerts/CustomSslCerts.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 10 6 | enable 7 | 8 | false 9 | InfluxDB3.Examples.CustomSslCerts 10 | InfluxDB3.Examples.CustomSslCerts 11 | 12 | ../../Keys/Key.snk 13 | true 14 | 15 | Exe 16 | InfluxDB3.Examples.CustomSslCerts.CustomSslCertsExample 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Examples/CustomSslCerts/CustomSslCertsExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using InfluxDB3.Client; 6 | using InfluxDB3.Client.Config; 7 | using InfluxDB3.Client.Query; 8 | using InfluxDB3.Client.Write; 9 | 10 | namespace InfluxDB3.Examples.CustomSslCerts; 11 | 12 | public class CustomSslCertsExample 13 | { 14 | static async Task Main(string[] args) 15 | { 16 | var host = Environment.GetEnvironmentVariable("INFLUXDB_URL") ?? 17 | "https://us-east-1-1.aws.cloud2.influxdata.com"; 18 | var token = Environment.GetEnvironmentVariable("INFLUXDB_TOKEN") ?? "my-token"; 19 | var database = Environment.GetEnvironmentVariable("INFLUXDB_DATABASE") ?? "my-database"; 20 | var sslRootsFilePath = Environment.GetEnvironmentVariable("INFLUXDB_SSL_ROOTS_FILE_PATH") ?? null; 21 | var proxyUrl = Environment.GetEnvironmentVariable("INFLUXDB_PROXY_URL") ?? null; 22 | 23 | using var client = new InfluxDBClient(new ClientConfig 24 | { 25 | Host = host, 26 | Token = token, 27 | Database = database, 28 | SslRootsFilePath = sslRootsFilePath, 29 | Proxy = proxyUrl == null 30 | ? null 31 | : new WebProxy 32 | { 33 | Address = new Uri(proxyUrl), 34 | BypassProxyOnLocal = false 35 | } 36 | }); 37 | 38 | // 39 | // Write by Point 40 | // 41 | var point = PointData.Measurement("temperature") 42 | .SetTag("location", "west") 43 | .SetField("value", 55.15) 44 | .SetTimestamp(DateTime.UtcNow.AddSeconds(-10)); 45 | await client.WritePointAsync(point: point); 46 | 47 | // 48 | // Write by LineProtocol 49 | // 50 | const string record = "temperature,location=north value=60.0"; 51 | await client.WriteRecordAsync(record: record); 52 | 53 | 54 | // 55 | // Query by SQL 56 | // 57 | const string sql = "select time,location,value from temperature order by time desc limit 10"; 58 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", "time", "location", "value"); 59 | await foreach (var row in client.Query(query: sql)) 60 | { 61 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", row[0], row[1], row[2]); 62 | } 63 | 64 | // 65 | // Query by parametrized SQL 66 | // 67 | const string sqlParams = 68 | "select time,location,value from temperature where location=$location order by time desc limit 10"; 69 | Console.WriteLine("Query by parametrized SQL"); 70 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", "time", "location", "value"); 71 | await foreach (var row in client.Query(query: sqlParams, 72 | namedParameters: new Dictionary { { "location", "west" } })) 73 | { 74 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", row[0], row[1], row[2]); 75 | } 76 | 77 | 78 | // 79 | // Query by InfluxQL 80 | // 81 | const string influxQL = 82 | "select MEAN(value) from temperature group by time(1d) fill(none) order by time desc limit 10"; 83 | Console.WriteLine("{0,-30}{1,-15}", "time", "mean"); 84 | await foreach (var row in client.Query(query: influxQL, queryType: QueryType.InfluxQL)) 85 | { 86 | Console.WriteLine("{0,-30}{1,-15}", row[1], row[2]); 87 | } 88 | 89 | // 90 | // SQL Query all PointDataValues 91 | // 92 | const string sql2 = "select *, 'temperature' as measurement from temperature order by time desc limit 5"; 93 | Console.WriteLine(); 94 | Console.WriteLine("simple query to points with measurement manually specified"); 95 | await foreach (var row in client.QueryPoints(query: sql2, queryType: QueryType.SQL)) 96 | { 97 | Console.WriteLine(row.AsPoint().ToLineProtocol()); 98 | } 99 | 100 | // 101 | // SQL Query windows 102 | // 103 | const string sql3 = @" 104 | SELECT 105 | date_bin('5 minutes', ""time"") as time, 106 | AVG(""value"") as avgvalue 107 | FROM ""temperature"" 108 | WHERE 109 | ""time"" >= now() - interval '1 hour' 110 | GROUP BY time 111 | ORDER BY time DESC 112 | limit 3 113 | ; 114 | "; 115 | Console.WriteLine(); 116 | Console.WriteLine("more complex query to points WITHOUT measurement manually specified"); 117 | await foreach (var row in client.QueryPoints(query: sql3, queryType: QueryType.SQL)) 118 | { 119 | Console.WriteLine(row.AsPoint("measurement").ToLineProtocol()); 120 | } 121 | 122 | Console.WriteLine(); 123 | Console.WriteLine("simple InfluxQL query to points. InfluxQL sends measurement in query"); 124 | await foreach (var row in client.QueryPoints(query: influxQL, queryType: QueryType.InfluxQL)) 125 | { 126 | Console.WriteLine(row.AsPoint().ToLineProtocol()); 127 | } 128 | } 129 | 130 | public static async Task Run() 131 | { 132 | await Main(Array.Empty()); 133 | } 134 | } -------------------------------------------------------------------------------- /Examples/Downsampling/Downsampling.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 10 6 | enable 7 | 8 | false 9 | InfluxDB3.Examples.Downsampling 10 | InfluxDB3.Examples.Downsampling 11 | 12 | ../../Keys/Key.snk 13 | true 14 | 15 | Exe 16 | InfluxDB3.Examples.Downsampling.DownsamplingExample 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Examples/Downsampling/DownsamplingExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using InfluxDB3.Client; 5 | 6 | namespace InfluxDB3.Examples.Downsampling; 7 | 8 | public class DownsamplingExample 9 | { 10 | static async Task Main(string[] args) 11 | { 12 | var host = Environment.GetEnvironmentVariable("INFLUXDB_URL") ?? "https://us-east-1-1.aws.cloud2.influxdata.com"; 13 | var token = Environment.GetEnvironmentVariable("INFLUXDB_TOKEN") ?? "my-token"; 14 | var database = Environment.GetEnvironmentVariable("INFLUXDB_DATABASE") ?? "my-database"; 15 | 16 | using var client = new InfluxDBClient(host: host, token: token, database: database); 17 | 18 | // 19 | // Write data 20 | // 21 | await client.WriteRecordAsync("stat,unit=temperature avg=24.5,max=45.0"); 22 | Thread.Sleep(1_000); 23 | 24 | await client.WriteRecordAsync("stat,unit=temperature avg=28,max=40.3"); 25 | Thread.Sleep(1_000); 26 | 27 | await client.WriteRecordAsync("stat,unit=temperature avg=20.5,max=49.0"); 28 | Thread.Sleep(1_000); 29 | 30 | // 31 | // Query downsampled data 32 | // 33 | const string downsamplingQuery = @"SELECT 34 | date_bin('5 minutes', ""time"") as window_start, 35 | AVG(""avg"") as avg, 36 | MAX(""max"") as max 37 | FROM ""stat"" 38 | WHERE 39 | ""time"" >= now() - interval '1 hour' 40 | GROUP BY window_start 41 | ORDER BY window_start ASC; 42 | "; 43 | 44 | // 45 | // Execute downsampling query into pointValues 46 | // 47 | await foreach (var row in client.QueryPoints(downsamplingQuery)) 48 | { 49 | var timestamp = row.GetField("window_start") ?? throw new InvalidOperationException(); 50 | Console.WriteLine($"{timestamp}: avg is {row.GetDoubleField("avg")}, max is {row.GetDoubleField("max")}"); 51 | 52 | // 53 | // write back downsampled date to 'stat_downsampled' measurement 54 | // 55 | var downsampledPoint = row 56 | .AsPoint("stat_downsampled") 57 | .RemoveField("window_start") 58 | .SetTimestamp(timestamp); 59 | 60 | await client.WritePointAsync(downsampledPoint); 61 | } 62 | } 63 | 64 | public static async Task Run() 65 | { 66 | await Main(Array.Empty()); 67 | } 68 | } -------------------------------------------------------------------------------- /Examples/General/General.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net8.0 6 | enable 7 | enable 8 | InfluxDB3.Examples.General 9 | InfluxDB3.Examples.General 10 | InfluxDB3.Examples.General.Runner 11 | ../../Keys/Key.snk 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Examples/General/HttpErrorHandled.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Frozen; 2 | using InfluxDB3.Client; 3 | using Console = System.Console; 4 | 5 | namespace InfluxDB3.Examples.General; 6 | 7 | public class HttpErrorHandled 8 | { 9 | public static async Task Run() 10 | { 11 | Console.WriteLine("HttpErrorHandled"); 12 | using var client = new InfluxDBClient(host: Runner.Host, 13 | token: Runner.Token, 14 | database: Runner.Database); 15 | 16 | Console.WriteLine("Writing record"); 17 | 18 | try 19 | { 20 | await client.WriteRecordAsync("vehicle,id=harfa vel=89.7,load=355i,state="); 21 | } 22 | catch (Exception ex) 23 | { 24 | if (ex is InfluxDBApiException) 25 | { 26 | InfluxDBApiException apiEx = (InfluxDBApiException)ex; 27 | Console.WriteLine("Caught ApiException: {0} \"{1}\"", 28 | apiEx.StatusCode, apiEx.Message); 29 | var headers = apiEx.Headers!.ToFrozenDictionary(); 30 | Console.WriteLine("Headers:"); 31 | foreach (var header in headers) 32 | { 33 | Console.WriteLine(" {0}: {1}", header.Key, header.Value.First()); 34 | } 35 | } 36 | else 37 | { 38 | throw new Exception($"Unexpected Exception: {ex.Message}", ex); 39 | } 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /Examples/General/Runner.cs: -------------------------------------------------------------------------------- 1 | using InfluxDB3.Examples.Downsampling; 2 | using InfluxDB3.Examples.IOx; 3 | using InfluxDB3.Examples.CustomSslCerts; 4 | 5 | namespace InfluxDB3.Examples.General; 6 | 7 | public class Runner 8 | { 9 | 10 | public static string Host { get; private set; } = Environment.GetEnvironmentVariable("INFLUXDB_URL") ?? 11 | "http://localhost:8086"; 12 | public static string Token { get; private set; } = Environment.GetEnvironmentVariable("INFLUXDB_TOKEN") ?? 13 | "my-token"; 14 | 15 | public static string Database { get; private set; } = Environment.GetEnvironmentVariable("INFLUXDB_DATABASE") ?? 16 | "my-database"; 17 | 18 | private static readonly Dictionary> Functions = new Dictionary>() 19 | { 20 | {"DownSampling", DownsamplingExample.Run}, 21 | {"HttpErrorHandled", HttpErrorHandled.Run}, 22 | {"IOx", IOxExample.Run}, 23 | {"CustomSslCerts", CustomSslCertsExample.Run} 24 | }; 25 | private static void Help() 26 | { 27 | Console.WriteLine("Usage:"); 28 | Console.WriteLine(" General.exe "); 29 | Console.WriteLine(" Available examples:"); 30 | foreach (var key in Functions.Keys) 31 | { 32 | Console.WriteLine($" {key}"); 33 | } 34 | } 35 | 36 | public static async Task Main(string[] args) 37 | { 38 | Console.WriteLine("Starting runner... args {0}", args.Length); 39 | if (args.Length != 1) 40 | { 41 | Console.WriteLine("InfluxDB Example Runner requires a single argument."); 42 | Help(); 43 | Environment.Exit(1); 44 | } 45 | 46 | try 47 | { 48 | await Functions[args[0]](); 49 | } 50 | catch (KeyNotFoundException) 51 | { 52 | Console.WriteLine($"Unknown example: {args[0]}"); 53 | Help(); 54 | Environment.Exit(1); 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /Examples/IOx/Examples.IOx.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net8.0 5 | 10 6 | enable 7 | 8 | false 9 | InfluxDB3.Examples.IOx 10 | InfluxDB3.Examples.IOx 11 | 12 | ../../Keys/Key.snk 13 | true 14 | 15 | Exe 16 | InfluxDB3.Examples.IOx.IOxExample 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /Examples/IOx/IOxExample.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using InfluxDB3.Client; 5 | using InfluxDB3.Client.Query; 6 | using InfluxDB3.Client.Write; 7 | 8 | namespace InfluxDB3.Examples.IOx; 9 | 10 | public class IOxExample 11 | { 12 | static async Task Main(string[] args) 13 | { 14 | var host = Environment.GetEnvironmentVariable("INFLUXDB_URL") ?? "https://us-east-1-1.aws.cloud2.influxdata.com"; 15 | var token = Environment.GetEnvironmentVariable("INFLUXDB_TOKEN") ?? "my-token"; 16 | var database = Environment.GetEnvironmentVariable("INFLUXDB_DATABASE") ?? "my-database"; 17 | 18 | using var client = new InfluxDBClient(host: host, token: token, database: database); 19 | 20 | // 21 | // Write by Point 22 | // 23 | var point = PointData.Measurement("temperature") 24 | .SetTag("location", "west") 25 | .SetField("value", 55.15) 26 | .SetTimestamp(DateTime.UtcNow.AddSeconds(-10)); 27 | await client.WritePointAsync(point: point); 28 | 29 | // 30 | // Write by LineProtocol 31 | // 32 | const string record = "temperature,location=north value=60.0"; 33 | await client.WriteRecordAsync(record: record); 34 | 35 | // 36 | // Query by SQL 37 | // 38 | const string sql = "select time,location,value from temperature order by time desc limit 10"; 39 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", "time", "location", "value"); 40 | await foreach (var row in client.Query(query: sql)) 41 | { 42 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", row[0], row[1], row[2]); 43 | } 44 | 45 | // 46 | // Query by parametrized SQL 47 | // 48 | const string sqlParams = "select time,location,value from temperature where location=$location order by time desc limit 10"; 49 | Console.WriteLine("Query by parametrized SQL"); 50 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", "time", "location", "value"); 51 | await foreach (var row in client.Query(query: sqlParams, namedParameters: new Dictionary { { "location", "west" } })) 52 | { 53 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", row[0], row[1], row[2]); 54 | } 55 | 56 | 57 | // 58 | // Query by InfluxQL 59 | // 60 | const string influxQL = 61 | "select MEAN(value) from temperature group by time(1d) fill(none) order by time desc limit 10"; 62 | Console.WriteLine("{0,-30}{1,-15}", "time", "mean"); 63 | await foreach (var row in client.Query(query: influxQL, queryType: QueryType.InfluxQL)) 64 | { 65 | Console.WriteLine("{0,-30}{1,-15}", row[1], row[2]); 66 | } 67 | 68 | // 69 | // SQL Query all PointDataValues 70 | // 71 | const string sql2 = "select *, 'temperature' as measurement from temperature order by time desc limit 5"; 72 | Console.WriteLine(); 73 | Console.WriteLine("simple query to points with measurement manually specified"); 74 | await foreach (var row in client.QueryPoints(query: sql2, queryType: QueryType.SQL)) 75 | { 76 | Console.WriteLine(row.AsPoint().ToLineProtocol()); 77 | } 78 | 79 | // 80 | // SQL Query windows 81 | // 82 | const string sql3 = @" 83 | SELECT 84 | date_bin('5 minutes', ""time"") as time, 85 | AVG(""value"") as avgvalue 86 | FROM ""temperature"" 87 | WHERE 88 | ""time"" >= now() - interval '1 hour' 89 | GROUP BY time 90 | ORDER BY time DESC 91 | limit 3 92 | ; 93 | "; 94 | Console.WriteLine(); 95 | Console.WriteLine("more complex query to points WITHOUT measurement manually specified"); 96 | await foreach (var row in client.QueryPoints(query: sql3, queryType: QueryType.SQL)) 97 | { 98 | Console.WriteLine(row.AsPoint("measurement").ToLineProtocol()); 99 | } 100 | 101 | Console.WriteLine(); 102 | Console.WriteLine("simple InfluxQL query to points. InfluxQL sends measurement in query"); 103 | await foreach (var row in client.QueryPoints(query: influxQL, queryType: QueryType.InfluxQL)) 104 | { 105 | Console.WriteLine(row.AsPoint().ToLineProtocol()); 106 | } 107 | } 108 | 109 | public static async Task Run() 110 | { 111 | await Main(Array.Empty()); 112 | } 113 | } -------------------------------------------------------------------------------- /Examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | - [General](General/Runner.cs) - for running other examples from the commandline. 4 | - [HttpErrorHandled](General/HttpErrorHandled.cs) - Accessing HTTP headers when an InfluxDBApiException is thrown. 5 | - [IOxExample](IOx/IOxExample.cs) - How to use write and query data from InfluxDB IOx 6 | - [Downsampling](Downsampling/DownsamplingExample.cs) - How to use queries to structure data for downsampling 7 | - [CustomSslCerts](CustomSslCerts/CustomSslCertsExample.cs) - How to configure custom SSL root certificates and proxy in 8 | client 9 | 10 | ## General Runner 11 | 12 | Examples can be run from the directory `Examples/General` by simply calling `dotnet run` 13 | 14 | For example: 15 | 16 | ```bash 17 | Examples/General/$ dotnet run "HttpErrorHandled" 18 | ``` 19 | -------------------------------------------------------------------------------- /InfluxDB3.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client", "Client\Client.csproj", "{B5669ECE-B3E4-444A-8F06-6E547B6A80C7}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Test", "Client.Test\Client.Test.csproj", "{2B458947-5B2B-4B58-A3F0-758C0B09217E}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Client.Test.Integration", "Client.Test.Integration\Client.Test.Integration.csproj", "{4BE03DEE-47BF-4F7B-8474-4214E643D72E}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CustomSslCerts", "Examples\CustomSslCerts\CustomSslCerts.csproj", "{2633D8DB-B03F-4F3E-846D-B16C4D4AA6E5}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{EFBEAA10-411F-4BDB-A96B-B54632FAE99D}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Examples.IOx", "Examples\IOx\Examples.IOx.csproj", "{B2DA69ED-C21D-4531-A32A-E537DC511785}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Downsampling", "Examples\Downsampling\Downsampling.csproj", "{44C91C8C-B194-48CE-A5B1-763457F118F4}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "General", "Examples\General\General.csproj", "{2355049F-531D-4B43-9DFD-D88C75023B04}" 21 | EndProject 22 | Global 23 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 24 | Debug|Any CPU = Debug|Any CPU 25 | Release|Any CPU = Release|Any CPU 26 | EndGlobalSection 27 | GlobalSection(SolutionProperties) = preSolution 28 | HideSolutionNode = FALSE 29 | EndGlobalSection 30 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 31 | {B5669ECE-B3E4-444A-8F06-6E547B6A80C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 32 | {B5669ECE-B3E4-444A-8F06-6E547B6A80C7}.Debug|Any CPU.Build.0 = Debug|Any CPU 33 | {B5669ECE-B3E4-444A-8F06-6E547B6A80C7}.Release|Any CPU.ActiveCfg = Release|Any CPU 34 | {B5669ECE-B3E4-444A-8F06-6E547B6A80C7}.Release|Any CPU.Build.0 = Release|Any CPU 35 | {2B458947-5B2B-4B58-A3F0-758C0B09217E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 36 | {2B458947-5B2B-4B58-A3F0-758C0B09217E}.Debug|Any CPU.Build.0 = Debug|Any CPU 37 | {2B458947-5B2B-4B58-A3F0-758C0B09217E}.Release|Any CPU.ActiveCfg = Release|Any CPU 38 | {2B458947-5B2B-4B58-A3F0-758C0B09217E}.Release|Any CPU.Build.0 = Release|Any CPU 39 | {4BE03DEE-47BF-4F7B-8474-4214E643D72E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 40 | {4BE03DEE-47BF-4F7B-8474-4214E643D72E}.Debug|Any CPU.Build.0 = Debug|Any CPU 41 | {4BE03DEE-47BF-4F7B-8474-4214E643D72E}.Release|Any CPU.ActiveCfg = Release|Any CPU 42 | {4BE03DEE-47BF-4F7B-8474-4214E643D72E}.Release|Any CPU.Build.0 = Release|Any CPU 43 | {B2DA69ED-C21D-4531-A32A-E537DC511785}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 44 | {B2DA69ED-C21D-4531-A32A-E537DC511785}.Debug|Any CPU.Build.0 = Debug|Any CPU 45 | {B2DA69ED-C21D-4531-A32A-E537DC511785}.Release|Any CPU.ActiveCfg = Release|Any CPU 46 | {B2DA69ED-C21D-4531-A32A-E537DC511785}.Release|Any CPU.Build.0 = Release|Any CPU 47 | {44C91C8C-B194-48CE-A5B1-763457F118F4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 48 | {44C91C8C-B194-48CE-A5B1-763457F118F4}.Debug|Any CPU.Build.0 = Debug|Any CPU 49 | {44C91C8C-B194-48CE-A5B1-763457F118F4}.Release|Any CPU.ActiveCfg = Release|Any CPU 50 | {44C91C8C-B194-48CE-A5B1-763457F118F4}.Release|Any CPU.Build.0 = Release|Any CPU 51 | {2355049F-531D-4B43-9DFD-D88C75023B04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {2355049F-531D-4B43-9DFD-D88C75023B04}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {2355049F-531D-4B43-9DFD-D88C75023B04}.Release|Any CPU.ActiveCfg = Release|Any CPU 54 | {2355049F-531D-4B43-9DFD-D88C75023B04}.Release|Any CPU.Build.0 = Release|Any CPU 55 | {2633D8DB-B03F-4F3E-846D-B16C4D4AA6E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 56 | {2633D8DB-B03F-4F3E-846D-B16C4D4AA6E5}.Debug|Any CPU.Build.0 = Debug|Any CPU 57 | {2633D8DB-B03F-4F3E-846D-B16C4D4AA6E5}.Release|Any CPU.ActiveCfg = Release|Any CPU 58 | {2633D8DB-B03F-4F3E-846D-B16C4D4AA6E5}.Release|Any CPU.Build.0 = Release|Any CPU 59 | EndGlobalSection 60 | GlobalSection(NestedProjects) = preSolution 61 | {B2DA69ED-C21D-4531-A32A-E537DC511785} = {EFBEAA10-411F-4BDB-A96B-B54632FAE99D} 62 | {44C91C8C-B194-48CE-A5B1-763457F118F4} = {EFBEAA10-411F-4BDB-A96B-B54632FAE99D} 63 | {2355049F-531D-4B43-9DFD-D88C75023B04} = {EFBEAA10-411F-4BDB-A96B-B54632FAE99D} 64 | {2633D8DB-B03F-4F3E-846D-B16C4D4AA6E5} = {EFBEAA10-411F-4BDB-A96B-B54632FAE99D} 65 | EndGlobalSection 66 | EndGlobal 67 | -------------------------------------------------------------------------------- /Keys/Key.public.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-csharp/71d1c5919f9e1fa1cc798f8e8b397fb9c6de6e62/Keys/Key.public.snk -------------------------------------------------------------------------------- /Keys/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-csharp/71d1c5919f9e1fa1cc798f8e8b397fb9c6de6e62/Keys/Key.snk -------------------------------------------------------------------------------- /Keys/README.md: -------------------------------------------------------------------------------- 1 | Contents 2 | -------- 3 | 4 | - Key.public.snk: Public key to verify strong name of `InfluxDB3.Client`. 5 | - Key.snk: Signing key to provide strong name of `InfluxDB3.Client`. 6 | 7 | [Microsoft guidance: Strong-named assemblies](https://msdn.microsoft.com/en-us/library/wd40t7ad(v=vs.110).aspx) 8 | 9 | Docker 10 | ------ 11 | 12 | ```shell 13 | # Get Public Key 14 | docker run -it -v "${PWD}:/opt/app" -w "/opt/app" mono:latest sn -tp Keys/Key.public.snk 15 | ``` 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 InfluxData Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | .NET Logo 3 |

4 |

5 | 6 | NuGet Badge 7 | 8 | 9 | docfx 10 | 11 | 12 | CodeQL analysis 13 | 14 | 15 | Lint Code Base 16 | 17 | 18 | CircleCI 19 | 20 | 21 | Code Cov 22 | 23 | 24 | Community Slack 25 | 26 |

27 | 28 | # InfluxDB 3 C# .NET Client 29 | 30 | The C# .NET client that provides an easy and convenient way to interact with InfluxDB 3. 31 | This package supports both writing data to InfluxDB and querying data using the FlightSQL client, 32 | which allows you to execute SQL queries against InfluxDB IOx. 33 | 34 | We offer this [Getting Started: InfluxDB 3.0 C# Client Library](https://www.youtube.com/watch?v=l2e4lXilvLA) video to learn more about the library. 35 | 36 | ## Installation 37 | 38 | Add the latest version of the client to your project: 39 | 40 | ```sh 41 | dotnet add package InfluxDB3.Client 42 | ``` 43 | 44 | ## Usage 45 | 46 | To start with the client, import the `InfluxDB3.Client` package and create a `InfluxDBClient` by constructor initializer: 47 | 48 | ```csharp 49 | using System.Threading.Tasks; 50 | using InfluxDB3.Client; 51 | using InfluxDB3.Client.Write; 52 | 53 | namespace InfluxDB3.Examples.IOx; 54 | 55 | public class IOxExample 56 | { 57 | static async Task Main(string[] args) 58 | { 59 | const string host = "https://us-east-1-1.aws.cloud2.influxdata.com"; 60 | const string token = "my-token"; 61 | const string database = "my-database"; 62 | 63 | using var client = new InfluxDBClient(host, token: token, database: database); 64 | } 65 | } 66 | ``` 67 | 68 | to insert data, you can use code like this: 69 | 70 | ```csharp 71 | // 72 | // Write by Point 73 | // 74 | var point = PointData.Measurement("temperature") 75 | .SetTag("location", "west") 76 | .SetField("value", 55.15) 77 | .SetTimestamp(DateTime.UtcNow.AddSeconds(-10)); 78 | await client.WritePointAsync(point: point); 79 | 80 | // 81 | // Write by LineProtocol 82 | // 83 | const string record = "temperature,location=north value=60.0"; 84 | await client.WriteRecordAsync(record: record); 85 | ``` 86 | 87 | to query your data, you can use code like this: 88 | 89 | ```csharp 90 | // 91 | // Query by SQL 92 | // 93 | const string sql = "select time,location,value from temperature order by time desc limit 10"; 94 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", "time", "location", "value"); 95 | await foreach (var row in client.Query(query: sql)) 96 | { 97 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", row[0], row[1], row[2]); 98 | } 99 | Console.WriteLine(); 100 | 101 | // 102 | // Query by parametrized SQL 103 | // 104 | const string sqlParams = "select time,location,value from temperature where location=$location order by time desc limit 10"; 105 | Console.WriteLine("Query by parametrized SQL"); 106 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", "time", "location", "value"); 107 | await foreach (var row in client.Query(query: sqlParams, namedParameters: new Dictionary { { "location", "west" } })) 108 | { 109 | Console.WriteLine("{0,-30}{1,-15}{2,-15}", row[0], row[1], row[2]); 110 | } 111 | Console.WriteLine(); 112 | 113 | // 114 | // Query by InfluxQL 115 | // 116 | const string influxQL = 117 | "select MEAN(value) from temperature group by time(1d) fill(none) order by time desc limit 10"; 118 | Console.WriteLine("{0,-30}{1,-15}", "time", "mean"); 119 | await foreach (var row in client.Query(query: influxQL, queryType: QueryType.InfluxQL)) 120 | { 121 | Console.WriteLine("{0,-30}{1,-15}", row[1], row[2]); 122 | } 123 | ``` 124 | 125 | ## Feedback 126 | 127 | If you need help, please use our [Community Slack](https://app.slack.com/huddle/TH8RGQX5Z/C02UDUPLQKA) 128 | or [Community Page](https://community.influxdata.com/). 129 | 130 | New features and bugs can be reported on GitHub: 131 | 132 | ## Contribution 133 | 134 | If you would like to contribute code you can do through GitHub by forking the repository and sending a pull request into 135 | the `main` branch. 136 | 137 | ## License 138 | 139 | The InfluxDB 3 C# .NET Client is released under the [MIT License](https://opensource.org/licenses/MIT). 140 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # folder # 3 | ############### 4 | /**/DROP/ 5 | /**/TEMP/ 6 | /**/packages/ 7 | /**/bin/ 8 | /**/obj/ 9 | _site 10 | -------------------------------------------------------------------------------- /docs/api/.gitignore: -------------------------------------------------------------------------------- 1 | ############### 2 | # temp file # 3 | ############### 4 | *.yml 5 | .manifest 6 | -------------------------------------------------------------------------------- /docs/docfx.json: -------------------------------------------------------------------------------- 1 | { 2 | "metadata": [ 3 | { 4 | "src": [ 5 | { 6 | "files": [ 7 | "**/Client/**.csproj" 8 | ], 9 | "src": ".." 10 | } 11 | ], 12 | "dest": "api", 13 | "disableGitFeatures": false, 14 | "properties": { 15 | "TargetFramework": "netstandard2.1" 16 | } 17 | } 18 | ], 19 | "build": { 20 | "content": [ 21 | { 22 | "files": [ 23 | "api/**.yml", 24 | "api/index.md" 25 | ] 26 | }, 27 | { 28 | "files": [ 29 | "toc.yml", 30 | "*.md" 31 | ] 32 | } 33 | ], 34 | "resource": [ 35 | { 36 | "files": [ 37 | "images/**", 38 | "favicon.ico" 39 | ] 40 | } 41 | ], 42 | "overwrite": [ 43 | { 44 | "files": [ 45 | ], 46 | "exclude": [ 47 | "obj/**", 48 | "_site/**" 49 | ] 50 | } 51 | ], 52 | "dest": "_site", 53 | "globalMetadataFiles": [], 54 | "fileMetadataFiles": [], 55 | "template": [ 56 | "statictoc" 57 | ], 58 | "globalMetadata": { 59 | "_appLogoPath": "images/influxdata-logo--symbol--white.png", 60 | "_disableContribution": true 61 | }, 62 | "postProcessors": [], 63 | "markdownEngineName": "markdig", 64 | "noLangKeyword": false, 65 | "keepFileLink": false, 66 | "cleanupCacheHistory": false, 67 | "disableGitFeatures": false 68 | } 69 | } -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-csharp/71d1c5919f9e1fa1cc798f8e8b397fb9c6de6e62/docs/favicon.ico -------------------------------------------------------------------------------- /docs/images/influxdata-logo--symbol--white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/InfluxCommunity/influxdb3-csharp/71d1c5919f9e1fa1cc798f8e8b397fb9c6de6e62/docs/images/influxdata-logo--symbol--white.png -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # influxdb3-csharp 2 | 3 | | Client | Description | API | 4 | |----------|------------------------------------------------------------------------------------------|---------------------------------------------------| 5 | | `Client` | The C# .NET client that provides an easy and convenient way to interact with InfluxDB 3. | [InfluxDB3.Client.html](api/InfluxDB3.Client.yml) | 6 | -------------------------------------------------------------------------------- /docs/publish-site.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # 4 | # Publish site to GitHub Pages 5 | # 6 | # How to run in Docker: 7 | # 8 | # docker run --rm -it \ 9 | # -v "${PWD}/docs":/code/docs \ 10 | # -v "${PWD}/.circleci":/code/.circleci \ 11 | # -v ~/.ssh:/root/.ssh \ 12 | # -v ~/.gitconfig:/root/.gitconfig \ 13 | # -w /code \ 14 | # bitnami/git:latest /code/docs/publish-site.sh 15 | # 16 | 17 | set -ev 18 | 19 | echo "# Clone client and switch to branch for GH-Pages" 20 | git clone -b gh-pages git@github.com:InfluxCommunity/influxdb3-csharp.git /code/influxdb3-csharp 21 | 22 | echo "# Remove old pages" 23 | rm -r /code/influxdb3-csharp/* 24 | 25 | echo "# Copy new docs" 26 | cp -Rf /code/docs/_site/* /code/influxdb3-csharp/ 27 | 28 | echo "# Copy CircleCI" 29 | cp -R /code/.circleci/ /code/influxdb3-csharp/ 30 | 31 | echo "# Deploy site" 32 | cd /code/influxdb3-csharp/ || exit 33 | git add -f . 34 | git -c commit.gpgsign=false commit -m "Pushed the latest Docs to GitHub pages [skip CI]" 35 | git push -fq origin gh-pages 36 | -------------------------------------------------------------------------------- /docs/toc.yml: -------------------------------------------------------------------------------- 1 | - name: InfluxDB3.Client API 2 | href: api/ 3 | homepage: api/InfluxDB3.Client.yml -------------------------------------------------------------------------------- /net_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | --------------------------------------------------------------------------------