| // Licensed to the Apache Software Foundation (ASF) under one or more |
| // contributor license agreements. See the NOTICE file distributed with |
| // this work for additional information regarding copyright ownership. |
| // The ASF licenses this file to You under the Apache License, Version 2.0 |
| // (the "License"); you may not use this file except in compliance with |
| // the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| = .NET Client |
| |
| Ignite 3 clients connect to the cluster via a standard socket connection. Unlike Ignite 2.x, there is no separate Thin and Thick clients in Ignite 3. All clients are 'thin'. |
| |
| Clients do not become a part of the cluster topology, never hold any data, and are not used as a destination for compute calculations. |
| |
| == Getting Started |
| |
| === Prerequisites |
| |
| To use C# thin client, .NET 8.0 or newer is required. |
| |
| === Installation |
| |
| C# client is available via NuGet. To add it, use the `add package` command: |
| |
| [source, bash, subs="attributes,specialchars"] |
| ---- |
| dotnet add package Ignite.Ignite --version 3.0.0 |
| ---- |
| |
| == Connecting to Cluster |
| |
| To initialize a client, use the `IgniteClient` class, and provide it with the configuration: |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| var clientCfg = new IgniteClientConfiguration |
| { |
| Endpoints = { "127.0.0.1" } |
| }; |
| using var client = await IgniteClient.StartAsync(clientCfg); |
| ---- |
| -- |
| |
| == Authentication |
| |
| To pass authentication information, pass it to `IgniteClient` builder: |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| var cfg = new IgniteClientConfiguration("127.0.0.1:10800") |
| { |
| Authenticator = new BasicAuthenticator |
| { |
| Username = "myUser", |
| Password = "myPassword" |
| } |
| }; |
| IIgniteClient client = await IgniteClient.StartAsync(cfg); |
| ---- |
| -- |
| |
| === Usage Examples |
| |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| public class Account |
| { |
| public long Id { get; set; } |
| public long Balance { get; set; } |
| |
| [NotMapped] |
| public Guid UnmappedId { get; set; } |
| } |
| ---- |
| -- |
| |
| |
| == SQL API |
| |
| Ignite 3 is focused on SQL, and SQL API is the primary way to work with the data. You can read more about supported SQL statements in the link:sql-reference/ddl[SQL Reference] section. Here is how you can send SQL requests: |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| IResultSet<IIgniteTuple> resultSet = await client.Sql.ExecuteAsync(transaction: null, "select name from tbl where id = ?", 42); |
| List<IIgniteTuple> rows = await resultSet.ToListAsync(); |
| IIgniteTuple row = rows.Single(); |
| Debug.Assert(row["name"] as string == "John Doe"); |
| ---- |
| -- |
| |
| |
| === SQL Scripts |
| |
| The default API executes SQL statements one at a time. If you want to execute large SQL statements, pass them to the `executeScript()` method. These statements will be executed in order. |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| string script = |
| "CREATE TABLE IF NOT EXISTS Person (id int primary key, city_id int, name varchar, age int, company varchar);" + |
| "INSERT INTO Person (1,3, 'John', 43, 'Sample')"; |
| |
| await Client.Sql.ExecuteScriptAsync(script); |
| ---- |
| -- |
| |
| NOTE: Execution of each statement is considered complete when the first page is ready to be returned. As a result, when working with large data sets, SELECT statement may be affected by later statements in the same script. |
| |
| == Transactions |
| |
| All table operations in Ignite 3 are transactional. You can provide an explicit transaction as a first argument of any Table and SQL API call. If you do not provide an explicit transaction, an implicit one will be created for every call. |
| |
| Here is how you can provide a transaction explicitly: |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| var accounts = table.GetKeyValueView<long, Account>(); |
| await accounts.PutAsync(transaction: null, 42, new Account(16_000)); |
| |
| await using ITransaction tx = await client.Transactions.BeginAsync(); |
| |
| (Account account, bool hasValue) = await accounts.GetAsync(tx, 42); |
| account = account with { Balance = account.Balance + 500 }; |
| |
| await accounts.PutAsync(tx, 42, account); |
| |
| Debug.Assert((await accounts.GetAsync(tx, 42)).Value.Balance == 16_500); |
| |
| await tx.RollbackAsync(); |
| |
| Debug.Assert((await accounts.GetAsync(null, 42)).Value.Balance == 16_000); |
| |
| public record Account(decimal Balance); |
| ---- |
| -- |
| |
| == Table API |
| |
| To execute table operations on a specific table, you need to get a specific view of the table and use one of its methods. You can only create new tables by using SQL API. |
| |
| When working with tables, you can use built-in Tuple type, which is a set of key-value pairs underneath, or map the data to your own types for a strongly-typed access. Here is how you can work with tables: |
| |
| === Getting a Table Instance |
| |
| First, get an instance of the table. To obtain an instance of table, use the `IgniteTables.table(String)` method. You can also use `IgniteTables.tables()` method to list all existing tables. |
| |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| var existingTables = await Client.Tables.GetTablesAsync(); |
| var firstTable = existingTables[0]; |
| |
| var myTable = await Client.Tables.GetTableAsync("MY_TABLE"); |
| ---- |
| -- |
| |
| === Basic Table Operations |
| |
| Once you've got a table you need to get a specific view to choose how you want to operate table records. |
| |
| ==== Binary Record View |
| |
| A binary record view. It can be used to operate table tuples directly. |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| IRecordView<IIgniteTuple> view = table.RecordBinaryView; |
| |
| IIgniteTuple fullRecord = new IgniteTuple |
| { |
| ["id"] = 42, |
| ["name"] = "John Doe" |
| }; |
| |
| await view.UpsertAsync(transaction: null, fullRecord); |
| |
| IIgniteTuple keyRecord = new IgniteTuple { ["id"] = 42 }; |
| (IIgniteTuple value, bool hasValue) = await view.GetAsync(transaction: null, keyRecord); |
| |
| Debug.Assert(hasValue); |
| Debug.Assert(value.FieldCount == 2); |
| Debug.Assert(value["id"] as int? == 42); |
| Debug.Assert(value["name"] as string == "John Doe"); |
| ---- |
| -- |
| |
| ==== Record View |
| |
| A record view mapped to a user type. It can be used to operate table using user objects which are mapped to table tuples. |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| var pocoView = table.GetRecordView<Poco>(); |
| |
| await pocoView.UpsertAsync(transaction: null, new Poco(42, "John Doe")); |
| var (value, hasValue) = await pocoView.GetAsync(transaction: null, new Poco(42)); |
| |
| Debug.Assert(hasValue); |
| Debug.Assert(value.Name == "John Doe"); |
| |
| public record Poco(long Id, string? Name = null); |
| ---- |
| -- |
| |
| ==== Key-Value Binary View |
| |
| A binary key-value view. It can be used to operate table using key and value tuples separately. |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| IKeyValueView<IIgniteTuple, IIgniteTuple> kvView = table.KeyValueBinaryView; |
| |
| IIgniteTuple key = new IgniteTuple { ["id"] = 42 }; |
| IIgniteTuple val = new IgniteTuple { ["name"] = "John Doe" }; |
| |
| await kvView.PutAsync(transaction: null, key, val); |
| (IIgniteTuple? value, bool hasValue) = await kvView.GetAsync(transaction: null, key); |
| |
| Debug.Assert(hasValue); |
| Debug.Assert(value.FieldCount == 1); |
| Debug.Assert(value["name"] as string == "John Doe"); |
| ---- |
| -- |
| |
| |
| ==== Key-Value View |
| |
| A key-value view with user objects. It can be used to operate table using key and value user objects mapped to table tuples. |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| IKeyValueView<long, Poco> kvView = table.GetKeyValueView<long, Poco>(); |
| |
| await kvView.PutAsync(transaction: null, 42, new Poco(Id: 0, Name: "John Doe")); |
| (Poco? value, bool hasValue) = await kvView.GetAsync(transaction: null, 42); |
| |
| Debug.Assert(hasValue); |
| Debug.Assert(value.Name == "John Doe"); |
| |
| public record Poco(long Id, string? Name = null); |
| ---- |
| -- |
| |
| == Streaming Data |
| |
| To stream a large amount of data, use the data streamer. Data streaming provides a quicker and more efficient way to load, organize and optimally distribute your data. Data streamer accepts a stream of data and distributes data entries across the cluster, where the processing takes place. Data streaming is available in all table views. |
| |
| image::images/data_streaming.png[] |
| |
| Data streaming provides at-least-once delivery guarantee. |
| |
| === Using Data Streamer API |
| |
| [tabs] |
| -- |
| tab:.NET[] |
| [source, csharp] |
| ---- |
| public async Task TestBasicStreamingRecordBinaryView() |
| { |
| var options = DataStreamerOptions.Default with { BatchSize = 10 }; |
| var data = Enumerable.Range(0, Count).Select(x => new IgniteTuple { ["id"] = 1L, ["name"] = "foo" }).ToList(); |
| |
| await TupleView.StreamDataAsync(data.ToAsyncEnumerable(), options); |
| } |
| ---- |
| -- |
| |
| |
| == Client Metrics |
| |
| Metrics are exposed by the .NET client through the `System.Diagnostics.Metrics` API with the `Apache.Ignite` meter name. For example, here is how you can access Ignite metrics by using the link:https://learn.microsoft.com/en-us/dotnet/core/diagnostics/dotnet-counters[dotnet-counters] tool: |
| |
| [source, bash] |
| ---- |
| dotnet-counters monitor --counters Apache.Ignite,System.Runtime --process-id PID |
| ---- |
| |
| You can also get metrics in your code by creating a listener: |
| |
| [source, csharp] |
| ---- |
| var listener = new MeterListener(); |
| listener.InstrumentPublished = (instrument, meterListener) => |
| { |
| if (instrument.Meter.Name == "Apache.Ignite") |
| { |
| meterListener.EnableMeasurementEvents(instrument); |
| } |
| }; |
| listener.SetMeasurementEventCallback<int>( |
| (instrument, measurement, tags, state) => Console.WriteLine($"{instrument.Name}: {measurement}")); |
| |
| listener.Start(); |
| ---- |
| |
| |
| === Available .NET Metrics |
| |
| [width="100%",cols="20%,80%",opts="header"] |
| |======================================================================= |
| |Metric name | Description |
| |
| |connections-active|The number of currently active connections. |
| |connections-established|The number of established connections. |
| |connections-lost|The number of connections lost. |
| |connections-lost-timeout|The number of connections lost due to a timeout. |
| |handshakes-failed|The number of failed handshakes. |
| |handshakes-failed-timeout|The number of handshakes that failed due to a timeout. |
| |requests-active|The number of currently active requests. |
| |requests-sent|The number of requests sent. |
| |requests-completed|The number of completed requests. Requests are completed once a response is received. |
| |requests-retried|The number of request retries. |
| |requests-failed|The number of failed requests. |
| |bytes-sent|The amount of bytes sent. |
| |bytes-received|The amount of bytes received. |
| |streamer-batches-sent|The number of data streamer batches sent. |
| |streamer-items-sent|The number of data streamer items sent. |
| |streamer-batches-active|The number of existing data streamer batches. |
| |streamer-items-queued|The number of queued data streamer items. |
| |
| |======================================================================= |