# Fauna v10 .NET/C# client driver (current) | Version: 1.0.1 | Repository: fauna/fauna-dotnet | | --- | --- | --- | --- | Fauna’s .NET/C# client driver lets you run FQL queries from .NET and C# applications. This guide shows how to set up the driver and use it to run FQL queries. ## [](#supported-net-and-c-versions)Supported .NET and C# versions * .NET 8.0 * C# ^10.0 ## [](#installation)Installation The driver is available on [NuGet](https://www.nuget.org/packages/fauna). To install it using the .NET CLI, run: ```csharp dotnet add package Fauna ``` ## [](#api-reference)API reference API reference documentation for the driver is available at [https://fauna.github.io/fauna-dotnet/](https://fauna.github.io/fauna-dotnet/). ## [](#sample-app)Sample app For a practical example, check out the [.NET sample app](https://github.com/fauna/dotnet-sample-app). This sample app is an e-commerce application that uses the Fauna .NET/C# driver. The source code includes comments highlighting best practices for using the driver and composing FQL queries. ## [](#basic-usage)Basic usage The following applications: * Initialize a client instance to connect to Fauna * Compose a basic FQL query using an `FQL` string template * Run the query using `QueryAsync()` or `PaginateAsync()` * Deserialize the results based on a provided type parameter Use `QueryAsync()` to run a non-paginated query: ```csharp using Fauna; using Fauna.Exceptions; using static Fauna.Query; try { // Initialize the client to connect to Fauna var config = new Configuration("FAUNA_SECRET") var client = new Client(config); // Compose a query var query = FQL($@" Product.byName('cups').first() {{ name, description, price }} "); // Run the query // Optionally specify the expected result type as a type parameter. // If not provided, the value will be deserialized as object. var response = await client.QueryAsync>(query); Console.WriteLine(response.Data["name"]); Console.WriteLine(response.Data["description"]); Console.WriteLine(response.Data["price"]); Console.WriteLine("--------"); } catch (FaunaException e) { Console.WriteLine(e); } ``` Queries that return a [Set](../../../reference/fql/types/#set) are automatically paginated. Use `PaginateAsync()` to iterate through paginated results: ```csharp using Fauna; using Fauna.Exceptions; using static Fauna.Query; try { // Initialize the client to connect to Fauna var client = new Client("FAUNA_SECRET"); // Compose a query var query = FQL($@" Product.sortedByPriceLowToHigh() {{ name, description, price }} "); // Run the query // PaginateAsync returns an IAsyncEnumerable of pages var response = client.PaginateAsync>(query); await foreach (var page in response) { foreach (var product in page.Data) { Console.WriteLine(product["name"]); Console.WriteLine(product["description"]); Console.WriteLine(product["price"]); Console.WriteLine("--------"); } } } catch (FaunaException e) { Console.WriteLine(e); } ``` ## [](#connect-to-fauna)Connect to Fauna Each Fauna query is an independently authenticated request to the [Query HTTP API endpoint](../../../reference/http/reference/core-api/#operation/query). You authenticate with Fauna with an [authentication secret](../../../learn/security/authentication/#secrets). ### [](#get-an-authentication-secret)Get an authentication secret Fauna supports several [secret types](../../../learn/security/authentication/#secret-types). For testing, you can create a [key](../../../learn/security/keys/), which is a type of secret: 1. Log in to the [Fauna Dashboard](https://dashboard.fauna.com/). 2. On the **Explorer** page, create a database. 3. In the database’s **Keys** tab, click **Create Key**. 4. Choose a **Role** of **server**. 5. Click **Save**. 6. Copy the **Key Secret**. The secret is scoped to the database. ### [](#initialize-a-client)Initialize a client To send query requests to a Fauna database, initialize a `Client` instance using an authentication secret scoped to the database: ```csharp var client = new Client("FAUNA_SECRET"); ``` `Client` requires a `secret` or `configuration` argument. For configuration options, see [Client configuration](#config). ### [](#connect-to-a-child-database)Connect to a child database A [scoped key](../../../learn/security/keys/#scoped-keys) lets you use a parent database’s admin key to send query requests to its child databases. For example, if you have an admin key for a parent database and want to connect to a child database named `childDB`, you can create a scoped key using the following format: ``` // Scoped key that impersonates an `admin` key for // the `childDB` child database. fn...:childDB:admin ``` You can then initialize a `Client` instance using the scoped key: ```csharp var client = new Client("fn...:childDB:admin"); ``` ### [](#multiple-connections)Multiple connections You can use a single client instance to run multiple asynchronous queries at once. The driver manages HTTP connections as needed. Your app doesn’t need to implement connection pools or other connection management strategies. You can create multiple client instances to connect to Fauna using different credentials or client configurations. ## [](#run-fql-queries)Run FQL queries Use `FQL` string templates to compose FQL queries. Run the queries using `QueryAsync()` or `PaginateAsync()`: ```csharp // Unpaginated query var query = FQL($@"Product.byName('cups').first()"); client.QueryAsync(query); // Paginated query // Adjust `pageSize()` size as needed var paginatedQuery = FQL($@"Category.all().pageSize(2)"); client.PaginateAsync(paginatedQuery); ``` You can only compose FQL queries using string templates. ### [](#var)Variable interpolation Use single braces `{}` to pass native variables to fql queries. Use `{{}}` to escape other single braces in the query. ```csharp // Create a native var var collectionName = "Product"; // Pass the var to an FQL query var query = FQL($@" let collection = Collection({collectionName}) collection.byName('cups').first() {{ price }}" ); client.QueryAsync(query); ``` The driver encodes interpolated variables to an appropriate [FQL type](../../../reference/fql/types/) and uses the [wire protocol](../../../reference/http/reference/wire-protocol/) to pass the query to the Core HTTP API’s [Query endpoint](../../../reference/http/reference/core-api/#operation/query). This helps prevent injection attacks. ### [](#query-composition)Query composition You can use variable interpolation to pass FQL string templates as query fragments to compose an FQL query: ```csharp // Create a reusable query fragment. var product = FQL($@"Product.byName(""pizza"").first()"); // Use the fragment in another FQL query. var query = FQL($@" let product = {product} product {{ name, price }} "); client.QueryAsync(query); ``` ### [](#poco-mapping)POCO mapping With `Fauna.Mapping`, you can map a POCO class to a Fauna document or object shape: ```csharp using Fauna.Mapping; class Category { // Property names are automatically converted to camelCase. [Id] public string? Id { get; set; } // Manually specify a name by providing a string. [Field("name")] public string? CatName { get; set; } } class Product { [Id] public string? Id { get; set; } public string? Name { get; set; } public string? Description { get; set; } public int Price { get; set; } // Reference to document public Ref Category { get; set; } } ``` * `[Id]`: Should only be used once per class on a field named `Id` that represents the Fauna document ID. It’s not encoded unless the `isClientGenerated` flag is true. * `[Ts]`: Should only be used once per class on a field named `Ts` that represents the timestamp of a document. It’s not encoded. * `[Collection]`: Typically goes unmodeled. Should only be used once per class on a field named `Coll` that represents the collection field of a document. It will never be encoded. * `[Field]`: Can be associated with any field to override its name in Fauna. * `[Ignore]`: Can be used to ignore fields during encoding and decoding. You can use POCO classes to deserialize query responses: ```csharp var query = FQL($@"Product.sortedByPriceLowToHigh()"); var products = client.PaginateAsync(query).FlattenAsync(); await foreach (var p in products) { Console.WriteLine($"{p.Name} {p.Description} {p.Price}"); } ``` You can also use POCO classes to write to your database: ```csharp var product = new Product { Id = "12345", Name = "limes", Description = "Organic, 2 ct", Price = 95 }; client.QueryAsync(FQL($@"Product.create({product})")); ``` ### [](#datacontext)`DataContext` The `DataContext` class provides a schema-aware view of your database. Subclass it and configure your collections: ```csharp class CustomerDb : DataContext { public class CustomerCollection : Collection { public Index ByEmail(string email) => Index().Call(email); public Index ByName(string name) => Index().Call(name); } public CustomerCollection Customer { get => GetCollection(); } } ``` `DataContext` provides `Client` querying, which automatically maps your collections to POCO equivalents, even when type hints are not provided. ```csharp var db = client.DataContext var result = db.QueryAsync(FQL($"Customer.all().first()")); var customer = (Customer)result.Data!; Console.WriteLine(customer.name); ``` ### [](#document-references)Document references The driver supports [document references](../../../learn/data-model/relationships/) using the `Ref` type. There are several ways to work with document references using the driver: 1. Fetch the reference without loading the referenced document: ```csharp // Gets a Product document. // The document's `category` field contains a // reference to a Category document. The // `category` field is not projected. var query = FQL($@" Product.byName('limes').first() "); var response = await client.QueryAsync(query); var product = response.Data; ``` 2. [Project](../../../reference/fql/projection/) the document reference to load the referenced document: ```csharp // Gets a Product document. // The `category` field is projected to load the // referenced document. var query = FQL($@" Product.byName('limes').first() { name, category { name } } "); var response = await client.QueryAsync>(query); var product = response.Data; Console.WriteLine(product["name"]); // Prints the category name. var category = (Dictionary)product["category"]; Console.WriteLine(category["name"]); ``` 3. Use `LoadRefAsync()` to load the referenced document: ```csharp // Gets a Product document. var query = FQL($@"Product.byName('limes').first()"); var response = await client.QueryAsync(query); var product = response.Data; // Loads the Category document referenced in // the Product document. var category = await client.LoadRefAsync(product.Category); // Prints the category name. Console.WriteLine(category.Name); ``` If the reference is already loaded, it returns the cached value without making another query to Fauna: ```csharp // This won't run another query if the referenced // document is already loaded. var sameCategory = await client.LoadRefAsync(product.Category); ``` #### [](#null-documents)Null documents A [null document](../../../reference/fql/types/#nulldoc) can be handled two ways: 1. Let the driver throw an exception and do something with it: ```csharp try { await client.QueryAsync(FQL($"SomeColl.byId('123')")) } catch (NullDocumentException e) { Console.WriteLine(e.Id); // "123" Console.WriteLine(e.Collection.Name); // "SomeColl" Console.WriteLine(e.Cause); // "not found" } ``` 2. Wrap your expected type in a `Ref<>` or `NamedRef`. You can wrap `Dictionary` and POCOs. ```csharp var q = FQL($"Collection.byName('Fake')"); var r = (await client.QueryAsync>>(q)).Data; if (r.Data.Exists) { Console.WriteLine(d.Id); // "Fake" Console.WriteLine(d.Collection.Name); // "Collection" var doc = r.Get(); // A dictionary with id, coll, ts, and any user-defined fields. } else { Console.WriteLine(d.Name); // "Fake" Console.WriteLine(d.Collection.Name); // "Collection" Console.WriteLine(d.Cause); // "not found" r.Get() // this throws a NullDocumentException } ``` ### [](#pagination)Pagination When you wish to paginate a [set](../../../reference/fql/types/#set), such as a collection or index, use `PaginateAsync()`. Example of a query that returns a Set: ```csharp var query = FQL($"Customer.all()"); await foreach (var page in client.PaginateAsync(query)) { // handle each page } await foreach (var item in client.PaginateAsync(query).FlattenAsync()) { // handle each item } ``` Example of a query that returns an object with an embedded Set: ```csharp class MyResult { [Field("customers")] public Page? Customers { get; set; } } var query = FQL($"{{customers: Customer.all()}}"); var result = await client.QueryAsync(query); await foreach (var page in client.PaginateAsync(result.Data.Customers!)) { // handle each page } await foreach (var item in client.PaginateAsync(result.Data.Customers!).FlattenAsync()) { // handle each item } ``` ### [](#query-stats)Query stats Successful query responses and `ServiceException` exceptions include [query stats](../../../reference/http/reference/query-stats/): ```csharp try { var client = new Client("FAUNA_SECRET"); var query = FQL($@"'Hello world'"); var response = await client.QueryAsync(query); Console.WriteLine(response.Stats.ToString()); } catch (FaunaException e) { if (e is ServiceException serviceException) { Console.WriteLine(serviceException.Stats.ToString()); Console.WriteLine(e); } else { Console.WriteLine(e); } } ``` ## [](#config)Client configuration The `Client` instance comes with reasonable configuration defaults. We recommend using the defaults in most cases. If needed, you can configure the client and override the defaults. This also lets you set default [Query options](#query-opts). ```csharp var config = new Configuration("FAUNA_SECRET") { // Configure the client Endpoint = new Uri("https://db.fauna.com"), RetryConfiguration = new RetryConfiguration(3, TimeSpan.FromSeconds(20)), // Set default query options DefaultQueryOptions = new QueryOptions { Linearized = false, QueryTags = new Dictionary { { "tag", "value" } }, QueryTimeout = TimeSpan.FromSeconds(60), TraceParent = "00-750efa5fb6a131eb2cf4db39f28366cb-000000000000000b-00", TypeCheck = false } }; var client = new Client(config); ``` For supported properties, see [Fauna.Configuration](https://fauna.github.io/fauna-dotnet/latest/class_fauna_1_1_configuration.html) in the API reference. ### [](#environment-variables)Environment variables By default, the client configuration’s `Secret` and `Endpoint` default to the respective `FAUNA_SECRET` and `FAUNA_ENDPOINT` environment variables. For example, if you set the following environment variables: ```bash export FAUNA_SECRET=FAUNA_SECRET export FAUNA_ENDPOINT=https://db.fauna.com/ ``` You can initialize the client with a default configuration: ```csharp var client = new Client(); ``` ### [](#retries)Retries By default, the client automatically retries query requests that return a `limit_exceeded` [error code](../../../reference/http/reference/errors/). Retries use an exponential backoff. The client retries a query up to three times by default. The maximum wait time between retries defaults to 20 seconds. To override these defaults, pass a `RetryConfiguration` instance to the [Client configuration](#config). ```csharp var config = new Configuration("FAUNA_SECRET") { RetryConfiguration = new RetryConfiguration(3, TimeSpan.FromSeconds(20)) }; var client = new Client(config); ``` For supported parameters, see [Fauna.Core.RetryConfiguration](https://fauna.github.io/fauna-dotnet/latest/class_fauna_1_1_core_1_1_retry_configuration.html) in the API reference. ## [](#query-opts)Query options The [Client configuration](#config) sets default query options for the following methods: * `QueryAsync()` * `PaginateAsync()` You can pass a `QueryOptions` argument to override these defaults: ```csharp var queryOptions = new QueryOptions { Linearized = false, QueryTags = new Dictionary { { "tag", "value" } }, QueryTimeout = TimeSpan.FromSeconds(60), TraceParent = "00-750efa5fb6a131eb2cf4db39f28366cb-000000000000000b-00", TypeCheck = true }; var query = FQL($@"'Hello world'"); client.QueryAsync(query, queryOptions); ``` For supported properties, see [Fauna.Core.QueryOptions](https://fauna.github.io/fauna-dotnet/latest/class_fauna_1_1_core_1_1_query_options.html) in the API reference. ## [](#event-feeds)Event feeds The driver supports [event feeds](../../../learn/cdc/#event-feeds). An event feed asynchronously polls an [event source](../../../learn/cdc/#create-an-event-source) for paginated events. To use event feeds, you must have a Pro or Enterprise plan. ### [](#request-an-event-feed)Request an event feed To get an event source, append [`set.eventSource()`](../../../reference/fql-api/set/eventsource/) or [`set.eventsOn()`](../../../reference/fql-api/set/eventson/) to a [supported Set](../../../learn/cdc/#sets). To get paginated events, pass the event source to `EventFeedAsync()`: ```csharp // Get an event source from a supported Set EventSource eventSource = await client.QueryAsync(FQL($"Product.all().eventSource()")); var feed = await client.EventFeedAsync(eventSource); ``` If changes occur between the creation of the event source and the event feed request, the feed replays and emits any related events. You can also pass a query that produces an event source directly to `EventFeedAsync()`: ```csharp var feed = await client.EventFeedAsync(FQL($"Product.all().eventSource()")); ``` If you pass an event source query to `EventFeedAsync()`, the driver creates the event source and requests the event feed at the same time. In most cases, you’ll get events after a specific [event cursor](#cursor) or [start time](#start-time). #### [](#start-time)Get events after a specific start time When you first poll an event source using an event feed, you usually include a `startTs` (start timestamp) in the [`FeedOptions` object](#event-feed-opts) that’s passed to `EventFeedAsync()`. The request returns events that occurred after the specified timestamp (exclusive). `startTs` is an integer representing a time in microseconds since the Unix epoch: ```csharp // Calculate timestamp for 10 minutes ago in microseconds long tenMinutesAgo = DateTimeOffset.UtcNow.AddMinutes(-10).ToUnixTimeMilliseconds() * 1000; var feedOptions = new FeedOptions(startTs: tenMinutesAgo); var feed = await client.EventFeedAsync(FQL($"Product.all().eventSource()", feedOptions)); ``` `startTs` must be later than the creation time of the event source. The period between the request and the `startTs` can’t exceed the `history_days` setting for the source Set’s collection. If `history_days` is `0` or unset, the period is limited to 15 minutes. #### [](#cursor)Get events after a specific event cursor After the initial request, you usually get subsequent events using the [cursor](../../../learn/cdc/#cursor) for the last page or event. To get events after a cursor (exclusive), include the `cursor` in the [`FeedOptions` object](#event-feed-opts) that’s passed to `EventFeedAsync()`: ```csharp var feedOptions = new FeedOptions(cursor: "gsGabc456"); // Cursor for a previous page var feed = await client.EventFeedAsync(FQL($"Product.all().eventSource()", feedOptions)); ``` You can reuse cursors across event sources with identical queries in the same database. ### [](#loop)Iterate on an event feed `EventFeedAsync()` returns a `FeedEnumerable` instance that acts as an `AsyncEnumerator`. Use `foreach()` to iterate through the pages of events: ```csharp await foreach (var page in feed) { foreach (var evt in page.Events) { Console.WriteLine($"Event Type: {evt.Type}"); Product product = evt.Data; Console.WriteLine($"Product Name: {product.Name}"); } } ``` The `FeedEnumerable` will stop when there are no more events to poll. Each page includes a top-level `cursor`. You can include the cursor in a [`FeedOptions` object](#event-feed-opts) passed to `EventFeedAsync()` to poll for events after the cursor. ### [](#error-handling)Error handling Exceptions can be raised at two different places: * While fetching a page * While iterating a page’s events This distinction allows for you to ignore errors originating from event processing. For example: ```csharp try { await foreach (var page in feed) { try { foreach (var evt in page.Events) { Console.WriteLine($"Event Type: {evt.Type}"); Product product = evt.Data; Console.WriteLine($"Product Name: {product.Name}"); } } // `EventException` is thrown for event processing errors. catch (EventException eventError) { Console.WriteLine($"Feed event error: {eventError}"); } } } catch (Exception error) { Console.WriteLine($"Non-retryable error: {error}"); } ``` Each page’s `cursor` contains the cursor for the page’s last successfully processed event. If you’re using a [loop to poll for changes](#loop), using the cursor will skip any events that caused errors. ### [](#event-feed-opts)Event feed options The client configuration sets the default options for `EventFeedAsync()`. You can pass a [`FeedOptions`](https://fauna.github.io/fauna-dotnet/latest/class_fauna_1_1_core_1_1_feed_options.html) object to override these defaults: ```csharp var feedOptions = new FeedOptions( startTs: 1710968002310000, pageSize: 10, cursor: "gsGabc456" ); var feed = await client.EventFeedAsync(FQL($"Product.all().eventSource()"), feedOptions); ``` For supported properties, see [`FeedOptions`](https://fauna.github.io/fauna-dotnet/latest/class_fauna_1_1_core_1_1_feed_options.html) in the API reference. ## [](#event-streaming)Event streams The driver supports [event streams](../../../learn/cdc/). ### [](#start-a-stream)Start a stream To get an event source, append [`set.eventSource()`](../../../reference/fql-api/set/eventsource/) or [`set.eventsOn()`](../../../reference/fql-api/set/eventson/) to a [supported Set](../../../learn/cdc/#sets). To stream the source’s events, pass the event source to `SubscribeStream()`: ```csharp var query = fql($@" let set = Customer.all() {{ initialPage: set.pageSize(10), eventSource: set.eventSource() }} "); var response = await client.QueryAsync(query); var eventSource = response["eventSource"].ToString(); await using var stream = client.SubscribeStream(eventSource); await foreach (var evt in stream) { Console.WriteLine($"Received Event Type: {evt.Type}"); if (evt.Data != null) // Status events won't have Data { Customer customer = evt.Data; Console.WriteLine($"Name: {customer.Name} - Email: {customer.Email}"); } } ``` You can also pass a query that produces an event source directly to `EventStreamAsync()`: ```csharp var stream = await client.EventStreamAsync(FQL($"Customer.all().eventSource()")); await foreach (var evt in stream) { Console.WriteLine($"Received Event Type: {evt.Type}"); if (evt.Data != null) { Customer customer = evt.Data; Console.WriteLine($"Name: {customer.Name} - Email: {customer.Email}"); } } ``` ### [](#stream-options)Stream options The [Client configuration](#config) sets default options for the `SubscribeStream()` and `EventStreamAsync()` methods. You can pass a [`StreamOptions`](https://fauna.github.io/fauna-dotnet/latest/class_fauna_1_1_stream_options.html) object to override these defaults: ```csharp var options = new StreamOptions( token: "", cursor: "gsGghu789" ); var stream = await client.EventStreamAsync( query: FQL("Product.all().eventSource()"), streamOptions: options ); await foreach (var evt in stream) { Console.WriteLine($"Received Event Type: {evt.Type}"); if (evt.Data != null) { Customer customer = evt.Data; Console.WriteLine($"Name: {customer.Name} - Email: {customer.Email}"); } } ``` ## [](#debug-logging)Debug logging To enable debug logging, set the `FAUNA_DEBUG` environment variable to an integer for the `Microsoft.Extensions.Logging.LogLevel`. For example: * `0`: `LogLevel.Trace` and higher (all messages) * `3`: `LogLevel.Warning` and higher The driver logs HTTP request and response details, including headers. For security, the `Authorization` header is redacted in debug logs but is visible in trace logs. For advanced logging, you can use a custom `ILogger` implementation, such as Serilog or NLog. Pass the implementation to the `Configuration` class when instantiating a `Client`. ### [](#basic-example-serilog)Basic example: Serilog Install the packages: ```bash dotnet add package Serilog dotnet add package Serilog.Extensions.Logging dotnet add package Serilog.Sinks.Console dotnet add package Serilog.Sinks.File ``` Configure and use the logger: ```csharp using Fauna; using Microsoft.Extensions.Logging; using Serilog; using static Fauna.Query; Log.Logger = new LoggerConfiguration() .MinimumLevel.Verbose() .WriteTo.Console() .WriteTo.File("log.txt", rollingInterval: RollingInterval.Day, rollOnFileSizeLimit: true) .CreateLogger(); var logFactory = new LoggerFactory().AddSerilog(Log.Logger); var config = new Configuration("mysecret", logger: logFactory.CreateLogger("myapp")); var client = new Client(config); await client.QueryAsync(FQL($"1+1")); // You should see LogLevel.Debug messages in both the Console and the "log{date}.txt" file ``` # .NET/C# driver source code # Files ## File: Fauna/Core/Connection.cs ```csharp using System.Collections.Concurrent; using System.Net.Http.Headers; using System.Runtime.CompilerServices; using System.Text.Json; using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Types; using Fauna.Util; using Microsoft.Extensions.Logging; using Polly; using Stream = System.IO.Stream; namespace Fauna.Core; /// /// A class that handles HTTP requests and retries. /// internal class Connection : IConnection { private readonly Configuration _cfg; private bool _disposed; public TimeSpan BufferedRequestTimeout { get; init; } /// /// Initializes a new instance of the Connection class. /// /// The to use. public Connection(Configuration configuration) { _cfg = configuration; BufferedRequestTimeout = _cfg.DefaultQueryOptions.QueryTimeout.Add(_cfg.ClientBufferTimeout); } public async Task DoPostAsync( string path, Stream body, Dictionary headers, TimeSpan requestTimeout, CancellationToken cancel = default) { HttpResponseMessage response; using var timeboundCts = new CancellationTokenSource(requestTimeout); using var combinedCts = CancellationTokenSource.CreateLinkedTokenSource(timeboundCts.Token, cancel); var policyResult = await _cfg.RetryConfiguration.RetryPolicy .ExecuteAndCaptureAsync(() => _cfg.HttpClient.SendAsync(CreateHttpRequest(path, body, headers), combinedCts.Token)) .ConfigureAwait(false); response = policyResult.Outcome == OutcomeType.Successful ? policyResult.Result : policyResult.FinalHandledResult ?? throw policyResult.FinalException; Logger.Instance.LogDebug( "Fauna HTTP Response {status} from {uri}, headers: {headers}", response.StatusCode.ToString(), response.RequestMessage?.RequestUri?.ToString() ?? "UNKNOWN", JsonSerializer.Serialize( response.Headers.ToDictionary(kv => kv.Key, kv => kv.Value.ToList())) ); Logger.Instance.LogTrace("Response body: {body}", await response.Content.ReadAsStringAsync(cancel)); return response; } public async IAsyncEnumerable> OpenStream( string path, Types.EventSource eventSource, Dictionary headers, MappingContext ctx, [EnumeratorCancellation] CancellationToken cancellationToken = default) where T : notnull { cancellationToken.ThrowIfCancellationRequested(); while (!cancellationToken.IsCancellationRequested) { using var bc = new BlockingCollection>(new ConcurrentQueue>()); Task> streamTask = _cfg.RetryConfiguration.RetryPolicy.ExecuteAndCaptureAsync(async () => { var streamData = new MemoryStream(); eventSource.Serialize(streamData); var response = await _cfg.HttpClient .SendAsync( CreateHttpRequest(path, streamData, headers), HttpCompletionOption.ResponseHeadersRead, cancellationToken) .ConfigureAwait(false); if (!response.IsSuccessStatusCode) { bc.CompleteAdding(); return response; } await using var streamAsync = await response.Content.ReadAsStreamAsync(cancellationToken); using var streamReader = new StreamReader(streamAsync); while (!streamReader.EndOfStream && !cancellationToken.IsCancellationRequested) { string? line = await streamReader.ReadLineAsync().WaitAsync(cancellationToken); if (string.IsNullOrWhiteSpace(line)) { continue; } var evt = Event.From(line, ctx); eventSource.Options.Cursor = evt.Cursor; bc.Add(evt, cancellationToken); } return response; }); foreach (var evt in bc.GetConsumingEnumerable(cancellationToken)) { yield return evt; } await streamTask; bc.CompleteAdding(); if (streamTask.Result.Result.IsSuccessStatusCode) { continue; } var httpResponse = streamTask.Result.Result; string body = await httpResponse.Content.ReadAsStringAsync(cancellationToken); throw ExceptionHandler.FromRawResponse(body, httpResponse); } } private HttpRequestMessage CreateHttpRequest(string path, Stream body, Dictionary headers) { body.Position = 0; var request = new HttpRequestMessage { Content = new StreamContent(body), Method = HttpMethod.Post, RequestUri = new Uri(_cfg.Endpoint, path) }; request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); request.Headers.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip")); foreach (var header in headers) { request.Headers.Add(header.Key, header.Value); } Logger.Instance.LogDebug( "Fauna HTTP {method} Request to {uri} (timeout {timeout}ms), headers: {headers}", HttpMethod.Post.ToString(), request.RequestUri.ToString(), _cfg.HttpClient.Timeout.TotalMilliseconds, JsonSerializer.Serialize( request.Headers .Select(header => { // Redact Auth header in debug logs if (header.Key.StartsWith("Authorization", StringComparison.InvariantCultureIgnoreCase)) { return KeyValuePair.Create(header.Key, new[] { "hidden" }.AsEnumerable()); } return header; }) .ToDictionary(kv => kv.Key, kv => kv.Value.ToList())) ); // Emit unredacted Auth header and response body in trace logs Logger.Instance.LogTrace("Unredacted Authorization header: {value}", request.Headers.Authorization?.ToString() ?? "null"); Logger.Instance.LogTrace("Request body: {body}", request.Content.ReadAsStringAsync().Result); return request; } private void Dispose(bool disposing) { if (_disposed) return; if (disposing && _cfg.DisposeHttpClient) { _cfg.HttpClient.Dispose(); GC.SuppressFinalize(this); } _disposed = true; } /// /// Disposes the resources used by the class. /// public void Dispose() { Dispose(true); } // A finalizer: https://stackoverflow.com/questions/151051/when-should-i-use-gc-suppressfinalize ~Connection() { Dispose(false); } } ``` ## File: Fauna/Core/Endpoints.cs ```csharp namespace Fauna.Core; /// /// Represents the endpoints used for accessing Fauna. /// public static class Endpoints { /// /// The default URI for Fauna, used for production. /// public static Uri Default { get; } = new("https://db.fauna.com"); /// /// Gets the configured endpoint URI, falling back to the default if not set. /// /// The URI for the Fauna endpoint. public static Uri GetFaunaEndpoint() { string? endpoint = Environment.GetEnvironmentVariable("FAUNA_ENDPOINT"); if (string.IsNullOrWhiteSpace(endpoint)) { return Default; } if (Uri.IsWellFormedUriString(endpoint, UriKind.Absolute)) { return new Uri(endpoint); } throw new UriFormatException("Invalid FAUNA_ENDPOINT environment variable. Must be a valid URI."); } } ``` ## File: Fauna/Core/ErrorInfo.cs ```csharp using System.Text.Json.Serialization; using Fauna.Exceptions; using static Fauna.Core.ResponseFields; namespace Fauna.Core; /// /// Contains detailed information about an error in a query response. /// public readonly struct ErrorInfo { /// /// The error code when a query fails. /// [JsonPropertyName(Error_CodeFieldName)] public string? Code { get; init; } /// /// The detailed message describing the cause of the error. /// [JsonPropertyName(Error_MessageFieldName)] public string? Message { get; init; } /// /// The constraint failures that occurred during the query. /// [JsonPropertyName(Error_ConstraintFailuresFieldName)] public ConstraintFailure[] ConstraintFailures { get; init; } /// /// The information about an abort operation within a transaction. /// [JsonPropertyName(Error_AbortFieldName)] public object? Abort { get; init; } } ``` ## File: Fauna/Core/FeedEnumerable.cs ```csharp using Fauna.Types; namespace Fauna.Core; /// /// Represents a Fauna event feed. /// /// Type to map each of the Events to. public class FeedEnumerable where T : notnull { private readonly BaseClient _client; private readonly EventSource _eventSource; private readonly CancellationToken _cancel; /// /// Current cursor for the Feed. /// public string? Cursor => CurrentPage?.Cursor; /// /// The latest page returned from the event feed enumerator. /// public FeedPage? CurrentPage { get; private set; } internal FeedEnumerable( BaseClient client, EventSource eventSource, CancellationToken cancel = default) { _client = client; _eventSource = eventSource; _cancel = cancel; } /// /// Returns an enumerator that iterates through the Feed. /// /// Event Page Enumerator public async IAsyncEnumerator> GetAsyncEnumerator() { await using var subscribeFeed = _client.SubscribeFeed( _eventSource, _client.MappingCtx, _cancel); while (!_cancel.IsCancellationRequested && await subscribeFeed.MoveNextAsync()) { CurrentPage = subscribeFeed.Current; yield return CurrentPage; } } } ``` ## File: Fauna/Core/FeedOptions.cs ```csharp using Fauna.Types; namespace Fauna.Core; /// /// Represents the options when subscribing to Fauna event feeds. /// public class FeedOptions : EventOptions { /// /// Initializes a new instance of the class with the specified cursor and optional page size. /// /// The cursor for the feed. Used to resume the Feed. /// Optional page size for the feed. Sets the maximum number of events returned per page. Must /// be in the range 1 to 16000 (inclusive). Defaults to 16. /// public FeedOptions(string cursor, int? pageSize = null) { Cursor = cursor; PageSize = pageSize; } /// /// Initializes a new instance of the class with the specified start timestamp and optional page size. /// /// The start timestamp for the feed. Used to resume the Feed. /// Optional page size for the feed. Sets the maximum number of events returned per page. Must /// be in the range 1 to 16000 (inclusive). Defaults to 16. /// public FeedOptions(long startTs, int? pageSize = null) { StartTs = startTs; PageSize = pageSize; } /// /// Initializes a new instance of the class with the specified page size. /// /// Maximum number of events returned per page. Must /// be in the range 1 to 16000 (inclusive). Defaults to 16. public FeedOptions(int pageSize) { PageSize = pageSize; } } ``` ## File: Fauna/Core/FeedPage.cs ```csharp using System.Text.Json; using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Types; using static Fauna.Core.ResponseFields; namespace Fauna.Core; /// /// Represents the response from Fauna event feed requests. /// /// public class FeedPage where T : notnull { /// /// List of Events returned by the Feed /// public List> Events { get; private init; } = []; /// /// Cursor returned from the Feed /// public string Cursor { get; private init; } = null!; /// /// Indicates if there are more pages for pagination. /// public bool HasNext { get; private init; } /// /// Stats returned from the Feed. /// public QueryStats Stats { get; private init; } internal static FeedPage From(string body, MappingContext ctx) { var json = JsonSerializer.Deserialize(body); var err = GetError(json); if (err != null) { throw new FaunaException(err.Value); } return new FeedPage { Cursor = GetCursor(json), Events = GetEvents(json, ctx), Stats = GetStats(json), HasNext = json.TryGetProperty(HasNextFieldName, out var elem) && elem.GetBoolean() }; } private static List> GetEvents(JsonElement json, MappingContext ctx) { if (!json.TryGetProperty(EventsFieldName, out var elem)) { return new List>(); } var events = elem.EnumerateArray().Select(e => Event.From(e, ctx)).ToList(); return events; } private static QueryStats GetStats(JsonElement json) { return json.TryGetProperty(StatsFieldName, out var elem) ? elem.Deserialize() : default; } private static string GetCursor(JsonElement json) { return json.TryGetProperty(CursorFieldName, out var elem) ? elem.GetString()! : null!; } private static ErrorInfo? GetError(JsonElement json) { return json.TryGetProperty(ErrorFieldName, out var elem) ? elem.Deserialize() : null; } } ``` ## File: Fauna/Core/Headers.cs ```csharp namespace Fauna.Core; /// /// Contains constant values for HTTP header names used in Fauna API requests. /// internal readonly struct Headers { /// /// Header for the authorization token in API requests. /// public const string Authorization = "Authorization"; /// /// Header indicating the minimum snapshot time for the query execution based on the highest transaction timestamp observed by the client. /// public const string LastTxnTs = "X-Last-Txn-Ts"; /// /// Header to enforce strictly serialized execution of the query, affecting read-only transactions. /// public const string Linearized = "X-Linearized"; /// /// Header indicating the maximum number of retries for a transaction due to contention failure before returning an error. /// public const string MaxContentionRetries = "X-Max-Contention-Retries"; /// /// Header specifying the query timeout in milliseconds. /// public const string QueryTimeoutMs = "X-Query-Timeout-Ms"; /// /// Header to enable or disable type checking of the query before evaluation. /// public const string TypeCheck = "X-Typecheck"; /// /// Header for passing custom, string-encoded tags for request identification in logs and responses. /// public const string QueryTags = "X-Query-Tags"; /// /// Header for the trace parent identifier in distributed tracing systems. /// public const string TraceParent = "Traceparent"; /// /// Header indicating the driver used for the API request. /// public const string Driver = "X-Driver"; /// /// Header for specifying the environment of the driver used in the API request. /// public const string DriverEnv = "X-Driver-Env"; /// /// Header for specifying the encoded format for query arguments and response data. /// Options are 'simple' and 'tagged'. 'Simple' is the default format. /// public const string Format = "X-Format"; } ``` ## File: Fauna/Core/IConnection.cs ```csharp using Fauna.Mapping; using Fauna.Types; using Stream = System.IO.Stream; namespace Fauna.Core; /// /// Represents an interface for making HTTP requests. /// internal interface IConnection : IDisposable { /// /// Asynchronously sends a POST request to the specified path with the provided body and headers. /// /// The path of the resource to send the request to. /// The stream containing the request body. /// A dictionary of headers to be included in the request. /// The HTTP request timeout /// A cancellation token to use with the request. /// A Task representing the asynchronous operation, which upon completion contains the response from the server as . Task DoPostAsync( string path, Stream body, Dictionary headers, TimeSpan requestTimeout, CancellationToken cancel); /// /// Asynchronously sends a POST request to open Stream. /// /// The path of the resource to send the request to. /// /// The headers to include in the request. /// /// A cancellation token that can be used to cancel the operation. /// A task that represents the asynchronous operation, which opens the HTTP stream and returns the . /// Implementation IAsyncEnumerable> OpenStream( string path, Types.EventSource eventSource, Dictionary headers, MappingContext ctx, CancellationToken cancellationToken = default) where T : notnull; } ``` ## File: Fauna/Core/QueryOptions.cs ```csharp namespace Fauna.Core; /// /// Represents the options for customizing Fauna queries. /// public class QueryOptions { /// /// Gets or sets a value indicating whether the query runs as strictly serialized, affecting read-only transactions. /// public bool? Linearized { get; set; } = null; /// /// Gets or sets a value indicating whether type checking of the query is enabled or disabled before evaluation. /// public bool? TypeCheck { get; set; } = null; /// /// Gets or sets the query timeout. It defines how long the client waits for a query to complete. /// Default value is 5 seconds. /// public TimeSpan QueryTimeout { get; set; } = TimeSpan.FromSeconds(5); /// /// Gets or sets a string-encoded set of caller-defined tags for identifying the request in logs and response bodies. /// Each key and value should be limited to [a-zA-Z0-9_]. /// public Dictionary? QueryTags { get; set; } = null; /// /// Gets or sets the trace parent identifier for distributed tracing systems. /// public string? TraceParent { get; set; } = null; /// /// Merges two instances of . /// /// The default query options. /// The query options provided for a specific query, overriding the defaults. /// A object representing the final combined set of query options. internal static QueryOptions GetFinalQueryOptions(QueryOptions options, QueryOptions? overrides) { if (overrides == null) { return options; } var finalQueryOptions = new QueryOptions() { Linearized = options.Linearized, TypeCheck = options.TypeCheck, QueryTimeout = options.QueryTimeout, QueryTags = options.QueryTags, TraceParent = options.TraceParent, }; var properties = typeof(QueryOptions).GetProperties(); foreach (var prop in properties) { if (prop.Name.Equals(nameof(QueryTags))) { continue; } var propertyOverride = prop.GetValue(overrides); if (propertyOverride != null) { prop.SetValue(finalQueryOptions, propertyOverride); } } if (overrides.QueryTags != null) { if (finalQueryOptions.QueryTags == null) { finalQueryOptions.QueryTags = overrides.QueryTags; } else { foreach (var kv in overrides.QueryTags) { if (finalQueryOptions.QueryTags.ContainsKey(kv.Key)) { finalQueryOptions.QueryTags[kv.Key] = kv.Value; } else { finalQueryOptions.QueryTags.Add(kv.Key, kv.Value); } } } } return finalQueryOptions; } } ``` ## File: Fauna/Core/QueryResponse.cs ```csharp using System.Net; using System.Text.Json; using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Serialization; using static Fauna.Core.ResponseFields; namespace Fauna.Core; /// /// Represents the response from a query executed. /// public abstract class QueryResponse { /// /// The raw JSON of the query response. /// public JsonElement RawJson { get; init; } /// /// Gets the last transaction seen by this query. /// public long LastSeenTxn { get; init; } /// /// Gets the schema version. /// public long SchemaVersion { get; init; } /// /// Gets a summary of the query execution. /// public string Summary { get; init; } = ""; /// /// Gets a dictionary of query tags, providing additional context about the query. /// public Dictionary QueryTags { get; init; } = new(); /// /// Gets the statistics related to the query execution. /// public QueryStats Stats { get; init; } internal QueryResponse(JsonElement json) { RawJson = json; if (json.TryGetProperty(LastSeenTxnFieldName, out var elem)) { if (elem.TryGetInt64(out var i)) LastSeenTxn = i; } if (json.TryGetProperty(SchemaVersionFieldName, out elem)) { if (elem.TryGetInt64(out var i)) LastSeenTxn = i; } if (json.TryGetProperty(SummaryFieldName, out elem)) { Summary = elem.GetString() ?? ""; } if (json.TryGetProperty(QueryTagsFieldName, out elem)) { var queryTagsString = elem.GetString(); if (!string.IsNullOrEmpty(queryTagsString)) { var tagPairs = queryTagsString.Split(',').Select(tag => { var tokens = tag.Split('='); return KeyValuePair.Create(tokens[0], tokens[1]); }); QueryTags = new Dictionary(tagPairs); } } if (json.TryGetProperty(StatsFieldName, out elem)) { Stats = elem.Deserialize(); } } /// /// Asynchronously parses the HTTP response message to create a QueryResponse instance. /// /// The expected data type of the query response. /// Serialization context for handling response data. /// A serializer for the success data type. /// The HTTP status code. /// The response body. /// A Task that resolves to a QueryResponse instance. public static QueryResponse? GetFromResponseBody( MappingContext ctx, ISerializer serializer, HttpStatusCode statusCode, string body) { try { var json = JsonSerializer.Deserialize(body); if (statusCode is >= HttpStatusCode.OK and <= (HttpStatusCode)299) { return new QuerySuccess(ctx, serializer, json); } return new QueryFailure(statusCode, json); } catch (JsonException) { return null; } } } /// /// Represents a successful query response. /// /// The type of data expected in the query result. public sealed class QuerySuccess : QueryResponse { /// /// Gets the deserialized data from the query response. /// public T Data { get; init; } /// /// Gets the static type information from the query response, if available. /// public string? StaticType { get; init; } /// /// Initializes a new instance of the class, deserializing the query response into the specified type. /// /// The serialization context used for deserializing the response data. /// A deserializer for the response data type. /// The parsed JSON response body. public QuerySuccess( MappingContext ctx, ISerializer serializer, JsonElement json) : base(json) { var dataText = json.GetProperty(DataFieldName).GetRawText(); var reader = new Utf8FaunaReader(dataText); reader.Read(); Data = serializer.Deserialize(ctx, ref reader); if (json.TryGetProperty(StaticTypeFieldName, out var elem)) { StaticType = elem.GetString(); } } } /// /// Represents a failed query response. /// public sealed class QueryFailure : QueryResponse { /// /// The HTTP status code. /// public HttpStatusCode StatusCode { get; init; } /// /// The Fauna error code. /// public string ErrorCode { get; init; } = ""; /// /// The query failure message. /// public string Message { get; init; } = ""; /// /// The constraint failures, if any. Only present for the constraint_failure error code. /// public ConstraintFailure[]? ConstraintFailures { get; init; } /// /// The abort object, if any. Only present for the abort error code. /// public object? Abort { get; init; } /// /// Initializes a new instance of the class, parsing the provided raw response text to extract error information. /// /// The HTTP status code. /// The JSON response body. public QueryFailure(HttpStatusCode statusCode, JsonElement json) : base(json) { StatusCode = statusCode; if (!json.TryGetProperty(ErrorFieldName, out var elem)) return; var info = elem.Deserialize(); ErrorCode = info.Code ?? ""; Message = info.Message ?? ""; ConstraintFailures = info.ConstraintFailures; Abort = info.Abort; } } ``` ## File: Fauna/Core/QueryStats.cs ```csharp using System.Text.Json.Serialization; using static Fauna.Core.ResponseFields; namespace Fauna.Core; /// /// Contains statistics related to the execution of a query in the Fauna database. /// public readonly struct QueryStats { /// /// The number of compute operations consumed by the query. /// [JsonPropertyName(Stats_ComputeOpsFieldName)] public int ComputeOps { get; init; } /// /// The number of read operations consumed by the query. /// [JsonPropertyName(Stats_ReadOps)] public int ReadOps { get; init; } /// /// The number of write operations consumed by the query. /// [JsonPropertyName(Stats_WriteOps)] public int WriteOps { get; init; } /// /// The query processing time in milliseconds. /// [JsonPropertyName(Stats_QueryTimeMs)] public int QueryTimeMs { get; init; } /// /// The write contention retry count. /// [JsonPropertyName(Stats_ContentionRetries)] public int ContentionRetries { get; init; } /// /// The amount of data read from storage, in bytes. /// [JsonPropertyName(Stats_StorageBytesRead)] public int StorageBytesRead { get; init; } /// /// The amount of data written to storage, in bytes. /// [JsonPropertyName(Stats_StorageBytesWrite)] public int StorageBytesWrite { get; init; } /// /// The types of operations that were limited or approaching rate limits. /// [JsonPropertyName(Stats_RateLimitsHit)] public List RateLimitsHit { get; init; } /// /// Processing time in milliseconds. Only returned on Events. /// [JsonPropertyName(Stats_ProcessingTimeMs)] public int? ProcessingTimeMs { get; init; } /// /// Returns a string representation of the query statistics. /// /// A string detailing the query execution statistics. public override string ToString() { return $"compute: {ComputeOps}, read: {ReadOps}, write: {WriteOps}, " + $"queryTime: {QueryTimeMs}, retries: {ContentionRetries}, " + $"storageRead: {StorageBytesRead}, storageWrite: {StorageBytesWrite}, " + $"{(ProcessingTimeMs.HasValue ? $"processingTime: {ProcessingTimeMs}, " : "")}" + $"limits: [{string.Join(',', RateLimitsHit)}]"; } } ``` ## File: Fauna/Core/ResponseFields.cs ```csharp namespace Fauna.Core; /// /// Contains constant values for the response field names returned by Fauna API queries. /// internal readonly struct ResponseFields { #region Top-level fields /// /// Field name for the main data content of the response. /// public const string DataFieldName = "data"; /// /// Field name for the transaction timestamp of the last transaction seen by the request. /// public const string LastSeenTxnFieldName = "txn_ts"; /// /// Field name for the stream cursor of the response. /// public const string CursorFieldName = "cursor"; /// /// Field name for static type information in the response. /// public const string StaticTypeFieldName = "static_type"; /// /// Field name for statistical information about the query execution. /// public const string StatsFieldName = "stats"; /// /// Field name for the schema version of the database at the time of query execution. /// public const string SchemaVersionFieldName = "schema_version"; /// /// Field name for the summary information about the query execution. /// public const string SummaryFieldName = "summary"; /// /// Field name for query tags associated with the request, used in logging and monitoring. /// public const string QueryTagsFieldName = "query_tags"; /// /// Field name for error information if the query fails. /// public const string ErrorFieldName = "error"; /// /// Field name for pagination information. /// public const string HasNextFieldName = "has_next"; /// /// Field name for array of events. /// public const string EventsFieldName = "events"; #endregion #region "stats" block /// /// Field name for the number of compute operations consumed by the query. /// public const string Stats_ComputeOpsFieldName = "compute_ops"; /// /// Field name for the number of read operations consumed by the query. /// public const string Stats_ReadOps = "read_ops"; /// /// Field name for the number of write operations consumed by the query. /// public const string Stats_WriteOps = "write_ops"; /// /// Field name for the query processing time in milliseconds. /// public const string Stats_QueryTimeMs = "query_time_ms"; /// /// Field name for the write contention retry count. /// public const string Stats_ContentionRetries = "contention_retries"; /// /// Field name for the amount of data read from storage, in bytes. /// public const string Stats_StorageBytesRead = "storage_bytes_read"; /// /// Field name for the amount of data written to storage, in bytes. /// public const string Stats_StorageBytesWrite = "storage_bytes_write"; /// /// Field name for the types of operations that were limited or approaching rate limits. /// public const string Stats_RateLimitsHit = "rate_limits_hit"; /// /// Field name for the processing time in milliseconds. /// public const string Stats_ProcessingTimeMs = "processing_time_ms"; #endregion #region "error" block /// /// Field name for the error code when a query fails. /// public const string Error_CodeFieldName = "code"; /// /// Field name for the detailed message describing the cause of the error. /// public const string Error_MessageFieldName = "message"; /// /// Field name for constraint failures that occurred during the query. /// public const string Error_ConstraintFailuresFieldName = "constraint_failures"; /// /// Field name for the message in constraint failures. /// public const string Error_ConstraintFailuresMessageFieldName = "message"; /// /// Field name for the name in constraint failures. /// public const string Error_ConstraintFailuresNameFieldName = "name"; /// /// Field name for the paths in constraint failures. /// public const string Error_ConstraintFailuresPathsFieldName = "paths"; /// /// Field name for information about an abort operation within a transaction. /// public const string Error_AbortFieldName = "abort"; #endregion } ``` ## File: Fauna/Core/RetryConfiguration.cs ```csharp using System.Net; using System.Net.Sockets; using Polly; namespace Fauna.Core; /// /// A class representing a retry configuration for queries. /// public class RetryConfiguration { /// /// Gets the retry policy. /// public AsyncPolicy RetryPolicy { get; set; } /// /// Creates a new instance. /// /// Maximum times to retry a request. /// The maximum backoff to apply. public RetryConfiguration(int retryCount, TimeSpan maxBackoff) { RetryPolicy = Policy .Handle() .Or() .Or() .Or() .OrResult(r => r.StatusCode == HttpStatusCode.TooManyRequests) .WaitAndRetryAsync(retryCount, attempt => { int calculated = (int)Math.Floor(Math.Pow(2, attempt)); int backoff = calculated > maxBackoff.Seconds ? maxBackoff.Seconds : calculated; return TimeSpan.FromSeconds(backoff); }); } } ``` ## File: Fauna/Core/StatsCollector.cs ```csharp namespace Fauna.Core; /// /// A struct representing stats aggregated across queries. /// public readonly struct Stats { /// /// The aggregate read ops. /// public long ReadOps { get; init; } /// /// The aggregate compute ops. /// public long ComputeOps { get; init; } /// /// The aggregate write ops. /// public long WriteOps { get; init; } /// /// The aggregate query time in milliseconds. /// public long QueryTimeMs { get; init; } /// /// The aggregate number of retries due to transaction contention. /// public int ContentionRetries { get; init; } /// /// The aggregate number of storage bytes read. /// public long StorageBytesRead { get; init; } /// /// The aggregate number of storage bytes written. /// public long StorageBytesWrite { get; init; } /// /// The aggregate number of queries summarized. /// public int QueryCount { get; init; } /// /// The aggregate count of rate limited queries due to read limits. /// public int RateLimitedReadQueryCount { get; init; } /// /// The aggregate count of rate limited queries due to compute limits. /// public int RateLimitedComputeQueryCount { get; init; } /// /// The aggregate count of rate limited queries due to write limits. /// public int RateLimitedWriteQueryCount { get; init; } } /// /// An interface used by a client instance for aggregating stats across all queries. /// public interface IStatsCollector { /// /// Add the to the current counts. /// /// QueryStats public void Add(QueryStats stats); /// /// Return the collected . /// public Stats Read(); /// /// Return the collected and Reset counts. /// public Stats ReadAndReset(); } /// /// The default implementation of . /// public class StatsCollector : IStatsCollector { private const string RateLimitReadOps = "read"; private const string RateLimitComputeOps = "compute"; private const string RateLimitWriteOps = "write"; private long _readOps; private long _computeOps; private long _writeOps; private long _queryTimeMs; private int _contentionRetries; private long _storageBytesRead; private long _storageBytesWrite; private int _queryCount; private int _rateLimitedReadQueryCount; private int _rateLimitedComputeQueryCount; private int _rateLimitedWriteQueryCount; /// public void Add(QueryStats stats) { Interlocked.Exchange(ref _readOps, _readOps + stats.ReadOps); Interlocked.Exchange(ref _computeOps, _computeOps + stats.ComputeOps); Interlocked.Exchange(ref _writeOps, _writeOps + stats.WriteOps); Interlocked.Exchange(ref _queryTimeMs, _queryTimeMs + stats.QueryTimeMs); Interlocked.Exchange(ref _contentionRetries, _contentionRetries + stats.ContentionRetries); Interlocked.Exchange(ref _storageBytesRead, _storageBytesRead + stats.StorageBytesRead); Interlocked.Exchange(ref _storageBytesWrite, _storageBytesWrite + stats.StorageBytesWrite); stats.RateLimitsHit?.ForEach(limitHit => { switch (limitHit) { case RateLimitReadOps: Interlocked.Increment(ref _rateLimitedComputeQueryCount); break; case RateLimitComputeOps: Interlocked.Increment(ref _rateLimitedComputeQueryCount); break; case RateLimitWriteOps: Interlocked.Increment(ref _rateLimitedWriteQueryCount); break; } }); Interlocked.Increment(ref _queryCount); } /// public Stats Read() { return new Stats { ReadOps = _readOps, ComputeOps = _computeOps, WriteOps = _writeOps, QueryTimeMs = _queryTimeMs, ContentionRetries = _contentionRetries, StorageBytesRead = _storageBytesRead, StorageBytesWrite = _storageBytesWrite, QueryCount = _queryCount, RateLimitedReadQueryCount = _rateLimitedReadQueryCount, RateLimitedComputeQueryCount = _rateLimitedComputeQueryCount, RateLimitedWriteQueryCount = _rateLimitedWriteQueryCount }; } /// public Stats ReadAndReset() { var beforeReset = new Stats { ReadOps = Interlocked.Exchange(ref _readOps, 0), ComputeOps = Interlocked.Exchange(ref _computeOps, 0), WriteOps = Interlocked.Exchange(ref _writeOps, 0), QueryTimeMs = Interlocked.Exchange(ref _queryTimeMs, 0), ContentionRetries = Interlocked.Exchange(ref _contentionRetries, 0), StorageBytesRead = Interlocked.Exchange(ref _storageBytesRead, 0), StorageBytesWrite = Interlocked.Exchange(ref _storageBytesWrite, 0), QueryCount = Interlocked.Exchange(ref _queryCount, 0), RateLimitedReadQueryCount = Interlocked.Exchange(ref _rateLimitedReadQueryCount, 0), RateLimitedComputeQueryCount = Interlocked.Exchange(ref _rateLimitedComputeQueryCount, 0), RateLimitedWriteQueryCount = Interlocked.Exchange(ref _rateLimitedWriteQueryCount, 0) }; return beforeReset; } } ``` ## File: Fauna/Core/StreamEnumerable.cs ```csharp using Fauna.Types; namespace Fauna.Core; /// /// A class representing a Fauna event stream. Additional queries will be made during enumeration. /// /// The return type of the stream. public class StreamEnumerable where T : notnull { private readonly BaseClient _client; private readonly EventSource _eventSource; private readonly CancellationToken _cancel; /// /// The token for the event source. /// public string Token => _eventSource.Token; internal StreamEnumerable( BaseClient client, EventSource eventSource, CancellationToken cancel = default) { _client = client; _eventSource = eventSource; _cancel = cancel; } /// /// Gets an async enumerator for the stream. /// /// An async enumerator that yields . public async IAsyncEnumerator> GetAsyncEnumerator() { await using var subscribeStream = _client.SubscribeStream( _eventSource, _client.MappingCtx, _cancel); while (!_cancel.IsCancellationRequested && await subscribeStream.MoveNextAsync()) { yield return subscribeStream.Current; } } } ``` ## File: Fauna/Core/StreamOptions.cs ```csharp using Fauna.Types; namespace Fauna; /// /// Represents the options when subscribing to Fauna event streams. /// public class StreamOptions : EventOptions { /// /// Initializes a new instance of the class with the specified token and cursor. /// /// The token for a Fauna event source. /// The cursor from the stream, must be used with the associated Token. Used to resume the stream. /// See Restart an event stream. public StreamOptions(string token, string cursor) { Token = token; Cursor = cursor; } /// /// Initializes a new instance of the class with the specified token and start timestamp. /// /// The token for a Fauna event source. /// The start timestamp to use for the stream. public StreamOptions(string token, long startTs) { Token = token; StartTs = startTs; } /// Token for a Fauna event source. /// See the Create an event source. public string? Token { get; } } ``` ## File: Fauna/Exceptions/AbortException.cs ```csharp using Fauna.Core; using Fauna.Mapping; using Fauna.Serialization; namespace Fauna.Exceptions; /// /// Represents an exception that occurs when the FQL `abort` function is called. /// This exception captures the data provided during the abort operation. /// public class AbortException : ServiceException { private readonly MappingContext _ctx; private readonly Dictionary _cache = new(); private readonly object? _abortRaw; /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A /// A mapping context. public AbortException(string message, QueryFailure failure, MappingContext ctx) : base(message, failure) { _ctx = ctx; _abortRaw = failure.Abort; } /// /// Retrieves the deserialized data associated with the abort operation as an object. /// /// The deserialized data as an object, or null if no data is available. public object? GetData() => GetData(Serializer.Dynamic); /// /// Retrieves the deserialized data associated with the abort operation as a specific type. /// /// The type to which the data should be deserialized. /// The deserialized data as the specified type, or null if no data is available. public T? GetData() where T : notnull => GetData(Serializer.Generate(_ctx)); /// /// Retrieves the deserialized data associated with the abort operation as a specific type. /// /// The type to which the data should be deserialized. /// A serializer for the abort data. /// The deserialized data as the specified type, or null if no data is available. public T? GetData(ISerializer serializer) { var typeKey = typeof(T); if (_cache.TryGetValue(typeKey, out var cachedData)) return (T?)cachedData; if (_abortRaw == null) return (T?)cachedData; var abortDataString = _abortRaw.ToString(); if (string.IsNullOrEmpty(abortDataString)) return (T?)cachedData; // TODO(matt) pull from context var reader = new Utf8FaunaReader(abortDataString); reader.Read(); var deserializedResult = serializer.Deserialize(_ctx, ref reader); _cache[typeKey] = deserializedResult; return deserializedResult; } } ``` ## File: Fauna/Exceptions/AuthenticationException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents an exception thrown when there is an authorization error in Fauna. /// Corresponds to the 'unauthorized' error code in Fauna. /// public class AuthenticationException : ServiceException { /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A . public AuthenticationException(string message, QueryFailure failure) : base(message, failure) { } } ``` ## File: Fauna/Exceptions/AuthorizationException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents an exception thrown when access to a resource is not allowed. /// Corresponds to the 'forbidden' error code in Fauna. /// public class AuthorizationException : ServiceException { /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A public AuthorizationException(string message, QueryFailure failure) : base(message, failure) { } } ``` ## File: Fauna/Exceptions/BadGatewayException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents an exception thrown for a bad gateway. /// Corresponds to the 'bad_gateway' error code in Fauna. /// public class BadGatewayException : ServiceException { /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A public BadGatewayException(string message, QueryFailure failure) : base(message, failure) { } } ``` ## File: Fauna/Exceptions/ConstraintFailure.cs ```csharp using System.Text.Json.Serialization; using static Fauna.Core.ResponseFields; namespace Fauna.Exceptions; /// /// A class representing a constraint failure from Fauna. /// public class ConstraintFailure { /// /// Initializes a new . /// /// The message describing the constraint failure. /// The name of the constraint failure. /// The paths for the constraint failure. public ConstraintFailure(string message, string name, object[][]? paths) { Message = message; Name = name; Paths = paths; } /// /// The constraint failure message describing the specific check that failed. /// [JsonPropertyName(Error_ConstraintFailuresMessageFieldName)] public string Message { get; set; } /// /// The constraint failure name. /// [JsonPropertyName(Error_ConstraintFailuresNameFieldName)] public string Name { get; set; } /// /// The constraint failure paths. /// [JsonPropertyName(Error_ConstraintFailuresPathsFieldName)] public object[][]? Paths { get; set; } } ``` ## File: Fauna/Exceptions/ConstraintFailureException.cs ```csharp using Fauna.Core; using Fauna.Mapping; namespace Fauna.Exceptions; /// /// Represents an exception that occurs when constraints are violated in a query. /// This exception captures the specific constraint failures for inspection. /// public class ConstraintFailureException : ServiceException { /// /// The constraint failures related to the exception. /// public ConstraintFailure[]? ConstraintFailures { get; } /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A public ConstraintFailureException(string message, QueryFailure failure) : base(message, failure) { ConstraintFailures = failure.ConstraintFailures; } } ``` ## File: Fauna/Exceptions/ContendedTransactionException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents an exception that occurs when a transaction is aborted due to concurrent modification. /// This exception is considered retryable after a suitable delay. /// public class ContendedTransactionException : ServiceException, IRetryableException { /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A . public ContendedTransactionException(string message, QueryFailure failure) : base(message, failure) { } } ``` ## File: Fauna/Exceptions/EventException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents an exception related to Fauna event stream and event feed errors. /// public class EventException : ServiceException { /// /// Initializes a new instance of the class. /// /// The from which to extract a message. public EventException(ErrorInfo err) : base(message: err.Message!) { } } ``` ## File: Fauna/Exceptions/ExceptionHandler.cs ```csharp using System.Net; using Fauna.Core; using Fauna.Mapping; namespace Fauna.Exceptions; /// /// A utility class for generating an appropriate from a . /// public static class ExceptionHandler { /// /// Creates an exception from a /// /// A used for exceptions that require additional deserialization, such as . /// The . /// public static Exception FromQueryFailure(MappingContext ctx, QueryFailure f) { var msg = $"{f.StatusCode} ({f.ErrorCode}): {f.Message}{(f.Summary is { Length: > 0 } ? "\n---\n" + f.Summary : "")}"; return f.ErrorCode switch { "abort" => new AbortException(msg, f, ctx), "bad_gateway" => new BadGatewayException(msg, f), "contended_transaction" => new ContendedTransactionException(msg, f), "forbidden" => new AuthorizationException(msg, f), "internal_error" => new ServiceException(msg, f), "invalid_query" => new QueryCheckException(msg, f), "invalid_request" => new InvalidRequestException(msg, f), "limit_exceeded" => new ThrottlingException(msg, f), "time_out" => new QueryTimeoutException(msg, f), "gateway_timeout" => new NetworkException(msg, f.StatusCode, f.Message), "unauthorized" => new AuthenticationException(msg, f), "constraint_failure" => new ConstraintFailureException(msg, f), _ => new QueryRuntimeException(msg, f) }; } /// /// Creates an exception from a body and an already consumed /// /// The response body. /// The with consumed body. /// public static Exception FromRawResponse(string body, HttpResponseMessage r) { if (r.StatusCode is >= HttpStatusCode.OK and <= (HttpStatusCode)299) { // We should never get here, but if we do it's outside of the expected wire protocol. return new ProtocolException("Malformed response.", r.StatusCode, body); } return r.StatusCode switch { HttpStatusCode.TooManyRequests => new ThrottlingException( $"{r.StatusCode}: {r.ReasonPhrase ?? "Too many requests."}"), _ => new FaunaException($"{r.StatusCode}: {r.ReasonPhrase}") }; } } ``` ## File: Fauna/Exceptions/FaunaException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents the base exception class for all exceptions specific to Fauna interactions. /// public class FaunaException : Exception { /// /// Initializes a FaunaException with a message. /// /// The exception message. public FaunaException(string message) : base(message) { } /// /// Initializes a FaunaException with a message and inner exception. /// /// The exception message. /// The inner exception. public FaunaException(string message, Exception innerException) : base(message, innerException) { } /// /// Initializes a FaunaException from an instance. /// /// The from which to extract a message. public FaunaException(ErrorInfo err) : base(message: err.Message) { } } ``` ## File: Fauna/Exceptions/InvalidRequestException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents exceptions caused by invalid requests to Fauna. /// public class InvalidRequestException : ServiceException { /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A . public InvalidRequestException(string message, QueryFailure failure) : base(message, failure) { } } ``` ## File: Fauna/Exceptions/IRetryableException.cs ```csharp namespace Fauna.Exceptions; /// /// Represents an interface for exceptions that are potentially recoverable through retrying the failed operation. /// public interface IRetryableException { } ``` ## File: Fauna/Exceptions/NetworkException.cs ```csharp using System.Net; namespace Fauna.Exceptions; /// /// Represents an exception that occurs when a request fails due to a network issue. /// public class NetworkException : FaunaException { /// /// The response body that caused the to be thrown. /// public string ResponseBody { get; init; } /// /// The HTTP status code associated with the . /// public HttpStatusCode StatusCode { get; init; } /// /// Initializes a new instance of the class. /// /// The error message that explains the reason for the exception. /// The HTTP status code from the related HTTP request. /// The HTTP response body that was out of protocol. public NetworkException(string message, HttpStatusCode statusCode, string body) : base(message) { StatusCode = statusCode; ResponseBody = body; } } ``` ## File: Fauna/Exceptions/NullDocumentException.cs ```csharp using Fauna.Types; namespace Fauna.Exceptions; /// /// An exception representing a case when a document cannot be materialized because it does not exist. /// public class NullDocumentException : Exception { /// /// The ID associated with the document. In the case of named documents, this will be null. /// public string? Id { get; } /// /// The name associated with the document. In the case of user documents, this will be null. /// public string? Name { get; } /// /// The collection to which the document belongs. /// public Module Collection { get; } /// /// The cause for the null document. /// public string Cause { get; } /// /// Initializes a new instance of the class. /// /// The ID of the document. Should be null if it's a named document. /// The name of the document. Should be null if it's a user docuemnt. /// The collection associated with the document. /// The cause of the null document. public NullDocumentException(string? id, string? name, Module collection, string cause) : base($"Document {id ?? name} in collection {collection.Name} is null: {cause}") { Id = id; Name = name; Collection = collection; Cause = cause; } } ``` ## File: Fauna/Exceptions/ProtocolException.cs ```csharp using System.Net; namespace Fauna.Exceptions; /// /// Represents exceptions when a response does not match the wire protocol. /// public class ProtocolException : FaunaException { /// /// The response body that caused the to be thrown. /// public string ResponseBody { get; init; } /// /// The HTTP status code associated with the . /// public HttpStatusCode StatusCode { get; init; } /// /// Initializes a new instance of the class. /// /// The error message that explains the reason for the exception. /// The HTTP status code from the related HTTP request. /// The HTTP response body that was out of protocol. public ProtocolException(string message, HttpStatusCode statusCode, string body) : base(message) { StatusCode = statusCode; ResponseBody = body; } } ``` ## File: Fauna/Exceptions/QueryCheckException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents exceptions thrown when the query has syntax errors. /// public class QueryCheckException : ServiceException { /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A . public QueryCheckException(string message, QueryFailure failure) : base(message, failure) { } } ``` ## File: Fauna/Exceptions/QueryRuntimeException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents exceptions thrown when the query fails at runtime. /// public class QueryRuntimeException : ServiceException { /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A . public QueryRuntimeException(string message, QueryFailure failure) : base(message, failure) { } } ``` ## File: Fauna/Exceptions/QueryTimeoutException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents exceptions thrown when the query execution time exceeds the specified or default timeout period. /// public class QueryTimeoutException : TimeoutException { /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A . public QueryTimeoutException(string message, QueryFailure failure) : base(message, failure) { } } ``` ## File: Fauna/Exceptions/SerializationException.cs ```csharp namespace Fauna.Exceptions; /// /// Represents error that occur during serialization and deserialization of Fauna data. /// public class SerializationException : Exception { /// /// Initializes a new instance of the class with a specified error message. /// /// The error message that explains the reason for the exception. public SerializationException(string? message) : base(message) { } /// /// Initializes a new instance of the class with a specified error message and inner exception. /// /// The error message that explains the reason for the exception. /// The inner exception. public SerializationException(string? message, Exception? innerException) : base(message, innerException) { } } ``` ## File: Fauna/Exceptions/ServiceException.cs ```csharp using System.Net; using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents an exception related to Fauna service errors, particularly for query failures. /// public class ServiceException : FaunaException { /// /// The error code when a query fails. /// public string? ErrorCode { get; init; } /// /// The tags on the x-query-tags header, if it was provided. /// public IDictionary QueryTags { get; init; } /// /// The schema version used by the query. This can be used by clients displaying /// schema to determine when they should refresh their schema. If the schema /// version that a client has stored differs from the one returned by the query, /// schema should be refreshed. /// public long? SchemaVersion { get; init; } /// /// The query stats for the request. /// public QueryStats Stats { get; init; } /// /// The HTTP status code. /// public HttpStatusCode? StatusCode { get; set; } /// /// A comprehensive, human readable summary of any errors, warnings and/or logs returned from the query. /// public string? Summary { get; init; } /// /// The transaction commit time in micros since epoch. Used by drivers to /// populate the x-last-txn-ts request header in order to get a consistent /// prefix RYOW guarantee. /// public long? TxnTs { get; init; } /// /// Initializes a new instance of the class with a specified query failure details and error message. /// /// The error message that explains the reason for the exception. public ServiceException(string message) : base(message) { QueryTags = new Dictionary(); } /// /// Initializes a new instance of the class with a specified query failure details and error message. /// /// The error message that explains the reason for the exception. /// A public ServiceException(string message, QueryFailure failure) : base(message) { StatusCode = failure.StatusCode; ErrorCode = failure.ErrorCode; Summary = failure.Summary; Stats = failure.Stats; TxnTs = failure.LastSeenTxn; SchemaVersion = failure.SchemaVersion; QueryTags = failure.QueryTags; } } ``` ## File: Fauna/Exceptions/ThrottlingException.cs ```csharp using System.Net; using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents an exception that indicates some capacity limit was exceeded and thus the request could not be served. /// This exception is considered retryable after a suitable delay. /// public class ThrottlingException : ServiceException, IRetryableException { /// /// Initializes a new instance of the class with a specified error message. /// /// The error message that explains the reason for the exception. public ThrottlingException(string message) : base(message) { StatusCode = HttpStatusCode.TooManyRequests; } /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A . public ThrottlingException(string message, QueryFailure failure) : base(message, failure) { } } ``` ## File: Fauna/Exceptions/TimeoutException.cs ```csharp using Fauna.Core; namespace Fauna.Exceptions; /// /// Represents exceptions thrown when the query execution time exceeds the specified or default timeout period. /// public class TimeoutException : ServiceException { /// /// Initializes a new instance of the class with a specified error message and query failure details. /// /// The error message that explains the reason for the exception. /// A . public TimeoutException(string message, QueryFailure failure) : base(message, failure) { } } ``` ## File: Fauna/Linq/DataContext.cs ```csharp using System.Collections.Immutable; using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Runtime.CompilerServices; using Fauna.Core; using Fauna.Mapping; using Fauna.Types; namespace Fauna.Linq; /// /// An abstract class representing a DataContext. This is a special type of Fauna client that can be used to execute LINQ-style queries. /// /// Users should implement this for each database they'd like to query with LINQ. /// public abstract class DataContext : BaseClient { private bool _initialized = false; [AllowNull] private IReadOnlyDictionary _collections = null!; [AllowNull] private Client _client = null!; [AllowNull] private MappingContext _ctx = null!; internal override MappingContext MappingCtx { get => _ctx; } internal Linq.LookupTable LookupTable { get => new Linq.LookupTable(_ctx); } internal void Init(Client client, Dictionary collections, MappingContext ctx) { _client = client; _collections = collections.ToImmutableDictionary(); _ctx = ctx; foreach (var col in collections.Values) { ((Linq.QuerySource)col).SetContext(this); } _initialized = true; } // IClient impl internal override Task> QueryAsyncInternal( Query query, Serialization.ISerializer serializer, MappingContext ctx, QueryOptions? queryOptions, CancellationToken cancel) { CheckInitialization(); return _client.QueryAsyncInternal(query, serializer, ctx, queryOptions, cancel); } internal override IAsyncEnumerator> SubscribeStreamInternal( EventSource eventSource, MappingContext ctx, CancellationToken cancel = default) { CheckInitialization(); return _client.SubscribeStreamInternal(eventSource, ctx, cancel); } internal override IAsyncEnumerator> SubscribeFeedInternal( EventSource eventSource, MappingContext ctx, CancellationToken cancel = default) { CheckInitialization(); return _client.SubscribeFeedInternal(eventSource, ctx, cancel); } // Schema DSL /// /// An attribute representing a collection name. /// [AttributeUsage(AttributeTargets.Class)] public class NameAttribute : Attribute { internal readonly string Name; /// /// Initializes a . /// /// The collection name. public NameAttribute(string name) { Name = name; } } /// /// An interface for a Fauna collection within a . /// public interface ICollection : Linq.IQuerySource { /// /// The collection name. /// public string Name { get; } /// /// The .NET type associated with documents in the collection. /// public Type DocType { get; } } /// /// An abstract collection. This should be implemented for each collection in the database. /// /// The .NET type associated with documents in the collection. public abstract class Collection : Linq.QuerySource, ICollection { /// public string Name { get; } /// public Type DocType { get => typeof(Doc); } /// /// Initializes a new collection with a name set to the , or the name of /// .NET type associated with its documents. /// public Collection() { var nameAttr = this.GetType().GetCustomAttribute(); Name = nameAttr?.Name ?? typeof(Doc).Name; SetQuery(Linq.IntermediateQueryHelpers.CollectionAll(this)); } // index call DSL /// /// Initializes an index associated with the collection. The name of the index can be assigned, or if /// declared inside a concrete , a canonical name will be assigned when a name /// is not provided. /// /// The name of the index. /// Used to generate a canonical name when name is null. /// /// protected IndexCall Index(string? name = null, [CallerMemberName] string? auto = null) { if (name is null && auto is not null) { name = FieldName.Canonical(auto); } if (string.IsNullOrEmpty(name)) throw new ArgumentException($"{nameof(name)} cannot be null or empty."); return new IndexCall(this, name, Ctx); } /// /// A class representing an index call. /// protected class IndexCall { private readonly ICollection _coll; private readonly string _name; private readonly DataContext _ctx; /// /// Initializes an index call. /// /// The collection the index belongs to. /// The name of the index. /// The public IndexCall(ICollection coll, string name, DataContext ctx) { _coll = coll; _name = name; _ctx = ctx; } /// /// Invokes an index. /// /// An query source. public Index Call() => Call(new object[] { }); /// /// Invokes an index. /// /// An index argument. /// An query source. public Index Call(object a1) => Call(new object[] { a1 }); /// /// Invokes an index. /// /// An index argument. /// An index argument. /// An query source. public Index Call(object a1, object a2) => Call(new object[] { a1, a2 }); /// /// Invokes an index. /// /// An index argument. /// An index argument. /// An index argument. /// An query source. public Index Call(object a1, object a2, object a3) => Call(new object[] { a1, a2, a3 }); /// /// Invokes an index. /// /// An array of index arguments. /// An query source. public Index Call(object[] args) => new Index(_coll, _name, args, _ctx); } } /// /// An interface representing an index query source. /// public interface IIndex : Linq.IQuerySource { /// /// The collection the index belongs to. /// public ICollection Collection { get; } /// /// The name of the index. /// public string Name { get; } /// /// The return type of the index. /// public Type DocType { get; } /// /// An index argument array. /// public object[] Args { get; } } /// /// A class representing an index query source. /// /// public class Index : Linq.QuerySource, IIndex { /// public ICollection Collection { get; } /// public string Name { get; } /// public Type DocType { get => typeof(Doc); } /// public object[] Args { get; } internal Index(ICollection coll, string name, object[] args, DataContext ctx) { Collection = coll; Name = name; Args = args; Ctx = ctx; SetQuery(Linq.IntermediateQueryHelpers.CollectionIndex(this)); } } // UDF / Function DSL /// /// An interface representing a function. /// public interface IFunction : Linq.IQuerySource { /// /// The name of the function. /// public string Name { get; } /// /// An array of arguments for the function. /// public object[] Args { get; } } /// /// A class representing a function call. /// /// The return type of the function. protected class FunctionCall where T : notnull { /// /// The name of the function. /// public string Name { get; } private readonly DataContext _ctx; /// /// Initializes a function call. /// /// The name of the function. /// The that the function belongs to. public FunctionCall(string name, DataContext ctx) { Name = name; _ctx = ctx; } /// /// Calls the function. /// /// The result of the function call. public T Call() => Call(Array.Empty()); /// /// Calls the function. /// /// A function call argument. /// The result of the function call. public T Call(object a1) => Call(new[] { a1 }); /// /// Calls the function. /// /// A function call argument. /// A function call argument. /// The result of the function call. public T Call(object a1, object a2) => Call(new[] { a1, a2 }); /// /// Calls the function. /// /// A function call argument. /// A function call argument. /// A function call argument. /// The result of the function call. public T Call(object a1, object a2, object a3) => Call(new[] { a1, a2, a3 }); /// /// Calls the function. /// /// An array of function call arguments. /// The result of the function call. public T Call(object[] args) => CallAsync(args).Result; /// /// Calls the function asynchronously. /// /// The result of the function call. public async Task CallAsync() => await CallAsync(Array.Empty()); /// /// Calls the function asynchronously. /// /// A function call argument. /// The result of the function call. public async Task CallAsync(object a1) => await CallAsync(new[] { a1 }); /// /// Calls the function asynchronously. /// /// A function call argument. /// A function call argument. /// The result of the function call. public async Task CallAsync(object a1, object a2) => await CallAsync(new[] { a1, a2 }); /// /// Calls the function asynchronously. /// /// A function call argument. /// A function call argument. /// A function call argument. /// The result of the function call. public async Task CallAsync(object a1, object a2, object a3) => await CallAsync(new[] { a1, a2, a3 }); /// /// Calls the function asynchronously. /// /// An array of function call arguments. /// The result of the function call. public async Task CallAsync(object[] args) { var q = Linq.IntermediateQueryHelpers.Function(Name, args); return (await _ctx.QueryAsync(q)).Data; } } /// /// A helper method to declare new function calls. /// /// The name of the function. If null, the caller name is used. /// The caller name. /// /// protected FunctionCall Fn(string name = "", [CallerMemberName] string callerName = "") where T : notnull { var fnName = name == "" ? callerName : name; return new FunctionCall(fnName, this); } /// /// Gets a collection of type . /// /// The type of the collection. /// The collection protected Col GetCollection() where Col : ICollection { CheckInitialization(); return (Col)_collections[typeof(Col)]; } private void CheckInitialization() { if (!_initialized) { throw new InvalidOperationException( "Uninitialized context. DataContext sub-classes must be instantiated using a client's .DataContext() method."); } } } ``` ## File: Fauna/Linq/DataContextBuilder.cs ```csharp using System.Diagnostics; using System.Reflection; using Fauna.Mapping; using Fauna.Util.Extensions; namespace Fauna.Linq; internal class DataContextBuilder where DB : DataContext { public DB Build(Client client) { var dbType = typeof(DB); const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static; var colTypes = dbType.GetNestedTypes(flags).Where(IsColType).ToList(); var colProps = dbType.GetProperties(flags).Where(IsColProp).ToList(); foreach (var ty in colTypes) { ValidateColType(ty); } foreach (var p in colProps) { ValidateColProp(colTypes, p); } var colImpls = new Dictionary(); foreach (var ty in colTypes) { colImpls[ty] = (DataContext.ICollection)Activator.CreateInstance(ty)!; var nameAttr = ty.GetCustomAttribute(); var colName = nameAttr?.Name ?? ty.Name; } var db = (DB)Activator.CreateInstance(dbType)!; db.Init(client, colImpls, new MappingContext(colImpls.Values)); return db; } private static bool IsColType(Type ty) => ty.GetInterfaces().Any(iface => iface == typeof(DataContext.ICollection)); private static void ValidateColType(Type ty) { var isGeneric = ty.IsGenericType; var colDef = GetColBase(ty); var errors = new List(); if (isGeneric) errors.Add("Cannot be generic."); if (colDef is null) errors.Add("Must inherit Collection<>."); if (errors.Any()) { throw new InvalidOperationException( $"Invalid collection type: {string.Join(" ", errors)}"); } } private static bool IsColProp(PropertyInfo prop) { var getter = prop.GetGetMethod(); if (getter is null) return false; if (getter.IsStatic) return false; var retType = getter.ReturnType; if (!IsColType(retType)) return false; return true; } private static void ValidateColProp(List colTypes, PropertyInfo prop) { var nullCtx = new NullabilityInfoContext(); var nullInfo = nullCtx.Create(prop); var getter = prop.GetGetMethod()!; var retType = getter.ReturnType; var returnsValidColType = colTypes.Contains(retType); var isNullable = nullInfo.ReadState is NullabilityState.Nullable; var errors = new List(); if (!returnsValidColType) errors.Add("Must return a nested collection type."); if (isNullable) errors.Add("Cannot be nullable."); if (errors.Any()) { throw new InvalidOperationException( $"Invalid collection property: {string.Join(" ", errors)}"); } } // helpers private static Type? GetColBase(Type ty) => ty.GetGenInst(typeof(DataContext.Collection<>)); private static Type GetDocType(Type ty) { var col = GetColBase(ty); Debug.Assert(col is not null); return col.GetGenericArguments()[0]; } } ``` ## File: Fauna/Linq/Deserializers.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Serialization; namespace Fauna.Linq; internal class MappedDeserializer : BaseSerializer { private ISerializer _inner; private Func _mapper; public MappedDeserializer(ISerializer inner, Func mapper) { _inner = inner; _mapper = mapper; } public override List GetSupportedTypes() => _inner.GetSupportedTypes(); public override O Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => _mapper(_inner.Deserialize(ctx, ref reader)); public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { throw new NotImplementedException(); } } internal class ProjectionDeserializer : BaseSerializer { private ISerializer[] _fields; public ProjectionDeserializer(IEnumerable fields) { _fields = fields.ToArray(); } public override List GetSupportedTypes() => _fields.SelectMany(x => x.GetSupportedTypes()).Distinct().ToList(); public override object?[] Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) { if (reader.CurrentTokenType != TokenType.StartArray) throw UnexpectedToken(reader.CurrentTokenType); var values = new object?[_fields.Length]; for (var i = 0; i < _fields.Length; i++) { if (!reader.Read()) throw new SerializationException("Unexpected end of stream"); if (reader.CurrentTokenType == TokenType.EndArray) throw UnexpectedToken(reader.CurrentTokenType); values[i] = _fields[i].Deserialize(ctx, ref reader); } if (!reader.Read()) throw new SerializationException("Unexpected end of stream"); if (reader.CurrentTokenType != TokenType.EndArray) throw UnexpectedToken(reader.CurrentTokenType); return values; } public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { throw new NotImplementedException(); } private new static SerializationException UnexpectedToken(TokenType tokenType) => new($"Unexpected token while deserializing LINQ element: {tokenType}"); } ``` ## File: Fauna/Linq/IntermediateQueryHelpers.cs ```csharp namespace Fauna.Linq; internal static class IntermediateQueryHelpers { public static QueryExpr Expr(string fql) => new(new List { new QueryLiteral(fql) }); public static QueryVal Const(object? v) => new(v); private static readonly Query _larr = Expr("["); private static readonly Query _rarr = Expr("]"); public static Query Array(Query inner) => _larr.Concat(inner).Concat(_rarr); public static Query Array(IEnumerable inners) => Join(inners, _larr, ",", _rarr); private static readonly Query _lparen = Expr("("); private static readonly Query _rparen = Expr(")"); public static Query Parens(Query inner) => _lparen.Concat(inner).Concat(_rparen); public static Query Parens(IEnumerable inners) => Join(inners, _lparen, ",", _rparen); private static readonly Query _lbrace = Expr("{"); private static readonly Query _rbrace = Expr("}"); public static Query Block(Query inner) => _lbrace.Concat(inner).Concat("}"); public static Query Block(IEnumerable inners) => Join(inners, _lbrace, ";", _rbrace); public static Query Obj(Query inner) => _lbrace.Concat(inner).Concat("}"); public static Query Obj(IEnumerable inners) => Join(inners, _lbrace, ",", _rbrace); public static Query Op(Query a, string op, Query b) => a.Concat(Expr(op)).Concat(b); public static Query FieldAccess(Query callee, string f) => callee.Concat($".{f}"); public static Query FnCall(string m) => Expr($"{m}()"); public static Query FnCall(string m, Query arg) => Expr($"{m}(").Concat(arg).Concat(_rparen); public static Query FnCall(string m, IEnumerable args) => Join(args, Expr($"{m}("), ",", _rparen); public static Query MethodCall(Query callee, string m) => callee.Concat($".{m}()"); public static Query MethodCall(Query callee, string m, Query a1) => callee.Concat($".{m}(").Concat(a1).Concat(_rparen); public static Query MethodCall(Query callee, string m, Query a1, Query a2) => callee.Concat($".{m}(").Concat(a1).Concat(",").Concat(a2).Concat(_rparen); public static Query MethodCall(Query callee, string m, IEnumerable args) => Join(args, callee.Concat($".{m}("), ",", _rparen); public static Query Join(IEnumerable ies, Query l, string sep, Query r) { Query ret = l; var init = true; foreach (var ie in ies) { if (init) init = false; else ret = ret.Concat(sep); ret = ret.Concat(ie); } ret = ret.Concat(r); return ret; } public static Query CollectionAll(DataContext.ICollection col) => MethodCall(Expr(col.Name), "all"); public static Query CollectionIndex(DataContext.IIndex idx) => MethodCall(Expr(idx.Collection.Name), idx.Name, idx.Args.Select(Const)); public static Query Function(string name, object[] args) => FnCall(name, args.Select(Const)); public static QueryExpr Concat(this Query q1, string str) { var frags = new List(); if (q1 is QueryExpr e1) { if (e1.Fragments.Last() is QueryLiteral l1) { frags.AddRange(e1.Fragments.SkipLast(1)); frags.Add(new QueryLiteral(l1.Unwrap + str)); } else { frags.AddRange(e1.Fragments); frags.Add(new QueryLiteral(str)); } } else { frags.Add(q1); frags.Add(new QueryLiteral(str)); } return new QueryExpr(frags); } public static QueryExpr Concat(this Query q1, Query q2) { var frags = new List(); if (q1 is QueryExpr e1) { if (q2 is QueryExpr e2) { if (e1.Fragments.Last() is QueryLiteral l1 && e2.Fragments.First() is QueryLiteral l2) { frags.AddRange(e1.Fragments.SkipLast(1)); frags.Add(new QueryLiteral(l1.Unwrap + l2.Unwrap)); frags.AddRange(e2.Fragments.Skip(1)); } else { frags.AddRange(e1.Fragments); frags.AddRange(e2.Fragments); } } else { frags.AddRange(e1.Fragments); frags.Add(q2); } } else { if (q2 is QueryExpr e2) { frags.Add(q1); frags.AddRange(e2.Fragments); } else { frags.Add(q1); frags.Add(q2); } } return new QueryExpr(frags); } } ``` ## File: Fauna/Linq/IQuerySource.cs ```csharp using System.Linq.Expressions; using Fauna.Core; using Fauna.Types; namespace Fauna.Linq; /// /// An interface for common static IQuerySource methods that are non-generic. /// public interface IQuerySource { // TODO(matt) use an API-specific exception in-line with what other LINQ // libraries do. internal static Exception Fail(Expression? expr) => Fail($"Unsupported {expr?.NodeType} expression: {expr}"); internal static Exception Fail(string op, string msg) => Fail($"Unsupported method call `{op}`: {msg}"); internal static Exception Fail(string msg) => new NotSupportedException(msg); } /// /// An interface defining the LINQ API for Fauna queries. /// /// The type returned by the query. public interface IQuerySource : IQuerySource { // Core execution /// /// Executes a paginating query asynchronously and returns an enumerable of pages. /// /// An instance of . /// A cancellation token. /// An IAsyncEnumerable of type . public IAsyncEnumerable> PaginateAsync(QueryOptions? queryOptions = null, CancellationToken cancel = default); /// /// Executes a query asynchronously. /// /// A cancellation token. /// An . public IAsyncEnumerable ToAsyncEnumerable(CancellationToken cancel = default); /// /// Executes a query. /// /// An . public IEnumerable ToEnumerable(); // Composition methods /// /// Obtains a distinct set of results. This is evaluated server-side. /// /// This instance. public IQuerySource Distinct(); /// /// Applies default ordering to the query. This is evaluated server-side. /// /// This instance. public IQuerySource Order(); /// /// Orders according to the selector. This is evaluated server-side. /// /// This instance. public IQuerySource OrderBy(Expression> keySelector); /// /// Orders by descending. This is evaluated server-side. /// /// This instance. public IQuerySource OrderDescending(); /// /// Orders by descending according to the selector. This is evaluated server-side. /// /// This instance. public IQuerySource OrderByDescending(Expression> keySelector); /// /// Reverses the order of the results. This is evaluated server-side. /// /// This instance. public IQuerySource Reverse(); /// /// Applies a projection to the query. This is evaluated server-side. /// /// This instance. public IQuerySource Select(Expression> selector); // public IQuerySource SelectMany(Expression>> selector); /// /// Skips the first N elements of the results. This is evaluated server-side. /// /// This instance. public IQuerySource Skip(int count); // public IQuerySource SelectMany(Expression>> selector); /// /// Takes the first N elements of the results. This is evaluated server-side. /// /// This instance. public IQuerySource Take(int count); // public IQuerySource SelectMany(Expression>> selector); /// /// Applies the predicate to the query. This is evaluated server-side. /// /// This instance. public IQuerySource Where(Expression> predicate); // Terminal result methods // public R Aggregate(A seed, Expression> accum, Func selector); // public Task AggregateAsync(A seed, Expression> accum, Func selector); // // not IQueryable // public R Fold(R seed, Expression> accum); // public Task FoldAsync(R seed, Expression> accum); // public IQuerySource SelectMany(Expression>> selector); /// /// Applies each predicate and executes the query. This is evaluated server-side. /// /// True if every predicate evaluates to true. Otherwise, false. public bool All(Expression> predicate); /// /// Applies each predicate and executes the query asynchronously. This is evaluated server-side. /// /// True if every predicate evaluate to true, otherwise false. public Task AllAsync(Expression> predicate, CancellationToken cancel = default); /// /// Determines if the result is not empty. This is evaluated server-side. /// /// True if the result contains any elements, otherwise false. public bool Any(); /// /// Determines if the result is not empty asynchronously. This is evaluated server-side. /// /// True if the result contains any elements, otherwise false. public Task AnyAsync(CancellationToken cancel = default); /// /// Applies each predicate and executes the query. This is evaluated server-side. /// /// True if any predicate evaluates to true, otherwise false. public bool Any(Expression> predicate); /// /// Applies each predicate and executes the query asynchronously. This is evaluated server-side. /// /// True if any predicate evaluates to true, otherwise false. public Task AnyAsync(Expression> predicate, CancellationToken cancel = default); /// /// Applies a count of the elements and executes the query. This is evaluated server-side. /// /// The number of elements in the result. public int Count(); /// /// Applies a count of the elements and executes the query asynchronously. This is evaluated server-side. /// /// The number of elements in the result. public Task CountAsync(CancellationToken cancel = default); /// /// Applies the predicate, applies a count, and executes the query. This is evaluated server-side. /// /// The number of elements in the result. public int Count(Expression> predicate); /// /// Applies the predicate, applies a count, and executes the query asynchronously. This is evaluated server-side. /// /// The number of elements in the result. public Task CountAsync(Expression> predicate, CancellationToken cancel = default); /// /// Executes the query and obtains the first element in the result. This is evaluated server-side. /// /// The first element in the result. public T First(); /// /// Executes the query asynchronously and obtains the first element in the result. This is evaluated server-side. /// /// The first element in the result. public Task FirstAsync(CancellationToken cancel = default); /// /// Applies the predicate, executes the query, and obtains the first element in the result. This is evaluated server-side. /// /// The first element in the result. public T First(Expression> predicate); /// /// Applies the predicate, executes the query asynchronously, and obtains the first element in the result. /// This is evaluated server-side. /// /// The first element in the result. public Task FirstAsync(Expression> predicate, CancellationToken cancel = default); /// /// Executes the query and obtains the first element in the result /// or the default value, if there is no result. This is evaluated server-side. /// /// The first element in the result or the default value, if there is no result. public T? FirstOrDefault(); /// /// Executes the query asynchronously and obtains the first element in the result /// or the default value, if there is no result. This is evaluated server-side. /// /// The first element in the result. public Task FirstOrDefaultAsync(CancellationToken cancel = default); /// /// Applies the predicate, executes the query, and obtains the first element in the result or the /// default value, if there is no result. This is evaluated server-side. /// /// The first element in the result. public T? FirstOrDefault(Expression> predicate); /// /// Applies the predicate, executes the query asynchronously, and obtains the first element in the result or the /// default value, if there is no result. This is evaluated server-side. /// /// The first element in the result. public Task FirstOrDefaultAsync(Expression> predicate, CancellationToken cancel = default); /// /// Executes the query and obtains the last element in the result. This is evaluated server-side. /// /// The last element in the result. public T Last(); /// /// Executes the query asynchronously and obtains the last element in the result. This is evaluated server-side. /// /// The last element in the result. public Task LastAsync(CancellationToken cancel = default); /// /// Applies the predicate, executes the query, and obtains the last element in the result. /// This is evaluated server-side. /// /// The last element in the result. public T Last(Expression> predicate); /// /// Applies the predicate, executes the query asynchronously, and obtains the last element in the result. /// This is evaluated server-side. /// /// The last element in the result. public Task LastAsync(Expression> predicate, CancellationToken cancel = default); /// /// Executes the query and obtains the last element in the result or the default value, if there is no result. /// This is evaluated server-side. /// /// The last element in the result or the default value, if there is no result. public T? LastOrDefault(); /// /// Executes the query asynchronously and obtains the last element in the result or the default value, /// if there is no result. This is evaluated server-side. /// /// The last element in the result or the default value, if there is no result. public Task LastOrDefaultAsync(CancellationToken cancel = default); /// /// Applies the predicate, executes the query and obtains the last element in the result /// or the default value, if there is no result. This is evaluated server-side. /// /// The last element in the result or the default value, if there is no result. public T? LastOrDefault(Expression> predicate); /// /// Applies the predicate, executes the query asynchronously and obtains the last element in the result /// or the default value, if there is no result. This is evaluated server-side. /// /// The last element in the result or the default value, if there is no result. public Task LastOrDefaultAsync(Expression> predicate, CancellationToken cancel = default); /// /// Applies a count of the elements and executes the query. This is evaluated server-side. /// /// The number of elements in the result. public long LongCount(); /// /// Applies a count of the elements and executes the query asynchronously. This is evaluated server-side. /// /// The number of elements in the result. public Task LongCountAsync(CancellationToken cancel = default); /// /// Applies the predicate, applies a count, and executes the query. This is evaluated server-side. /// /// The number of elements in the result. public long LongCount(Expression> predicate); /// /// Applies the predicate, applies a count, and executes the query asynchronously. This is evaluated server-side. /// /// The number of elements in the result. public Task LongCountAsync(Expression> predicate, CancellationToken cancel = default); /// /// Executes a query and returns the maximum value in the result set. This is evaluated server-side. ///

/// (a, b) => if (a >= b) a else b ///

/// The maximum . public T Max(); /// /// Executes a query asynchronously and returns the maximum value in the result set. This is evaluated server-side. ///

/// (a, b) => if (a >= b) a else b ///

/// The maximum . public Task MaxAsync(CancellationToken cancel = default); /// /// Applies a selector, executes a query, and returns the maximum value in the result set. This is evaluated server-side. ///

/// (a, b) => if (a >= b) a else b ///

/// The maximum . public R Max(Expression> selector); /// /// Applies a selector, executes a query asynchronously, and returns the maximum value in the result set. /// This is evaluated server-side. ///

/// (a, b) => if (a >= b) a else b ///

/// The maximum . public Task MaxAsync(Expression> selector, CancellationToken cancel = default); // public T MaxBy(Expression> selector); // public Task MaxByAsync(Expression> selector, CancellationToken cancel = default); /// /// Executes a query and returns the minimum value in the result set. This is evaluated server-side. ///

/// (a, b) => if (a <= b) a else b ///

/// The minimum . public T Min(); /// /// Executes a query asynchronously and returns the minimum value in the result set. This is evaluated server-side. ///

/// (a, b) => if (a <= b) a else b ///

/// The minimum . public Task MinAsync(CancellationToken cancel = default); // public T MinBy(Expression> selector); // public Task MinByAsync(Expression> selector, CancellationToken cancel = default); /// /// Applies the selector, executes the query, and returns the minimum value in the result set. /// This is evaluated server-side. ///

/// (a, b) => if (a <= b) a else b ///

/// The minimum . public R Min(Expression> selector); /// /// Applies the selector, executes the query asynchronously, and returns the minimum value in the result set. /// This is evaluated server-side. ///

/// (a, b) => if (a <= b) a else b ///

/// The minimum . public Task MinAsync(Expression> selector, CancellationToken cancel = default); /// /// Executes the query. If the result is a single element, returns it. Otherwise, throws an exception. /// This is evaluated server-side. /// /// The query result. public T Single(); /// /// Executes the query asynchronously. If the result is a single element, returns it. Otherwise, throws an exception. /// This is evaluated server-side. /// /// The query result. public Task SingleAsync(CancellationToken cancel = default); /// /// Applies the predicate and executes the query. If the result is a single element, returns it. Otherwise, throws an exception. /// This is evaluated server-side. /// /// The query result. public T Single(Expression> predicate); /// /// Applies the predicate and executes the query asynchronously. If the result is a single element, returns it. Otherwise, throws an exception. /// This is evaluated server-side. /// /// The query result. public Task SingleAsync(Expression> predicate, CancellationToken cancel = default); /// /// Executes the query. If the result is a single element, returns it. Otherwise, returns the default. /// This is evaluated server-side. /// /// The query result or default. public T SingleOrDefault(); /// /// Executes the query asynchronously. If the result is a single element, returns it. Otherwise, returns the default. /// This is evaluated server-side. /// /// The query result or default. public Task SingleOrDefaultAsync(CancellationToken cancel = default); /// /// Applies the predicate and executes the query. If the result is a single element, returns it. Otherwise, returns the default. /// This is evaluated server-side. /// /// The query result or default. public T SingleOrDefault(Expression> predicate); /// /// Applies the predicate and executes the query asynchronously. If the result is a single element, returns it. Otherwise, returns the default. /// This is evaluated server-side. /// /// The query result or default. public Task SingleOrDefaultAsync(Expression> predicate, CancellationToken cancel = default); /// /// Calculates the sum of values returned by a provided selector. /// This is evaluated server-side. /// /// The sum of selected values. public int Sum(Expression> selector); /// /// Asynchronously calculates the sum of values returned by a provided selector. /// This is evaluated server-side. /// /// The sum of selected values. public Task SumAsync(Expression> selector, CancellationToken cancel = default); /// /// Calculates the sum of values returned by a provided selector. /// This is evaluated server-side. /// /// The sum of selected values. public long Sum(Expression> selector); /// /// Asynchronously calculates the sum of values returned by a provided selector. /// This is evaluated server-side. /// /// The sum of selected values. public Task SumAsync(Expression> selector, CancellationToken cancel = default); // public float Sum(Expression> selector); // public Task SumAsync(Expression> selector, CancellationToken cancel = default); /// /// Calculates the sum of values returned by a provided selector. /// This is evaluated server-side. /// /// The sum of selected values. public double Sum(Expression> selector); /// /// Asynchronously calculates the sum of values returned by a provided selector. /// This is evaluated server-side. /// /// The sum of selected values. public Task SumAsync(Expression> selector, CancellationToken cancel = default); /// /// Calculates the mean average of values returned by a provided selector. /// This is evaluated server-side. /// /// The mean average of selected values. public double Average(Expression> selector); /// /// Asynchronously calculates the mean average of values returned by a provided selector. /// This is evaluated server-side. /// /// The mean average of selected values. public Task AverageAsync(Expression> selector, CancellationToken cancel = default); // Collection result methods /// /// Executes the query and converts the results to a . /// /// A . public List ToList(); /// /// Executes the query asynchronously and converts the results to a . /// /// A . public Task> ToListAsync(CancellationToken cancel = default); /// /// Executes the query and converts the results to a . /// /// A . public T[] ToArray(); /// /// Executes the query asynchronously and converts the results to a . /// /// A . public Task ToArrayAsync(CancellationToken cancel = default); /// /// Executes the query and converts the results to a . /// /// A . public HashSet ToHashSet(); /// /// Executes the query asynchronously and converts the results to a . /// /// A . public Task> ToHashSetAsync(CancellationToken cancel = default); /// /// Executes the query and converts the results to a . /// /// A comparer to use. /// A . public HashSet ToHashSet(IEqualityComparer? comparer); /// /// Executes the query asynchronously and converts the results to a . /// /// A comparer to use. /// A cancellation token. /// A . public Task> ToHashSetAsync(IEqualityComparer? comparer, CancellationToken cancel = default); /// /// Executes the query synchronously and converts the results to a . /// /// A function used to obtain a key. /// A function used to obtain a value. /// The key type of the dictionary. /// The value type of the dictionary. /// The query result as a . public Dictionary ToDictionary(Func getKey, Func getValue) where K : notnull; /// /// Executes the query asynchronously and converts the results to a . /// /// A function used to obtain a key. /// A function used to obtain a value. /// A cancellation token. /// The key type of the dictionary. /// The value type of the dictionary. /// The query result as an awaitable of . public Task> ToDictionaryAsync(Func getKey, Func getValue, CancellationToken cancel = default) where K : notnull; /// /// Executes the query synchronously and converts the results to a . /// /// A function used to obtain a key. /// A function used to obtain a value. /// A comparer used to compare keys. /// The key type of the dictionary. /// The value type of the dictionary. /// The query result as a . public Dictionary ToDictionary(Func getKey, Func getValue, IEqualityComparer? comparer) where K : notnull; /// /// Executes the query asynchronously and converts the results to a . /// /// A function used to obtain a key. /// A function used to obtain a value. /// A comparer used to compare keys. /// A cancellation token. /// The key type of the dictionary. /// The value type of the dictionary. /// The query result as an awaitable of . public Task> ToDictionaryAsync(Func getKey, Func getValue, IEqualityComparer? comparer, CancellationToken cancel = default) where K : notnull; } ``` ## File: Fauna/Linq/LookupTable.cs ```csharp using System.Linq.Expressions; using System.Reflection; using Fauna.Mapping; using Fauna.Serialization; namespace Fauna.Linq; // TODO(matt) reconcile/merge this behavior with MappingCtx internal record struct LookupTable(MappingContext Ctx) { public record class Result(string Name, ISerializer Serializer, Type Type); private static Result R(string name, ISerializer ser, Type ty) => new Result(name, ser, ty); public Result? FieldLookup(PropertyInfo prop, Expression callee) { if (Ctx.TryGetBaseType(callee.Type, out var info)) { var field = info.Fields.FirstOrDefault(f => f.Property == prop); return field is null ? null : R(field.Name, field.Serializer, field.Type); } return Table(prop, callee); } public Result? MethodLookup(MethodInfo method, Expression callee) => Table(method, callee); public bool HasField(PropertyInfo prop, Expression callee) => FieldLookup(prop, callee) is not null; public bool HasMethod(MethodInfo method, Expression callee) => MethodLookup(method, callee) is not null; // built-ins private Result? Table(MemberInfo member, Expression callee) => callee.Type.Name switch { "string" => StringTable(member, callee), _ => null, }; private Result? StringTable(MemberInfo member, Expression callee) => member.Name switch { "Length" => R("length", Serializer.Generate(Ctx), typeof(int)), "EndsWith" => R("endsWith", Serializer.Generate(Ctx), typeof(int)), "StartsWith" => R("startsWith", Serializer.Generate(Ctx), typeof(int)), _ => null, }; } ``` ## File: Fauna/Linq/Pipeline.cs ```csharp using System.Linq.Expressions; using Fauna.Serialization; namespace Fauna.Linq; /// /// The mode of the query pipeline. /// public enum PipelineMode { /// /// When a "pure" query, no local processing is required except deserialization. /// Query, /// /// When elements have local projection. /// Project, /// /// When post-processing on the loaded set is required. /// SetLoad, /// /// When there is a final, non-enum result, no more transformations are allowed. /// Scalar, } internal readonly record struct Pipeline( PipelineMode Mode, Query Query, Type ElemType, bool ElemNullable, ISerializer? ElemSerializer, LambdaExpression? ProjectExpr) { public IPipelineExecutor GetExec(DataContext ctx) { var ser = ElemSerializer ?? (ElemNullable ? Serializer.GenerateNullable(ctx.MappingCtx, ElemType) : Serializer.Generate(ctx.MappingCtx, ElemType)); var proj = ProjectExpr?.Compile(); return IPipelineExecutor.Create(ctx, Query, ser, proj, Mode); } } ``` ## File: Fauna/Linq/PipelineExecutor.cs ```csharp using System.Diagnostics; using System.Reflection; using System.Runtime.CompilerServices; using Fauna.Core; using Fauna.Serialization; using Fauna.Types; using Fauna.Util.Extensions; namespace Fauna.Linq; internal interface IPipelineExecutor { private static readonly MethodInfo _createEnumExec = typeof(IPipelineExecutor).GetMethod(nameof(CreateEnumExec), BindingFlags.Public | BindingFlags.Static)!; private static readonly MethodInfo _createScalarExec = typeof(IPipelineExecutor).GetMethod(nameof(CreateScalarExec), BindingFlags.Public | BindingFlags.Static)!; Type ElemType { get; } Type ResType { get; } IAsyncEnumerable> PagedResult(QueryOptions? queryOptions, CancellationToken cancel = default); Task Result(QueryOptions? queryOptions, CancellationToken cancel = default); IAsyncEnumerable> PagedResult(QueryOptions? queryOptions, CancellationToken cancel = default); Task Result(QueryOptions? queryOptions, CancellationToken cancel = default); public static IPipelineExecutor Create( DataContext ctx, Query query, ISerializer ser, Delegate? proj, PipelineMode mode) { Debug.Assert(mode != PipelineMode.SetLoad); var innerTy = ser.GetType() .GetGenInst(typeof(ISerializer<>))! .GetGenericArguments()[0]; var elemTy = proj is null ? innerTy : proj.GetType().GetGenInst(typeof(Func<,>))! .GetGenericArguments()[1]; var method = mode switch { PipelineMode.Query or PipelineMode.Project => _createEnumExec, PipelineMode.Scalar => _createScalarExec, _ => throw new Exception("unreachable"), }; var typeArgs = new Type[] { innerTy, elemTy }; var args = new object?[] { ctx, query, ser, proj }; var exec = method.MakeGenericMethod(typeArgs).Invoke(null, args); return (IPipelineExecutor)exec!; } public static EnumExecutor CreateEnumExec( DataContext ctx, Query query, ISerializer ser, Func? proj) => new EnumExecutor(ctx, query, new PageSerializer(MapSer(ser, proj))); public static ScalarExecutor CreateScalarExec( DataContext ctx, Query query, ISerializer ser, Func? proj) => new ScalarExecutor(ctx, query, MapSer(ser, proj)); private static ISerializer MapSer(ISerializer inner, Func? proj) { if (proj is not null) { return new MappedDeserializer(inner, proj); } Debug.Assert(typeof(I) == typeof(E)); return (ISerializer)inner; } public readonly record struct EnumExecutor( DataContext Ctx, Query Query, PageSerializer Ser) : IPipelineExecutor { public Type ElemType { get => typeof(E); } public Type ResType { get => typeof(IEnumerable); } public IAsyncEnumerable> PagedResult(QueryOptions? queryOptions, CancellationToken cancel = default) { var pages = Ctx.PaginateAsyncInternal(Query, Ser, queryOptions, cancel); if (pages is IAsyncEnumerable> ret) { return ret; } Debug.Assert(typeof(T) == ElemType); throw new Exception("unreachable"); } public async Task Result(QueryOptions? queryOptions, CancellationToken cancel = default) { var pages = PagedResult(queryOptions, cancel); var elems = new List(); if (elems is T res) { await foreach (var page in pages) { cancel.ThrowIfCancellationRequested(); elems.AddRange(page.Data); } return res; } Debug.Assert(typeof(T) == ResType, $"{typeof(T)} is not {ResType}"); throw new Exception("unreachable"); } public async IAsyncEnumerable> PagedResult(QueryOptions? queryOptions, [EnumeratorCancellation] CancellationToken cancel = default) { await foreach (var page in PagedResult(queryOptions, cancel)) { var data = page.Data.Select(e => (object?)e).ToList(); yield return new Page(data, page.After); } } public async Task Result(QueryOptions? queryOptions, CancellationToken cancel = default) => await Result>(queryOptions, cancel); } public readonly record struct ScalarExecutor( DataContext Ctx, Query Query, ISerializer Ser) : IPipelineExecutor { public Type ElemType { get => typeof(E); } public Type ResType { get => typeof(E); } public async Task Result(QueryOptions? queryOptions, CancellationToken cancel = default) { var qres = await Ctx.QueryAsync(Query, Ser, queryOptions, cancel); if (qres.Data is T ret) { return ret; } if (qres.Data is null) { return default(T)!; } Debug.Assert(typeof(T) == ResType, $"{typeof(T)} is not {ResType}"); throw new Exception("unreachable"); } public async IAsyncEnumerable> PagedResult(QueryOptions? queryOptions, [EnumeratorCancellation] CancellationToken cancel = default) { if (await Result(queryOptions, cancel) is T ret) { yield return new Page(new List { ret }, null); } Debug.Assert(typeof(T) == ElemType); throw new Exception("unreachable"); } public async Task Result(QueryOptions? queryOptions, CancellationToken cancel = default) => await Result(queryOptions, cancel); public async IAsyncEnumerable> PagedResult(QueryOptions? queryOptions, [EnumeratorCancellation] CancellationToken cancel = default) { yield return new Page(new List { await Result(queryOptions, cancel) }, null); } } } ``` ## File: Fauna/Linq/ProjectionVisitors.cs ```csharp using System.Diagnostics; using System.Linq.Expressions; using System.Reflection; using Fauna.Mapping; namespace Fauna.Linq; internal class ProjectionAnalysisVisitor : ExpressionVisitor { private readonly LookupTable _l; private readonly ParameterExpression _param; public HashSet Accesses { get; } = new(); public bool Escapes { get; private set; } = false; public ProjectionAnalysisVisitor(MappingContext ctx, ParameterExpression param) { _l = new LookupTable(ctx); _param = param; } protected override Expression VisitMember(MemberExpression node) { // FIXME handle chaining if (node.Expression == _param && node.Member is PropertyInfo prop && _l.HasField(prop, node.Expression)) { Accesses.Add(prop); return node; } return base.VisitMember(node); } protected override Expression VisitMethodCall(MethodCallExpression node) { // FIXME(matt) handle these by checking arg FQL purity return base.VisitMethodCall(node); } protected override Expression VisitParameter(ParameterExpression node) { if (node == _param) { Escapes = true; return node; } return base.VisitParameter(node); } } internal class ProjectionRewriteVisitor : ExpressionVisitor { private readonly ParameterExpression _param; private readonly PropertyInfo[] _props; private readonly Expression[] _fieldAccesses; public ProjectionRewriteVisitor( ParameterExpression doc, PropertyInfo[] props, ParameterExpression projected) { var accesses = new Expression[props.Length]; for (var i = 0; i < props.Length; i++) { accesses[i] = Expression.Convert( Expression.ArrayIndex(projected, Expression.Constant(i)), props[i].PropertyType); } _param = doc; _props = props; _fieldAccesses = accesses; } protected override Expression VisitMember(MemberExpression node) { if (node.Expression == _param) { var prop = node.Member as PropertyInfo; var idx = -1; Debug.Assert(prop is not null); for (var i = 0; idx < 0 && i < _props.Length; i++) { if (_props[i] == prop) { idx = i; } } Debug.Assert(idx >= 0); return _fieldAccesses[idx]; } return base.VisitMember(node); } } ``` ## File: Fauna/Linq/QuerySource.cs ```csharp using System.Diagnostics.CodeAnalysis; using Fauna.Core; using Fauna.Types; using Fauna.Util.Extensions; namespace Fauna.Linq; /// /// An abstract class representing a QuerySource for LINQ-style queries. /// public abstract class QuerySource : IQuerySource { [AllowNull] internal DataContext Ctx { get; private protected set; } [AllowNull] internal Pipeline Pipeline { get; private protected set; } internal void SetContext(DataContext ctx) { Ctx = ctx; } internal void SetQuery(Query query) { Pipeline = new Pipeline(PipelineMode.Query, query, typeof(TElem), false, null, null); } } public partial class QuerySource : QuerySource, IQuerySource { internal QuerySource(DataContext ctx, Pipeline pl) { Ctx = ctx; Pipeline = pl; } // Collection/Index DSLs are allowed to set _expr and _ctx in their own // constructors, so they use this base one. internal QuerySource() { } /// /// Executes a query with pagination. /// /// Query options. /// A cancellation token. /// An async enumerable for page results. public IAsyncEnumerable> PaginateAsync(QueryOptions? queryOptions = null, CancellationToken cancel = default) { var pe = Pipeline.GetExec(Ctx); return pe.PagedResult(queryOptions, cancel); } /// /// Executes a query asynchronously. /// /// A cancellation token. /// A enumerable of results. public IAsyncEnumerable ToAsyncEnumerable(CancellationToken cancel = default) => PaginateAsync(cancel: cancel).FlattenAsync(); /// /// Executes a query. /// /// A enumerable of results. public IEnumerable ToEnumerable() => new QuerySourceEnumerable(this); } ``` ## File: Fauna/Linq/QuerySourceDsl.cs ```csharp using System.Diagnostics; using System.Linq.Expressions; using System.Runtime.CompilerServices; using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Serialization; using Fauna.Util; using QH = Fauna.Linq.IntermediateQueryHelpers; namespace Fauna.Linq; public partial class QuerySource { private Query Query { get => Pipeline.Query; } private MappingContext MappingCtx { get => Ctx.MappingCtx; } private LookupTable Lookup { get => Ctx.LookupTable; } // Composition methods /// public IQuerySource Distinct() { RequireQueryMode(); return Chain(q: QH.MethodCall(Query, "distinct")); } /// public IQuerySource Order() { RequireQueryMode(); return Chain(q: QH.MethodCall(Query, "order")); } /// public IQuerySource OrderBy(Expression> keySelector) { RequireQueryMode(); return Chain(q: QH.MethodCall(Query, "order", SubQuery(keySelector))); } /// public IQuerySource OrderByDescending(Expression> keySelector) { RequireQueryMode(); return Chain(q: QH.MethodCall(Query, "order", QH.FnCall("desc", SubQuery(keySelector)))); } /// public IQuerySource OrderDescending() { RequireQueryMode(); return Chain(q: QH.MethodCall(Query, "order", QH.Expr("desc(x => x)"))); } /// public IQuerySource Reverse() => Chain(q: QH.MethodCall(Query, "reverse")); /// public IQuerySource Select(Expression> selector) { var pl = SelectCall(Query, selector); return new QuerySource(Ctx, pl); } /// public IQuerySource Skip(int count) => Chain(q: QH.MethodCall(Query, "drop", QH.Const(count))); /// public IQuerySource Take(int count) => Chain(q: QH.MethodCall(Query, "take", QH.Const(count))); /// public IQuerySource Where(Expression> predicate) => Chain(q: WhereCall(Query, predicate)); // Terminal result methods /// public bool All(Expression> predicate) => Execute(AllImpl(predicate)); /// public Task AllAsync(Expression> predicate, CancellationToken cancel = default) => ExecuteAsync(AllImpl(predicate), cancel); private Pipeline AllImpl(Expression> predicate) { RequireQueryMode("All"); return CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(Query, "every", SubQuery(predicate)), ety: typeof(bool)); } /// public bool Any() => Execute(AnyImpl(null)); /// public Task AnyAsync(CancellationToken cancel = default) => ExecuteAsync(AnyImpl(null), cancel); /// public bool Any(Expression> predicate) => Execute(AnyImpl(predicate)); /// public Task AnyAsync(Expression> predicate, CancellationToken cancel = default) => ExecuteAsync(AnyImpl(predicate), cancel); private Pipeline AnyImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(MaybeWhereCall(Query, predicate), "nonEmpty"), ety: typeof(bool)); /// public int Count() => Execute(CountImpl(null)); /// public Task CountAsync(CancellationToken cancel = default) => ExecuteAsync(CountImpl(null), cancel); /// public int Count(Expression> predicate) => Execute(CountImpl(predicate)); /// public Task CountAsync(Expression> predicate, CancellationToken cancel = default) => ExecuteAsync(CountImpl(predicate), cancel); private Pipeline CountImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(MaybeWhereCall(Query, predicate), "count"), ety: typeof(int)); /// public T First() => Execute(FirstImpl(null)); /// public Task FirstAsync(CancellationToken cancel = default) => ExecuteAsync(FirstImpl(null), cancel); /// public T First(Expression> predicate) => Execute(FirstImpl(predicate)); /// public Task FirstAsync(Expression> predicate, CancellationToken cancel = default) => ExecuteAsync(FirstImpl(predicate), cancel); private Pipeline FirstImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(AbortIfEmpty(MaybeWhereCall(Query, predicate)), "first")); /// public T? FirstOrDefault() => Execute(FirstOrDefaultImpl(null)); /// public Task FirstOrDefaultAsync(CancellationToken cancel = default) => ExecuteAsync(FirstOrDefaultImpl(null), cancel); /// public T? FirstOrDefault(Expression> predicate) => Execute(FirstOrDefaultImpl(predicate)); /// public Task FirstOrDefaultAsync(Expression> predicate, CancellationToken cancel = default) => ExecuteAsync(FirstOrDefaultImpl(predicate), cancel); private Pipeline FirstOrDefaultImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(MaybeWhereCall(Query, predicate), "first"), ety: typeof(T), enull: true); /// public T Last() => Execute(LastImpl(null)); /// public Task LastAsync(CancellationToken cancel = default) => ExecuteAsync(LastImpl(null), cancel); /// public T Last(Expression> predicate) => Execute(LastImpl(predicate)); /// public Task LastAsync(Expression> predicate, CancellationToken cancel = default) => ExecuteAsync(LastImpl(predicate), cancel); private Pipeline LastImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(AbortIfEmpty(MaybeWhereCall(Query, predicate)), "last")); /// public T? LastOrDefault() => Execute(LastOrDefaultImpl(null)); /// public Task LastOrDefaultAsync(CancellationToken cancel = default) => ExecuteAsync(LastOrDefaultImpl(null), cancel); /// public T? LastOrDefault(Expression> predicate) => Execute(LastOrDefaultImpl(predicate)); /// public Task LastOrDefaultAsync(Expression> predicate, CancellationToken cancel = default) => ExecuteAsync(LastOrDefaultImpl(predicate), cancel); private Pipeline LastOrDefaultImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(MaybeWhereCall(Query, predicate), "last"), ety: typeof(T), enull: true); /// public long LongCount() => Execute(LongCountImpl(null)); /// public Task LongCountAsync(CancellationToken cancel = default) => ExecuteAsync(LongCountImpl(null), cancel); /// public long LongCount(Expression> predicate) => Execute(LongCountImpl(predicate)); /// public Task LongCountAsync(Expression> predicate, CancellationToken cancel = default) => ExecuteAsync(LongCountImpl(predicate), cancel); private Pipeline LongCountImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(MaybeWhereCall(Query, predicate), "count"), ety: typeof(long)); private static readonly Query _maxReducer = QH.Expr("(a, b) => if (a >= b) a else b"); /// public T Max() => Execute(MaxImpl(null)); /// public Task MaxAsync(CancellationToken cancel = default) => ExecuteAsync(MaxImpl(null), cancel); /// public R Max(Expression> selector) => Execute(MaxImpl(selector)); /// public Task MaxAsync(Expression> selector, CancellationToken cancel = default) => ExecuteAsync(MaxImpl(selector), cancel); private Pipeline MaxImpl(Expression>? selector, [CallerMemberName] string callerName = "") { RequireQueryMode(callerName); return CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(MaybeMap(AbortIfEmpty(Query), selector), "reduce", _maxReducer), ety: typeof(R)); } private static readonly Query _minReducer = QH.Expr("(a, b) => if (a <= b) a else b"); /// public T Min() => Execute(MinImpl(null)); /// public Task MinAsync(CancellationToken cancel = default) => ExecuteAsync(MinImpl(null), cancel); /// public R Min(Expression> selector) => Execute(MinImpl(selector)); /// public Task MinAsync(Expression> selector, CancellationToken cancel = default) => ExecuteAsync(MinImpl(selector), cancel); private Pipeline MinImpl(Expression>? selector, [CallerMemberName] string callerName = "") { RequireQueryMode(callerName); return CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(MaybeMap(AbortIfEmpty(Query), selector), "reduce", _minReducer), ety: typeof(R)); } /// public double Average(Expression> selector) => Execute(AverageImpl(selector)); /// public Task AverageAsync(Expression> selector, CancellationToken cancel = default) => ExecuteAsync(AverageImpl(selector), cancel); private Pipeline AverageImpl(Expression> selector) { RequireQueryMode("Average"); return CopyPipeline( mode: PipelineMode.Scalar, q: QH.FnCall("Math.mean", QH.MethodCall(QH.MethodCall(AbortIfEmpty(Query), "map", SubQuery(selector)), "toArray")), ety: typeof(R)); } /// public T Single() => Execute(SingleImpl(null)); /// public Task SingleAsync(CancellationToken cancel = default) => ExecuteAsync(SingleImpl(null), cancel); /// public T Single(Expression> predicate) => Execute(SingleImpl(predicate)); /// public Task SingleAsync(Expression> predicate, CancellationToken cancel = default) => ExecuteAsync(SingleImpl(predicate), cancel); private Pipeline SingleImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(AbortIfEmpty(Singularize(MaybeWhereCall(Query, predicate))), "first")); /// public T SingleOrDefault() => Execute(SingleOrDefaultImpl(null)); /// public Task SingleOrDefaultAsync(CancellationToken cancel = default) => ExecuteAsync(SingleOrDefaultImpl(null), cancel); /// public T SingleOrDefault(Expression> predicate) => Execute(SingleOrDefaultImpl(predicate)); /// public Task SingleOrDefaultAsync(Expression> predicate, CancellationToken cancel = default) => ExecuteAsync(SingleOrDefaultImpl(predicate), cancel); private Pipeline SingleOrDefaultImpl(Expression>? predicate) => CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(Singularize(MaybeWhereCall(Query, predicate)), "first"), ety: typeof(T), enull: true); private static readonly Query _sumReducer = QH.Expr("(a, b) => a + b"); /// public int Sum(Expression> selector) => Execute(SumImpl(selector)); /// public Task SumAsync(Expression> selector, CancellationToken cancel = default) => ExecuteAsync(SumImpl(selector), cancel); /// public long Sum(Expression> selector) => Execute(SumImpl(selector)); /// public Task SumAsync(Expression> selector, CancellationToken cancel = default) => ExecuteAsync(SumImpl(selector), cancel); /// public double Sum(Expression> selector) => Execute(SumImpl(selector)); /// public Task SumAsync(Expression> selector, CancellationToken cancel = default) => ExecuteAsync(SumImpl(selector), cancel); private Pipeline SumImpl(Expression> selector) { RequireQueryMode("Sum"); var seed = (typeof(R) == typeof(int) || typeof(R) == typeof(long)) ? QH.Expr("0") : QH.Expr("0.0"); var mapped = QH.MethodCall(Query, "map", SubQuery(selector)); return CopyPipeline( mode: PipelineMode.Scalar, q: QH.MethodCall(mapped, "fold", seed, _sumReducer), ety: typeof(R)); } // helpers private void RequireQueryMode([CallerMemberName] string callerName = "") { if (Pipeline.Mode != PipelineMode.Query) { throw IQuerySource.Fail( callerName, $"Query is not pure: Earlier `Select` could not be translated to pure FQL."); } } private R Execute(Pipeline pl) { try { var res = ExecuteAsync(pl); res.Wait(); return res.Result; } catch (AggregateException ex) { throw TranslateException(ex.InnerExceptions.First()); } } private async Task ExecuteAsync(Pipeline pl, CancellationToken cancel = default) { try { return await pl.GetExec(Ctx).Result(queryOptions: null, cancel: cancel); } catch (AggregateException ex) { throw TranslateException(ex.InnerExceptions.First()); } } private QuerySource Chain( PipelineMode? mode = null, Query? q = null, ISerializer? ser = null, Type? ety = null, bool enull = false, LambdaExpression? proj = null) => new QuerySource(Ctx, CopyPipeline(mode, q, ser, ety, enull, proj)); private Pipeline CopyPipeline( PipelineMode? mode = null, Query? q = null, ISerializer? ser = null, Type? ety = null, bool enull = false, LambdaExpression? proj = null) { if (ser is not null) Debug.Assert(ety is not null); var mode0 = mode ?? Pipeline.Mode; var q0 = q ?? Pipeline.Query; // if ety is not null, reset ser and proj if not provided. var (ety0, enull0, ser0, proj0) = ety is not null ? (ety, enull, ser, proj) : (Pipeline.ElemType, Pipeline.ElemNullable, Pipeline.ElemSerializer, proj ?? Pipeline.ProjectExpr); return new Pipeline(mode0, q0, ety0, enull0, ser0, proj0); } // There is a bug in abort data deserialization if the abort // value is a string. Work around it by using an array. // FIXME(matt) remove workaround and use a string private Query AbortIfEmpty(Query setq) => QH.Expr(@"({ let s = (").Concat(setq).Concat(@") if (s.isEmpty()) abort(['empty']) s })"); private Query Singularize(Query setq) => QH.Expr(@"({ let s = (").Concat(setq).Concat(@") let s = if (s isa Set) s.toArray() else s if (s isa Array) { if (s.length > 1) abort(['not single']) s.take(1) } else { [s] } })"); private Exception TranslateException(Exception ex) => ex switch { AbortException aex => aex.GetData>()?.First() switch { "empty" => new InvalidOperationException("Empty set"), "not single" => new InvalidOperationException("Set contains more than one element"), _ => aex, }, _ => ex }; private Query MaybeWhereCall(Query callee, Expression? predicate, [CallerMemberName] string callerName = "") => predicate is null ? callee : WhereCall(callee, predicate, callerName); private Query MaybeMap(Query setq, Expression? selector) => selector is null ? setq : QH.MethodCall(setq, "map", SubQuery(selector)); private Query SubQuery(Expression expr) => new SubQuerySwitch(Ctx.LookupTable).Apply(expr); private Query WhereCall(Query callee, Expression predicate, [CallerMemberName] string callerName = "") { RequireQueryMode(callerName); return QH.MethodCall(callee, "where", SubQuery(predicate)); } private Pipeline SelectCall(Query callee, Expression proj, [CallerMemberName] string callerName = "") { var lambda = Expressions.UnwrapLambda(proj); Debug.Assert(lambda is not null, $"lambda is {proj.NodeType}"); Debug.Assert(lambda.Parameters.Count() == 1); // there is already a projection wired up, so tack on to its mapping lambda if (Pipeline.Mode == PipelineMode.Project) { Debug.Assert(Pipeline.ProjectExpr is not null); var prev = Pipeline.ProjectExpr; var pbody = Expression.Invoke(lambda, new Expression[] { prev.Body }); var plambda = Expression.Lambda(pbody, prev.Parameters); return CopyPipeline(proj: plambda); } Debug.Assert(Pipeline.Mode == PipelineMode.Query); var lparam = lambda.Parameters.First()!; var analysis = new ProjectionAnalysisVisitor(MappingCtx, lparam); analysis.Visit(lambda.Body); // select is a simple field access which we can translate directly to FQL. // TODO(matt) translate more cases to pure FQL if (lambda.Body is MemberExpression mexpr && mexpr.Expression == lparam) { Debug.Assert(!analysis.Escapes); var info = MappingCtx.GetInfo(lparam.Type); var access = analysis.Accesses.First(); var field = Lookup.FieldLookup(access, lparam); Debug.Assert(field is not null); return CopyPipeline( q: QH.MethodCall(callee, "map", QH.Expr($".{field.Name}")), ser: field.Serializer, ety: field.Type); } if (analysis.Escapes) { return CopyPipeline(mode: PipelineMode.Project, proj: lambda); } else { var accesses = analysis.Accesses.OrderBy(f => f.Name).ToArray(); var fields = accesses.Select(a => Lookup.FieldLookup(a, lparam)!); // projection query fragment var accs = fields.Select(f => QH.Expr($"x.{f.Name}")); var pquery = QH.Expr("x => ").Concat(QH.Array(accs)); // projected field deserializer var deser = new ProjectionDeserializer(fields.Select(f => f.Serializer)); var ety = typeof(object?[]); // build mapping lambda expression var pparam = Expression.Parameter(typeof(object?[]), "x"); var rewriter = new ProjectionRewriteVisitor(lparam, accesses, pparam); var pbody = rewriter.Visit(lambda.Body); var plambda = Expression.Lambda(pbody, pparam); return CopyPipeline( q: QH.MethodCall(callee, "map", pquery), mode: PipelineMode.Project, ser: deser, ety: ety, proj: plambda); } } } ``` ## File: Fauna/Linq/QuerySourceExtensions.cs ```csharp namespace Fauna.Linq; internal static class QuerySourceExtensions { public static Dictionary ToDictionary(this IQuerySource> src) where K : notnull => src.ToDictionary(t => t.Item1, t => t.Item2); public static Task> ToDictionaryAsync(this IQuerySource> src, CancellationToken cancel = default) where K : notnull => src.ToDictionaryAsync(t => t.Item1, t => t.Item2, cancel); public static Dictionary ToDictionary(this IQuerySource> src, IEqualityComparer? comparer) where K : notnull => src.ToDictionary(t => t.Item1, t => t.Item2, comparer); public static Task> ToDictionaryAsync(this IQuerySource> src, IEqualityComparer? comparer, CancellationToken cancel = default) where K : notnull => src.ToDictionaryAsync(t => t.Item1, t => t.Item2, comparer, cancel); public static int Sum(this IQuerySource src) => src.Sum(x => x); public static Task SumAsync(this IQuerySource src, CancellationToken cancel = default) => src.SumAsync(x => x, cancel); public static long Sum(this IQuerySource src) => src.Sum(x => x); public static Task SumAsync(this IQuerySource src, CancellationToken cancel = default) => src.SumAsync(x => x, cancel); public static double Sum(this IQuerySource src) => src.Sum(x => x); public static Task SumAsync(this IQuerySource src, CancellationToken cancel = default) => src.SumAsync(x => x, cancel); public static double Average(this IQuerySource src) => src.Average(x => x); public static Task AverageAsync(this IQuerySource src, CancellationToken cancel = default) => src.AverageAsync(x => x, cancel); } ``` ## File: Fauna/Linq/QuerySourceToCollections.cs ```csharp using System.Collections; namespace Fauna.Linq; /// /// Represents a Fauna query with a LINQ-style API. /// /// The type returned by the query when evaluated. public partial class QuerySource { /// /// Executes the query synchronously and converts the results to a . /// /// The query result as a . public List ToList() => ToEnumerable().ToList(); /// /// Executes the query asynchronously and converts the results to a . /// /// The query result as an awaitable of . public async Task> ToListAsync(CancellationToken cancel = default) { var ret = new List(); await foreach (var e in ToAsyncEnumerable(cancel)) ret.Add(e); return ret; } /// /// Executes the query synchronously and converts the results to a . /// /// The query result as . public T[] ToArray() => ToEnumerable().ToArray(); /// /// Executes the query asynchronously and converts the results to a . /// /// The query result as an awaitable of . public async Task ToArrayAsync(CancellationToken cancel = default) => (await ToListAsync(cancel)).ToArray(); /// /// Executes the query synchronously and converts the results to a . /// /// The query result as a . public HashSet ToHashSet() => ToHashSet(null); /// /// Executes the query asynchronously and converts the results to a . /// /// The query result as an awaitable of . public Task> ToHashSetAsync(CancellationToken cancel = default) => ToHashSetAsync(null, cancel); /// /// Executes the query synchronously and converts the results to a using a comparer. /// /// The comparer to use. /// The query result as a . public HashSet ToHashSet(IEqualityComparer? comparer) => ToEnumerable().ToHashSet(comparer); /// /// Executes the query asynchronously and converts the results to a . /// /// The comparer to use. /// A cancellation token. /// The query result as an awaitable of . public async Task> ToHashSetAsync(IEqualityComparer? comparer, CancellationToken cancel = default) { var ret = new HashSet(comparer); await foreach (var e in ToAsyncEnumerable(cancel)) ret.Add(e); return ret; } /// /// Executes the query synchronously and converts the results to a . /// /// A function used to obtain a key. /// A function used to obtain a value. /// The key type of the dictionary. /// The value type of the dictionary. /// The query result as a . public Dictionary ToDictionary(Func getKey, Func getValue) where K : notnull => ToDictionary(getKey, getValue, null); /// /// Executes the query asynchronously and converts the results to a . /// /// A function used to obtain a key. /// A function used to obtain a value. /// A cancellation token. /// The key type of the dictionary. /// The value type of the dictionary. /// The query result as an awaitable of . public Task> ToDictionaryAsync(Func getKey, Func getValue, CancellationToken cancel = default) where K : notnull => ToDictionaryAsync(getKey, getValue, null, cancel); /// /// Executes the query synchronously and converts the results to a . /// /// A function used to obtain a key. /// A function used to obtain a value. /// A comparer used to compare keys. /// The key type of the dictionary. /// The value type of the dictionary. /// The query result as a . public Dictionary ToDictionary(Func getKey, Func getValue, IEqualityComparer? comparer) where K : notnull => ToEnumerable().ToDictionary(getKey, getValue, comparer); /// /// Executes the query asynchronously and converts the results to a . /// /// A function used to obtain a key. /// A function used to obtain a value. /// A comparer used to compare keys. /// A cancellation token. /// The key type of the dictionary. /// The value type of the dictionary. /// The query result as an awaitable of . public async Task> ToDictionaryAsync(Func getKey, Func getValue, IEqualityComparer? comparer, CancellationToken cancel = default) where K : notnull { var ret = new Dictionary(comparer); await foreach (var e in ToAsyncEnumerable(cancel)) ret[getKey(e)] = getValue(e); return ret; } /// /// A struct to treat a as enumerable. /// /// public record struct QuerySourceEnumerable(QuerySource Source) : IEnumerable { /// /// Gets an enumerator for the wrapped . /// /// An enumerator. public IEnumerator GetEnumerator() { var pe = Source.PaginateAsync().GetAsyncEnumerator(); try { var mv = pe.MoveNextAsync().AsTask(); mv.Wait(); while (mv.Result) { var page = pe.Current; foreach (var e in page.Data) { yield return e; } mv = pe.MoveNextAsync().AsTask(); mv.Wait(); } } finally { pe.DisposeAsync(); } } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } } ``` ## File: Fauna/Linq/SubQuerySwitch.cs ```csharp using System.Linq.Expressions; using System.Reflection; using Fauna.Util; using Fauna.Util.Extensions; using QH = Fauna.Linq.IntermediateQueryHelpers; namespace Fauna.Linq; internal class SubQuerySwitch : DefaultExpressionSwitch { private readonly LookupTable _lookup; public SubQuerySwitch(LookupTable lookup) { _lookup = lookup; } protected override Query ApplyDefault(Expression? expr) => throw IQuerySource.Fail(expr); protected override Query ConstantExpr(ConstantExpression expr) { return expr.Value switch { DataContext.ICollection col => QH.CollectionAll(col), DataContext.IIndex idx => QH.CollectionIndex(idx), _ => QH.Const(expr.Value) }; } protected override Query LambdaExpr(LambdaExpression expr) { var ps = expr.Parameters; var pinner = string.Join(", ", ps.Select(p => p.Name)); var param = ps.Count() == 1 ? pinner : $"({pinner})"; var arrow = QH.Expr($"{param} =>"); return arrow.Concat(QH.Parens(Apply(expr.Body))); } protected override Query ParameterExpr(ParameterExpression expr) => QH.Expr(expr.Name!); protected override Query BinaryExpr(BinaryExpression expr) { var op = expr.NodeType switch { ExpressionType.Add => "+", ExpressionType.AddChecked => "+", ExpressionType.And => "&", // bitwise ExpressionType.AndAlso => "&&", // boolean // ExpressionType.ArrayIndex => , ExpressionType.Coalesce => "??", ExpressionType.Divide => "/", ExpressionType.Equal => "==", ExpressionType.ExclusiveOr => "^", ExpressionType.GreaterThan => ">", ExpressionType.GreaterThanOrEqual => ">=", ExpressionType.LeftShift => "<<", ExpressionType.LessThan => "<", ExpressionType.LessThanOrEqual => "<=", ExpressionType.Modulo => "%", ExpressionType.Multiply => "*", ExpressionType.MultiplyChecked => "*", ExpressionType.NotEqual => "!=", ExpressionType.Or => "|", // bitwise ExpressionType.OrElse => "||", // boolean ExpressionType.Power => "**", ExpressionType.RightShift => ">>", ExpressionType.Subtract => "-", ExpressionType.SubtractChecked => "-", _ => throw IQuerySource.Fail(expr) }; var lhs = Apply(expr.Left); var rhs = Apply(expr.Right); return QH.Parens(QH.Op(lhs, op, rhs)); } protected override Query CallExpr(MethodCallExpression expr) { var (callee, args, ext) = Expressions.GetCalleeAndArgs(expr); var name = _lookup.MethodLookup(expr.Method, callee)?.Name; if (name is null) throw IQuerySource.Fail(expr); return QH.MethodCall(Apply(callee), name, ApplyAll(args)); } protected override Query MemberAccessExpr(MemberExpression expr) { var callee = expr.Expression; if (callee is null) { var val = Expression.Lambda(expr).Compile().DynamicInvoke(); return QH.Const(val); } else if (callee.Type.IsClosureType()) { var val = Expression.Lambda(expr).Compile().DynamicInvoke(); return QH.Const(val); } switch (Apply(callee)) { case QueryVal v: var c = Expression.Constant(v.Unwrap); var access = Expression.PropertyOrField(c, expr.Member.Name); var val = Expression.Lambda(access).Compile().DynamicInvoke(); return QH.Const(val); case var q: var name = expr.Member is PropertyInfo prop ? _lookup.FieldLookup(prop, callee)?.Name : null; if (name is null) throw IQuerySource.Fail(expr); return QH.FieldAccess(q, name); } } } ``` ## File: Fauna/Mapping/Attributes.cs ```csharp namespace Fauna.Mapping; /// /// Attribute used to indicate that a class represents a Fauna document or struct. /// [AttributeUsage(AttributeTargets.Class)] [Obsolete("Object attribute is no longer used and will not influence serialization.")] public class ObjectAttribute : Attribute { } /// /// Attribute used to indicate that a field should be ignored during serialization and deserialization. /// [AttributeUsage(AttributeTargets.Property)] public class IgnoreAttribute : Attribute { /// /// Initializes an instance of an . /// public IgnoreAttribute() { } } /// /// An abstract type for attributing user-defined document classes. /// public abstract class BaseFieldAttribute : Attribute { /// /// The name of the field. /// public readonly string? Name; /// /// The type of the field. /// public readonly FieldType Type; internal BaseFieldAttribute(string? name, FieldType type) { Name = name; Type = type; } } /// /// Attribute used to specify fields on a Fauna document or struct. /// [AttributeUsage(AttributeTargets.Property)] public class FieldAttribute : BaseFieldAttribute { /// /// Initializes a of type Field with no name override. The name is inferred. /// public FieldAttribute() : base(null, FieldType.Field) { } /// /// Initializes a of type Field with a name. /// /// The name of the field as stored in Fauna. public FieldAttribute(string name) : base(name, FieldType.Field) { } } /// /// Attribute used to specify the id field on a Fauna document. The associated field will be ignored during /// serialization unless isClientGenerated is set to true. /// [AttributeUsage(AttributeTargets.Property)] public class IdAttribute : BaseFieldAttribute { private const string FieldName = "id"; /// /// Initializes a , indicating Fauna will generate the ID. /// public IdAttribute() : base(FieldName, FieldType.ServerGeneratedId) { } /// /// Initializes a . /// /// Whether the ID should be considered client-generated or not. public IdAttribute(bool isClientGenerated) : base(FieldName, isClientGenerated ? FieldType.ClientGeneratedId : FieldType.ServerGeneratedId) { } } /// /// Attribute used to specify the coll (Collection) field on a Fauna document. The associated field will be ignored /// during serialization. /// [AttributeUsage(AttributeTargets.Property)] public class CollectionAttribute : BaseFieldAttribute { private const string FieldName = "coll"; /// /// Initializes a . The Fauna field name will always be `coll` for this attribute. /// public CollectionAttribute() : base(FieldName, FieldType.Coll) { } } /// /// Attribute used to specify the ts field on a Fauna document. The associated field will be ignored during /// serialization. /// [AttributeUsage(AttributeTargets.Property)] public class TsAttribute : BaseFieldAttribute { private const string FieldName = "ts"; /// /// Initializes a . The Fauna field name will always be `ts` for this attribute. /// public TsAttribute() : base(FieldName, FieldType.Ts) { } } ``` ## File: Fauna/Mapping/FieldInfo.cs ```csharp using System.Reflection; using Fauna.Serialization; namespace Fauna.Mapping; /// /// A class that encapsulates the field mapping, serialization, and deserialization of a particular field in Fauna. /// public sealed class FieldInfo { /// /// The name of the field. /// public string Name { get; } /// /// The property info of an associated class. /// public PropertyInfo Property { get; } /// /// The for the particular property. /// public FieldType FieldType { get; } /// /// The that the field should deserialize into. /// public Type Type { get; } /// /// Whether the field is nullable. /// public bool IsNullable { get; } private MappingContext _ctx; private ISerializer? _serializer; internal FieldInfo(MappingContext ctx, BaseFieldAttribute attr, PropertyInfo prop) { var nullCtx = new NullabilityInfoContext(); var nullInfo = nullCtx.Create(prop); Name = attr.Name ?? FieldName.Canonical(prop.Name); FieldType = attr.Type; Property = prop; Type = prop.PropertyType; IsNullable = nullInfo.WriteState is NullabilityState.Nullable; _ctx = ctx; } internal ISerializer Serializer { get { lock (_ctx) { if (_serializer is null) { _serializer = Serialization.Serializer.Generate(_ctx, Type); if (IsNullable && (!_serializer.GetType().IsGenericType || (_serializer.GetType().IsGenericType && _serializer.GetType().GetGenericTypeDefinition() != typeof(NullableStructSerializer<>)))) { var serType = typeof(NullableSerializer<>).MakeGenericType(new[] { Type }); var ser = Activator.CreateInstance(serType, new[] { _serializer }); _serializer = (ISerializer)ser!; } } return _serializer; } } } } ``` ## File: Fauna/Mapping/FieldName.cs ```csharp namespace Fauna.Mapping; /// /// A class of utilities for field names. /// public static class FieldName { /// /// The canonical representation of a field name in Faun. C# properties are capitalized whereas Fauna fields are not by convention. /// /// The field name. /// The canonical name for the field in Fauna. public static string Canonical(string name) => (string.IsNullOrEmpty(name) || char.IsLower(name[0])) ? name : string.Concat(name[0].ToString().ToLower(), name.AsSpan(1)); } ``` ## File: Fauna/Mapping/FieldType.cs ```csharp namespace Fauna.Mapping; /// /// An enum for the field type, used with concrete implementations of . /// public enum FieldType { /// /// Indicates the document ID is client-generated. /// ClientGeneratedId, /// /// Indicates the document ID is Fauna-generated. /// ServerGeneratedId, /// /// Indicates the field is the coll (collection) field of the document. /// Coll, /// /// Indicates the field is the ts field of the document. /// Ts, /// /// Indicates the field contains user-defined data for the document. /// Field } ``` ## File: Fauna/Mapping/MappingContext.cs ```csharp using System.Diagnostics.CodeAnalysis; using Fauna.Linq; namespace Fauna.Mapping; /// /// A class representing the mapping context to be used during serialization and deserialization. /// public sealed class MappingContext { // FIXME(matt) possibly replace with more efficient cache impl private readonly Dictionary _cache = new(); private readonly Dictionary _collections = new(); private readonly Dictionary _baseTypes = new(); /// /// Initializes an empty . /// public MappingContext() { } /// /// Initializes a with associated collections. /// /// The collections to associate. public MappingContext(IEnumerable collections) { foreach (var col in collections) { var info = GetInfo(col.DocType, col.Name); _collections[col.Name] = info; _baseTypes[col.DocType] = info; } } /// /// Initializes a with associated collections. /// /// The collections to associate. public MappingContext(Dictionary collections) { foreach ((string name, Type ty) in collections) { _collections[name] = GetInfo(ty, name); } } /// /// Gets the for a given collection name. /// /// The collection name to get. /// When this method returns, contains the associated with the collection if found; otherwise, null. This parameter is passed uninitialized. /// true if the contains for the specified collection; otherwise, false. public bool TryGetCollection(string col, [NotNullWhen(true)] out MappingInfo? ret) { return _collections.TryGetValue(col, out ret); } /// /// Gets the for a given . /// /// The type to get. /// When this method returns, contains the associated with the type if found; otherwise, null. This parameter is passed uninitialized. /// true if the contains for the specified type; otherwise, false. public bool TryGetBaseType(Type ty, [NotNullWhen(true)] out MappingInfo? ret) { return _baseTypes.TryGetValue(ty, out ret); } /// /// Gets the for a given . /// /// The type to get. /// The associated collection name, if any /// public MappingInfo GetInfo(Type ty, string? colName = null) { lock (this) { if (_cache.TryGetValue(ty, out var ret)) { return ret; } } // MappingInfo caches itself during construction in order to make // itself available early for recursive lookup. return new MappingInfo(this, ty, colName); } internal void Add(Type ty, MappingInfo info) { lock (this) { _cache[ty] = info; } } } ``` ## File: Fauna/Mapping/MappingInfo.cs ```csharp using System.Collections.Immutable; using System.Reflection; using Fauna.Serialization; using Module = Fauna.Types.Module; namespace Fauna.Mapping; /// /// A class that encapsulates the class mapping, serialization, and deserialization of a Fauna object, including documents. /// public sealed class MappingInfo { /// /// The associated type. /// public Type Type { get; } /// /// A read-only list of representing the object. /// public IReadOnlyList Fields { get; } /// /// A read-only dictionary of representing the object. /// public IReadOnlyDictionary FieldsByName { get; } internal bool ShouldEscapeObject { get; } internal ISerializer ClassSerializer { get; } internal Module? Collection { get; } internal MappingInfo(MappingContext ctx, Type ty, string? colName = null) { ctx.Add(ty, this); Type = ty; Collection = colName != null ? new Module(colName) : null; var fields = new List(); var byName = new Dictionary(); foreach (var prop in ty.GetProperties()) { if (prop.GetCustomAttribute() != null) continue; var attr = prop.GetCustomAttribute() ?? new FieldAttribute(); var info = new FieldInfo(ctx, attr, prop); if (byName.ContainsKey(info.Name)) throw new ArgumentException($"Duplicate field name {info.Name} in {ty}"); fields.Add(info); byName[info.Name] = info; } ShouldEscapeObject = Serializer.Tags.Overlaps(byName.Values.Select(i => i.Name)); Fields = fields.ToImmutableList(); FieldsByName = byName.ToImmutableDictionary(); var serType = typeof(ClassSerializer<>).MakeGenericType(new[] { ty }); ClassSerializer = (ISerializer)Activator.CreateInstance(serType, this)!; } } ``` ## File: Fauna/Properties/AssemblyInfo.cs ```csharp using System.Runtime.CompilerServices; #if DEBUG [assembly: InternalsVisibleTo("Fauna.Test")] #endif ``` ## File: Fauna/Query/IQueryFragment.cs ```csharp using System.Text; using Fauna.Mapping; using Fauna.Serialization; namespace Fauna; /// /// Represents the base interface for a query fragment used for FQL query construction. /// public interface IQueryFragment { /// /// Serializes the query fragment into the provided stream. /// /// The context to be used during serialization. /// The writer to which the query fragment is serialized. void Serialize(MappingContext ctx, Utf8FaunaWriter writer); } /// /// Provides extension methods for the interface to enhance its functionality, /// allowing for more flexible serialization options. /// public static class QueryFragmentExtensions { /// /// Serializes the query fragment to a string using UTF-8 encoding. /// /// The query fragment to serialize. /// A to influence serialization. /// A string representation of the serialized query fragment. public static string Serialize(this IQueryFragment fragment, MappingContext ctx) { using var ms = new MemoryStream(); using var fw = new Utf8FaunaWriter(ms); fragment.Serialize(ctx, fw); fw.Flush(); return Encoding.UTF8.GetString(ms.ToArray()); } } ``` ## File: Fauna/Query/Query.cs ```csharp using Fauna.Mapping; using Fauna.Serialization; namespace Fauna; /// /// Represents the abstract base class for constructing FQL queries. /// public abstract class Query : IEquatable, IQueryFragment { /// /// Serializes the query into the provided stream. /// /// The context to be used during serialization. /// The writer to which the query is serialized. public abstract void Serialize(MappingContext ctx, Utf8FaunaWriter writer); /// /// Returns a hash code for the current query. /// /// A hash code for the current query. public abstract override int GetHashCode(); /// /// Determines whether the specified object is equal to the current query. /// /// The object to compare with the current query. /// true if the specified object is equal to the current query; otherwise, false. public abstract override bool Equals(object? otherObject); /// /// Determines whether the specified Query is equal to the current query. /// /// The Query to compare with the current query. /// true if the specified Query is equal to the current query; otherwise, false. public abstract bool Equals(Query? otherQuery); /// /// Constructs an FQL query using the specified QueryStringHandler. /// /// The QueryStringHandler that contains the query fragments. /// A Query instance constructed from the handler. public static Query FQL(ref QueryStringHandler handler) { return handler.Result(); } } ``` ## File: Fauna/Query/QueryArr.cs ```csharp using Fauna.Mapping; using Fauna.Serialization; namespace Fauna; /// /// Represents an array of FQL queries. /// internal sealed class QueryArr : Query, IQueryFragment { /// /// Gets the value of the specified type represented in the query. /// public IEnumerable Unwrap { get; } /// /// Initializes a new instance of the QueryArr class with the specified value. /// /// The value of the specified type to be represented in the query. public QueryArr(IEnumerable v) { Unwrap = v; } /// /// Serializes the query value. /// /// The serialization context. /// The writer to serialize the query value to. public override void Serialize(MappingContext ctx, Utf8FaunaWriter writer) { writer.WriteStartObject(); writer.WriteFieldName("array"); writer.WriteStartArray(); foreach (Query t in Unwrap) { var ser = Serializer.Generate(ctx, t.GetType()); ser.Serialize(ctx, writer, t); } writer.WriteEndArray(); writer.WriteEndObject(); } /// /// Determines whether the specified QueryArr is equal to the current QueryArr. /// /// The QueryArr to compare with the current QueryArr. /// true if the specified QueryArr is equal to the current QueryArr; otherwise, false. public override bool Equals(Query? o) { return o is QueryArr other && Unwrap.SequenceEqual(other.Unwrap); } /// /// Determines whether the specified object is equal to the current QueryArr. /// /// The object to compare with the current QueryArr. /// true if the specified object is equal to the current QueryArr; otherwise, false. public override bool Equals(object? otherObject) { return Equals(otherObject as QueryArr); } /// /// The default hash function. /// /// A hash code for the current QueryArr. public override int GetHashCode() { return Unwrap.GetHashCode(); } /// /// Returns a string that represents the current QueryArr. /// /// A string that represents the current QueryArr. public override string ToString() { return $"QueryArr({string.Join(", ", Unwrap)})"; } } ``` ## File: Fauna/Query/QueryExpr.cs ```csharp using System.Collections.ObjectModel; using Fauna.Mapping; using Fauna.Serialization; namespace Fauna; /// /// Represents an FQL query expression. This class encapsulates a list of IQueryFragment instances, allowing for complex query constructions. /// public sealed class QueryExpr : Query, IQueryFragment { /// /// Initializes a new instance of the QueryExpr class with a collection of query fragments. /// /// The collection of IQueryFragment instances. public QueryExpr(IList fragments) { Unwrap = new ReadOnlyCollection(fragments); } /// /// Initializes a new instance of the QueryExpr class with one or more query fragments. /// /// The array of IQueryFragment instances. public QueryExpr(params IQueryFragment[] fragments) : this(fragments.ToList()) { } /// /// Gets the readonly collection of query fragments. /// public IReadOnlyCollection Unwrap { get; } /// /// Gets the readonly collection of query fragments. /// public IReadOnlyCollection Fragments => Unwrap; /// /// Serializes the query expression. /// /// The serialization context. /// The writer to serialize the query expression to. public override void Serialize(MappingContext ctx, Utf8FaunaWriter writer) { writer.WriteStartObject(); writer.WriteFieldName("fql"); writer.WriteStartArray(); foreach (var t in Unwrap) { t.Serialize(ctx, writer); } writer.WriteEndArray(); writer.WriteEndObject(); } /// /// Determines whether the specified QueryExpr is equal to the current QueryExpr. /// /// The QueryExpr to compare with the current QueryExpr. /// true if the specified QueryExpr is equal to the current QueryExpr; otherwise, false. public override bool Equals(Query? o) => IsEqual(o as QueryExpr); /// /// Determines whether the specified object is equal to the current QueryExpr. /// /// The object to compare with the current QueryExpr. /// true if the specified object is equal to the current QueryExpr; otherwise, false. public override bool Equals(object? o) { if (ReferenceEquals(this, o)) { return true; } return o is QueryExpr expr && IsEqual(expr); } /// /// The default hash function. /// /// A hash code for the current QueryExpr. public override int GetHashCode() => Fragments.GetHashCode(); /// /// Returns a string that represents the current QueryExpr. /// /// A string that represents the current QueryExpr. public override string ToString() => $"QueryExpr({string.Join(",", Fragments)})"; private bool IsEqual(QueryExpr? o) { if (o is null) { return false; } if (Fragments == null || o.Fragments == null) { return Fragments == null && o.Fragments == null; } return Fragments.SequenceEqual(o.Fragments); } /// /// Determines whether two specified instances of QueryExpr are equal. /// /// The first QueryExpr to compare. /// The second QueryExpr to compare. /// true if left and right are equal; otherwise, false. public static bool operator ==(QueryExpr left, QueryExpr right) { if (ReferenceEquals(left, right)) { return true; } if (left is null || right is null) { return false; } return left.Equals(right); } /// /// Determines whether two specified instances of QueryExpr are not equal. /// /// The first QueryExpr to compare. /// The second QueryExpr to compare. /// true if left and right are not equal; otherwise, false. public static bool operator !=(QueryExpr left, QueryExpr right) { return !(left == right); } } ``` ## File: Fauna/Query/QueryLiteral.cs ```csharp using Fauna.Mapping; using Fauna.Serialization; namespace Fauna; /// /// Represents a literal part of an FQL query. This class is used for embedding raw string values directly into the query structure. /// public sealed class QueryLiteral : IQueryFragment { /// /// Initializes a new instance of the QueryLiteral class with the specified value. /// /// The string value to be represented as a query literal. /// Thrown when the value is null. public QueryLiteral(string v) { if (v == null) { throw new ArgumentNullException(nameof(v), "Value cannot be null."); } Unwrap = v; } /// /// Gets the string value of the query literal. /// public string Unwrap { get; } /// /// Returns a string that represents the current QueryLiteral. /// /// A string that represents the current QueryLiteral. public override string ToString() { return $"QueryLiteral({Unwrap})"; } /// /// Serializes the query literal. /// /// The serialization context. /// The writer to serialize the query literal to. public void Serialize(MappingContext ctx, Utf8FaunaWriter writer) { writer.WriteStringValue(Unwrap); } /// /// Determines whether the specified object is equal to the current QueryLiteral. /// /// The object to compare with the current QueryLiteral. /// true if the specified object is equal to the current QueryLiteral; otherwise, false. public override bool Equals(object? other) { var otherQuery = other as IQueryFragment; if (otherQuery is null) { return false; } if (ReferenceEquals(this, otherQuery)) { return true; } if (otherQuery is QueryLiteral otherLiteral) { return Unwrap == otherLiteral.Unwrap; } return false; } /// /// The default hash function. /// /// A hash code for the current QueryLiteral. public override int GetHashCode() { return Unwrap.GetHashCode(); } /// /// Determines whether two specified instances of QueryLiteral are equal. /// /// The first QueryLiteral to compare. /// The second QueryLiteral to compare. /// true if left and right are equal; otherwise, false. public static bool operator ==(QueryLiteral left, QueryLiteral right) { return Equals(left, right); } /// /// Determines whether two specified instances of QueryLiteral are not equal. /// /// The first QueryLiteral to compare. /// The second QueryLiteral to compare. /// true if left and right are not equal; otherwise, false. public static bool operator !=(QueryLiteral left, QueryLiteral right) { return !Equals(left, right); } } ``` ## File: Fauna/Query/QueryObj.cs ```csharp using Fauna.Mapping; using Fauna.Serialization; namespace Fauna; /// /// Represents a dictionary of FQL queries. /// public sealed class QueryObj : Query, IQueryFragment { /// /// Gets the value of the specified type represented in the query. /// public IDictionary Unwrap { get; } /// /// Initializes a new instance of the QueryObj class with the specified value. /// /// The value of the specified type to be represented in the query. public QueryObj(IDictionary v) { Unwrap = v; } /// /// Serializes the query value. /// /// The serialization context. /// The writer to serialize the query value to. public override void Serialize(MappingContext ctx, Utf8FaunaWriter writer) { var ser = Serializer.Generate(ctx, GetType()); ser.Serialize(ctx, writer, this); } /// /// Determines whether the specified QueryObj is equal to the current QueryObj. /// /// The QueryObj to compare with the current QueryObj. /// true if the specified QueryObj is equal to the current QueryObj; otherwise, false. public override bool Equals(Query? o) => IsEqual(o as QueryObj); /// /// Determines whether the specified object is equal to the current QueryObj. /// /// The object to compare with the current QueryObj. /// true if the specified object is equal to the current QueryObj; otherwise, false. public override bool Equals(object? o) { return ReferenceEquals(this, o) || IsEqual(o as QueryObj); } /// /// The default hash function. /// /// A hash code for the current QueryObj. public override int GetHashCode() { int hash = 31; hash *= Unwrap.GetHashCode(); return hash; } /// /// Returns a string that represents the current QueryObj. /// /// A string that represents the current QueryObj. public override string ToString() => $"QueryObj({Unwrap})"; /// /// Determines whether two specified instances of QueryObj are equal. /// /// The first QueryObj to compare. /// The second QueryObj to compare. /// true if left and right are equal; otherwise, false. public static bool operator ==(QueryObj left, QueryObj right) { return ReferenceEquals(left, right) || left.Equals(right); } /// /// Determines whether two specified instances of QueryObj are not equal. /// /// The first QueryObj to compare. /// The second QueryObj to compare. /// true if left and right are not equal; otherwise, false. public static bool operator !=(QueryObj left, QueryObj right) { return !(left == right); } private bool IsEqual(QueryObj? o) { return o is not null && Unwrap.Equals(o.Unwrap); } } ``` ## File: Fauna/Query/QueryStringHandler.cs ```csharp using System.Runtime.CompilerServices; namespace Fauna; /// /// Provides a mechanism to build FQL query expressions using interpolated strings. This structure collects fragments and literals to construct complex query expressions. /// [InterpolatedStringHandler] public ref struct QueryStringHandler { private readonly List _fragments; /// /// Initializes a new instance of the struct. /// /// The estimated length of the literals in the interpolated string. /// The number of format items in the interpolated string. public QueryStringHandler(int literalLength, int formattedCount) { _fragments = new List(); } /// /// Appends a literal string to the query. /// /// The literal string to append. public void AppendLiteral(string value) { _fragments.Add(new QueryLiteral(value)); } /// /// Appends a formatted value to the query. The value is wrapped as a or depending on its type. /// /// The value to append. public void AppendFormatted(object? value) { if (value is IQueryFragment frag) { _fragments.Add(frag); } else { _fragments.Add(new QueryVal(value)); } } /// /// Constructs and returns a instance representing the current state of the handler. /// /// A instance representing the constructed query fragments. public Query Result() { return new QueryExpr(_fragments); } } ``` ## File: Fauna/Query/QueryVal.cs ```csharp using Fauna.Mapping; using Fauna.Serialization; namespace Fauna; /// /// Represents a generic value holder for FQL queries. This class allows embedding values of various types into the query, with support for primitives, POCOs, and other types. /// public sealed class QueryVal : Query, IQueryFragment { /// /// Gets the value of the specified type represented in the query. /// public object? Unwrap { get; } /// /// Initializes a new instance of the QueryVal class with the specified value. /// /// The value of the specified type to be represented in the query. public QueryVal(object? v) { Unwrap = v; } /// /// Serializes the query value. /// /// The serialization context. /// The writer to serialize the query value to. public override void Serialize(MappingContext ctx, Utf8FaunaWriter writer) { writer.WriteStartObject(); writer.WriteFieldName("value"); var ser = Unwrap is not null ? Serializer.Generate(ctx, Unwrap.GetType()) : DynamicSerializer.Singleton; ser.Serialize(ctx, writer, Unwrap); writer.WriteEndObject(); } /// /// Determines whether the specified QueryVal is equal to the current QueryVal. /// /// The QueryVal to compare with the current QueryVal. /// true if the specified QueryVal is equal to the current QueryVal; otherwise, false. public override bool Equals(Query? o) => IsEqual(o as QueryVal); /// /// Determines whether the specified object is equal to the current QueryVal. /// /// The object to compare with the current QueryVal. /// true if the specified object is equal to the current QueryVal; otherwise, false. public override bool Equals(object? o) { if (ReferenceEquals(this, o)) { return true; } return IsEqual(o as QueryVal); } /// /// The default hash function. /// /// A hash code for the current QueryVal. public override int GetHashCode() { var hash = 31; if (Unwrap is not null) { hash *= Unwrap.GetHashCode(); } return hash; } /// /// Returns a string that represents the current QueryVal. /// /// A string that represents the current QueryVal. public override string ToString() => $"QueryVal({Unwrap})"; /// /// Determines whether two specified instances of QueryVal are equal. /// /// The first QueryVal to compare. /// The second QueryVal to compare. /// true if left and right are equal; otherwise, false. public static bool operator ==(QueryVal left, QueryVal right) { if (ReferenceEquals(left, right)) { return true; } if (left is null || right is null) { return false; } return left.Equals(right); } /// /// Determines whether two specified instances of QueryVal are not equal. /// /// The first QueryVal to compare. /// The second QueryVal to compare. /// true if left and right are not equal; otherwise, false. public static bool operator !=(QueryVal left, QueryVal right) { return !(left == right); } private bool IsEqual(QueryVal? o) { if (o is null) { return false; } if (Unwrap is null) { return (o.Unwrap is null) ? true : false; } return Unwrap.Equals(o.Unwrap); } } ``` ## File: Fauna/Serialization/BaseRefSerializer.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Types; namespace Fauna.Serialization; internal class RefSerializer : BaseSerializer> where T : notnull { private readonly BaseRefSerializer _baseRefSerializer; public RefSerializer(ISerializer docSerializer) { _baseRefSerializer = new BaseRefSerializer(docSerializer); } public override List GetSupportedTypes() => new List { FaunaType.Null, FaunaType.Ref }; public override Ref Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) { return (Ref)_baseRefSerializer.Deserialize(ctx, ref reader); } public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { _baseRefSerializer.Serialize(context, writer, o); } } internal class NamedRefSerializer : BaseSerializer> where T : notnull { private readonly BaseRefSerializer _baseRefSerializer; public NamedRefSerializer(ISerializer docSerializer) { _baseRefSerializer = new BaseRefSerializer(docSerializer); } public override List GetSupportedTypes() => new List { FaunaType.Null, FaunaType.Ref }; public override NamedRef Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) { return (NamedRef)_baseRefSerializer.Deserialize(ctx, ref reader); } public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { _baseRefSerializer.Serialize(context, writer, o); } } internal class BaseRefSerializer : BaseSerializer> where T : notnull { private readonly ISerializer _docSerializer; public BaseRefSerializer(ISerializer docSerializer) { _docSerializer = docSerializer; } public override List GetSupportedTypes() => new List { FaunaType.Null, FaunaType.Ref }; public override BaseRef Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) { return reader.CurrentTokenType switch { TokenType.StartRef => DeserializeRefInternal(new BaseRefBuilder(), ctx, ref reader), TokenType.StartDocument => DeserializeDocument(new BaseRefBuilder(), ctx, ref reader), _ => throw new SerializationException( $"Unexpected token while deserializing into Ref: {reader.CurrentTokenType}") }; } public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case Ref r: writer.WriteStartRef(); writer.WriteString("id", r.Id); writer.WriteModule("coll", r.Collection); writer.WriteEndRef(); break; case NamedRef r: writer.WriteStartRef(); writer.WriteString("name", r.Name); writer.WriteModule("coll", r.Collection); writer.WriteEndRef(); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } private static BaseRef DeserializeRefInternal(BaseRefBuilder builder, MappingContext context, ref Utf8FaunaReader reader) { while (reader.Read() && reader.CurrentTokenType != TokenType.EndRef) { if (reader.CurrentTokenType != TokenType.FieldName) throw new SerializationException( $"Unexpected token while deserializing into NamedRef: {reader.CurrentTokenType}"); string fieldName = reader.GetString()!; reader.Read(); switch (fieldName) { case "id": builder.Id = reader.GetString(); break; case "name": builder.Name = reader.GetString(); break; case "coll": builder.Collection = reader.GetModule(); break; case "cause": builder.Cause = reader.GetString(); break; case "exists": builder.Exists = reader.GetBoolean(); break; default: throw new SerializationException( $"Unexpected field while deserializing into Ref: {fieldName}"); } } return builder.Build(); } public BaseRef DeserializeDocument(BaseRefBuilder builder, MappingContext context, ref Utf8FaunaReader reader) { while (reader.Read() && reader.CurrentTokenType != TokenType.EndDocument) { if (reader.CurrentTokenType != TokenType.FieldName) throw new SerializationException( $"Unexpected token while deserializing into NamedRef: {reader.CurrentTokenType}"); string fieldName = reader.GetString()!; reader.Read(); switch (fieldName) { case "id": builder.Id = reader.GetString(); break; case "name": builder.Name = reader.GetString(); break; case "coll": builder.Collection = reader.GetModule(); if (_docSerializer is not IPartialDocumentSerializer cs) { throw new SerializationException($"Serializer {_docSerializer.GetType().Name} must implement IPartialDocumentSerializer interface."); } // This assumes ordering on the wire. If name is not null and we're here, then it's a named document so name is a string. builder.Doc = (T?)cs.DeserializeDocument(context, builder.Id, builder.Name, builder.Collection, ref reader); break; } // After we deserialize into a doc, we end on the EndDocument a token and do not want to read again if (reader.CurrentTokenType == TokenType.EndDocument) break; } return builder.Build(); } } ``` ## File: Fauna/Serialization/ClassSerializer.cs ```csharp using System.Diagnostics; using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Types; namespace Fauna.Serialization; internal class ClassSerializer : BaseSerializer, IPartialDocumentSerializer { private const string IdField = "id"; private const string NameField = "name"; private const string CollField = "coll"; private readonly MappingInfo _info; public ClassSerializer(MappingInfo info) { Debug.Assert(info.Type == typeof(T)); _info = info; } public override List GetSupportedTypes() => new List { FaunaType.Document, FaunaType.Null, FaunaType.Object, FaunaType.Ref }; public object DeserializeDocument(MappingContext context, string? id, string? name, Module? coll, ref Utf8FaunaReader reader) { object instance = CreateInstance(); if (id is not null) TrySetId(instance, id); if (name is not null) TrySetName(instance, name); if (coll is not null) TrySetColl(instance, coll); SetFields(instance, context, ref reader, TokenType.EndDocument); return instance; } public override T Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) { var endToken = reader.CurrentTokenType switch { TokenType.StartDocument => TokenType.EndDocument, TokenType.StartObject => TokenType.EndObject, TokenType.StartRef => TokenType.EndRef, var other => throw UnexpectedToken(other), }; if (endToken == TokenType.EndRef) { string? id = null; string? name = null; Module? coll = null; string? cause = null; bool exists = true; while (reader.Read() && reader.CurrentTokenType != TokenType.EndRef) { if (reader.CurrentTokenType != TokenType.FieldName) throw new SerializationException( $"Unexpected token while deserializing into Ref: {reader.CurrentTokenType}"); string fieldName = reader.GetString()!; reader.Read(); switch (fieldName) { case "id": id = reader.GetString(); break; case "name": name = reader.GetString(); break; case "coll": coll = reader.GetModule(); break; case "cause": cause = reader.GetString(); break; case "exists": exists = reader.GetBoolean(); break; } } if ((id != null || name != null) && coll != null && exists) { throw new SerializationException("Cannot deserialize refs into classes."); } if (id != null) { throw new NullDocumentException(id, null, coll!, cause!); } throw new NullDocumentException(null, name, coll!, cause!); } object instance = CreateInstance(); SetFields(instance, ctx, ref reader, endToken); return (T)instance; } public override void Serialize(MappingContext ctx, Utf8FaunaWriter writer, object? o) { SerializeInternal(ctx, writer, o); } private static void SerializeInternal(MappingContext ctx, Utf8FaunaWriter w, object? o) { if (o == null) { w.WriteNullValue(); return; } var t = o.GetType(); var info = ctx.GetInfo(t); bool shouldEscape = info.ShouldEscapeObject; if (shouldEscape) w.WriteStartEscapedObject(); else w.WriteStartObject(); foreach (var field in info.Fields) { if (field.FieldType is FieldType.ServerGeneratedId or FieldType.Ts or FieldType.Coll) { continue; } object? v = field.Property.GetValue(o); if (field.FieldType is FieldType.ClientGeneratedId && v == null) { // The field is a client generated ID but set to null, so assume they're doing something // other than creating the object. continue; } w.WriteFieldName(field.Name); field.Serializer.Serialize(ctx, w, v); } if (shouldEscape) w.WriteEndEscapedObject(); else w.WriteEndObject(); } private object CreateInstance() => Activator.CreateInstance(_info.Type)!; private void SetFields(object instance, MappingContext context, ref Utf8FaunaReader reader, TokenType endToken) { while (reader.Read() && reader.CurrentTokenType != endToken) { if (reader.CurrentTokenType != TokenType.FieldName) throw UnexpectedToken(reader.CurrentTokenType); string fieldName = reader.GetString()!; reader.Read(); if (fieldName == IdField && reader.CurrentTokenType == TokenType.String) { TrySetId(instance, reader.GetString()!); } else if (fieldName == NameField && reader.CurrentTokenType == TokenType.String) { TrySetName(instance, reader.GetString()!); } else if (_info.FieldsByName.TryGetValue(fieldName, out var field)) { field.Property.SetValue(instance, field.Serializer.Deserialize(context, ref reader)); } else { reader.Skip(); } } } private void TrySetId(object instance, string id) { if (!_info.FieldsByName.TryGetValue(IdField, out var field)) { return; } if (field.Type == typeof(long)) { field.Property.SetValue(instance, long.Parse(id)); } else if (field.Type == typeof(string)) { field.Property.SetValue(instance, id); } else { throw UnexpectedToken(TokenType.String); } } private void TrySetName(object instance, string name) { if (_info.FieldsByName.TryGetValue(NameField, out var field)) { if (field.Type == typeof(string)) { field.Property.SetValue(instance, name); } else { throw UnexpectedToken(TokenType.String); } } } private void TrySetColl(object instance, Module coll) { if (_info.FieldsByName.TryGetValue(CollField, out var field)) { if (field.Type == typeof(Module)) { field.Property.SetValue(instance, coll); } else { throw UnexpectedToken(TokenType.String); } } } private new SerializationException UnexpectedToken(TokenType tokenType) => new($"Unexpected token while deserializing into class {_info.Type.Name}: {tokenType}"); } ``` ## File: Fauna/Serialization/DictionarySerializer.cs ```csharp using System.Text.Json; using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Types; namespace Fauna.Serialization; internal class DictionarySerializer : BaseSerializer>, IPartialDocumentSerializer { private readonly ISerializer _elemSerializer; public DictionarySerializer(ISerializer elemSerializer) { _elemSerializer = elemSerializer; } public override List GetSupportedTypes() => new List { FaunaType.Null, FaunaType.Object }; public override Dictionary Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) { switch (reader.CurrentTokenType) { case TokenType.StartObject: return DeserializeInternal(new Dictionary(), TokenType.EndObject, ctx, ref reader); case TokenType.StartDocument: return DeserializeInternal(new Dictionary(), TokenType.EndDocument, ctx, ref reader); default: throw new SerializationException( $"Unexpected token while deserializing into {typeof(Dictionary)}: {reader.CurrentTokenType}"); } } public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case Dictionary d: bool shouldEscape = Serializer.Tags.Overlaps(d.Keys); if (shouldEscape) writer.WriteStartEscapedObject(); else writer.WriteStartObject(); foreach (var (key, value) in d) { writer.WriteFieldName(key); _elemSerializer.Serialize(context, writer, value); } if (shouldEscape) writer.WriteEndEscapedObject(); else writer.WriteEndObject(); break; default: throw new NotImplementedException(); } } public object DeserializeDocument(MappingContext context, string? id, string? name, Module? coll, ref Utf8FaunaReader reader) { var dict = new Dictionary(); if (typeof(T) == typeof(object)) { if (id != null) dict.Add("id", (T)(object)id); if (name != null) dict.Add("name", (T)(object)name); if (coll != null) dict.Add("coll", (T)(object)coll); } return DeserializeInternal(dict, TokenType.EndDocument, context, ref reader); } private Dictionary DeserializeInternal( Dictionary dict, TokenType endToken, MappingContext context, ref Utf8FaunaReader reader) { while (reader.Read() && reader.CurrentTokenType != endToken) { if (reader.CurrentTokenType != TokenType.FieldName) throw new SerializationException( $"Unexpected token while deserializing field of {typeof(Dictionary)}: {reader.CurrentTokenType}"); string fieldName = reader.GetString()!; reader.Read(); dict.Add(fieldName, _elemSerializer.Deserialize(context, ref reader)); } return dict; } } ``` ## File: Fauna/Serialization/DynamicSerializer.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Types; namespace Fauna.Serialization; internal class DynamicSerializer : BaseSerializer { public static DynamicSerializer Singleton { get; } = new(); private readonly ListSerializer _list; private readonly PageSerializer _page; private readonly DictionarySerializer _dict; private readonly BaseRefSerializer> _docref; private DynamicSerializer() { _list = new ListSerializer(this); _page = new PageSerializer(this); _dict = new DictionarySerializer(this); _docref = new BaseRefSerializer>(_dict); } public override List GetSupportedTypes() => new List { FaunaType.Array, FaunaType.Boolean, FaunaType.Bytes, FaunaType.Date, FaunaType.Double, FaunaType.Document, FaunaType.Int, FaunaType.Long, FaunaType.Module, FaunaType.Null, FaunaType.Object, FaunaType.Ref, FaunaType.Set, FaunaType.Stream, FaunaType.String, FaunaType.Time }; public override object? Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.StartObject => _dict.Deserialize(ctx, ref reader), TokenType.StartArray => _list.Deserialize(ctx, ref reader), TokenType.StartPage => _page.Deserialize(ctx, ref reader), TokenType.StartRef => _docref.Deserialize(ctx, ref reader), TokenType.StartDocument => DeserializeDocumentInternal(ctx, ref reader), TokenType.String => reader.GetString(), TokenType.Int => reader.GetInt(), TokenType.Long => reader.GetLong(), TokenType.Double => reader.GetDouble(), TokenType.Date => reader.GetDate(), TokenType.Time => reader.GetTime(), TokenType.True or TokenType.False => reader.GetBoolean(), TokenType.Module => reader.GetModule(), TokenType.Bytes => reader.GetBytes(), TokenType.Null => null, TokenType.EventSource => reader.GetEventSource(), _ => throw new SerializationException( $"Unexpected token while deserializing: {reader.CurrentTokenType}"), }; private object DeserializeDocumentInternal(MappingContext context, ref Utf8FaunaReader reader) { var builder = new BaseRefBuilder>(); while (reader.Read() && reader.CurrentTokenType != TokenType.EndDocument) { if (reader.CurrentTokenType != TokenType.FieldName) throw new SerializationException( $"Unexpected token while deserializing @doc: {reader.CurrentTokenType}"); string fieldName = reader.GetString()!; reader.Read(); switch (fieldName) { // Relies on ordering for doc fields. case "id": builder.Id = reader.GetString(); break; case "name": builder.Name = reader.GetString(); break; case "coll": builder.Collection = reader.GetModule(); // if we encounter a mapped collection, jump to the class deserializer. // NB this relies on the fact that docs on the wire always start with id and coll. if (context.TryGetCollection(builder.Collection.Name, out var info) && info.ClassSerializer is IPartialDocumentSerializer ser) { return new BaseRefBuilder { Id = builder.Id, Name = builder.Name, Collection = builder.Collection, Doc = ser.DeserializeDocument(context, builder.Id, builder.Name, builder.Collection, ref reader) }.Build(); } builder.Doc = (Dictionary?)_dict.DeserializeDocument(context, builder.Id, builder.Name, builder.Collection, ref reader); break; } // After we deserialize into a doc, we end on the EndDocument a token and do not want to read again if (reader.CurrentTokenType == TokenType.EndDocument) break; } return builder.Build(); } /// /// Serializes an object to a Fauna compatible format. /// /// The context for serialization. /// The writer to serialize the object to. /// The object to serialize. /// Thrown when serialization fails. public override void Serialize(MappingContext ctx, Utf8FaunaWriter w, object? o) { if (o == null) { w.WriteNullValue(); return; } var ser = Serializer.Generate(ctx, o.GetType()); ser.Serialize(ctx, w, o); } } ``` ## File: Fauna/Serialization/EventSourceSerializer.cs ```csharp using Fauna.Mapping; using Fauna.Types; namespace Fauna.Serialization; internal class EventSourceSerializer : BaseSerializer { public override List GetSupportedTypes() => new List { FaunaType.Null, FaunaType.Stream }; public override EventSource Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.EventSource => reader.GetEventSource(), _ => throw UnexpectedToken(reader.CurrentTokenType) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { throw new NotImplementedException(); } } ``` ## File: Fauna/Serialization/FaunaType.cs ```csharp namespace Fauna.Serialization; /// /// An enum representing possible Fauna types. /// public enum FaunaType { /// /// A Fauna integer. /// Int, /// /// A Fauna long. /// Long, /// /// A Fauna double. /// Double, /// /// A Fauna string. /// String, /// /// A Fauna date. /// Date, /// /// A Fauna time. /// Time, /// /// A Fauna boolean. /// Boolean, /// /// A Fauna object. This is different from a . /// Object, /// /// A Fauna document reference. This includes named documents. /// Ref, /// /// A Fauna document. /// Document, /// /// A Fauna array. /// Array, /// /// A Fauna byte array, stored as a base-64 encoded string. /// Bytes, /// /// A null value. /// Null, /// /// A Fauna event source. /// Stream, /// /// A Fauna module. /// Module, /// /// A Fauna set. /// Set } ``` ## File: Fauna/Serialization/IPartialDocumentSerializer.cs ```csharp using Fauna.Mapping; using Fauna.Types; namespace Fauna.Serialization; internal interface IPartialDocumentSerializer : ISerializer { public object DeserializeDocument(MappingContext context, string? id, string? name, Module? coll, ref Utf8FaunaReader reader); } ``` ## File: Fauna/Serialization/ISerializer.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; namespace Fauna.Serialization; /// /// A generic interface that defines serialize and deserialize behavior for a specific type, and declares the supported . /// /// The type applies to. public interface ISerializer : ISerializer { /// /// Consumes all or some of a and returns an instance of . /// /// A used to influence deserialization. /// The to consume. /// An instance representing part or all of the consumed . new T Deserialize(MappingContext ctx, ref Utf8FaunaReader reader); } /// /// An interface that defines serialize and deserialize behavior for each supported . /// public interface ISerializer { /// /// Consumes all or some of a and returns an object or null. /// /// A used to influence deserialization. /// The to consume. /// An instance representing part or all of the consumed . object? Deserialize(MappingContext ctx, ref Utf8FaunaReader reader); /// /// Serializes the provided object into the . /// /// >A used to influence serialization. /// The to write to. /// The object to serialize. void Serialize(MappingContext ctx, Utf8FaunaWriter writer, object? o); /// /// A list of types to which the applies. /// /// A list of s. List GetSupportedTypes(); } /// /// An abstract class encapsulating common serialization and deserialization logic. /// /// The type the BaseSerializer applies to. public abstract class BaseSerializer : ISerializer { /// /// Supported types for the serializer. /// /// A list of supported s. public virtual List GetSupportedTypes() => new List(); /// /// Gets the end token for a given start token. /// /// A start token. /// The end token. /// Thrown when the start token does not have a related end token. protected static TokenType EndTokenFor(TokenType start) { return start switch { TokenType.StartObject => TokenType.EndObject, TokenType.StartArray => TokenType.EndArray, TokenType.StartPage => TokenType.EndPage, TokenType.StartRef => TokenType.EndRef, TokenType.StartDocument => TokenType.EndDocument, _ => throw new ArgumentOutOfRangeException(nameof(start), start, null) }; } /// /// A helper to build an unsupported serialization type exception message. /// /// The unsupported type /// An exception message to use. protected string UnsupportedSerializationTypeMessage(Type type) => $"Cannot serialize `{type}` with `{GetType()}`"; /// /// A helper to build an unexpected type decoding exception message. /// /// The unexpected fauna type. /// An exception message to use. protected string UnexpectedTypeDecodingMessage(FaunaType faunaType) => $"Unable to deserialize `{faunaType.GetType().Name}.{faunaType}` with `{GetType().Name}`. " + $"Supported types are [{string.Join(", ", GetSupportedTypes().Select(x => $"{x.GetType().Name}.{x}"))}]"; object? ISerializer.Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => Deserialize(ctx, ref reader); /// /// Consumes or partially consumes the provided reader and deserializes into a result. /// /// A to influence deserialization. /// A to consume or partially consume. /// public abstract T Deserialize(MappingContext ctx, ref Utf8FaunaReader reader); void ISerializer.Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) => Serialize(context, writer, o); /// /// Serializes the provided object onto the /// /// A to influence serialization. /// A to write to. /// The object to write. public abstract void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o); /// /// Creates a serailization exception with an unexpected token exception message. /// /// /// protected static SerializationException UnexpectedToken(TokenType token) => new($"Unexpected token while deserializing: {token}"); } ``` ## File: Fauna/Serialization/ListSerializer.cs ```csharp using System.Collections; using Fauna.Exceptions; using Fauna.Mapping; using ArgumentException = System.ArgumentException; namespace Fauna.Serialization; internal class ListSerializer : BaseSerializer> { private readonly ISerializer _elemSerializer; public ListSerializer(ISerializer elemSerializer) { _elemSerializer = elemSerializer; } public override List GetSupportedTypes() => new List { FaunaType.Array, FaunaType.Null }; public override List Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) { if (reader.CurrentTokenType == TokenType.StartPage) throw new SerializationException( $"Unexpected token while deserializing into {typeof(List)}: {reader.CurrentTokenType}"); var wrapInList = reader.CurrentTokenType != TokenType.StartArray; var lst = new List(); if (wrapInList) { lst.Add(_elemSerializer.Deserialize(ctx, ref reader)); } else { while (reader.Read() && reader.CurrentTokenType != TokenType.EndArray) { lst.Add(_elemSerializer.Deserialize(ctx, ref reader)); } } return lst; } public override void Serialize(MappingContext ctx, Utf8FaunaWriter w, object? o) { if (o is null) { w.WriteNullValue(); return; } if (o.GetType().IsGenericType && o.GetType().GetGenericTypeDefinition() == typeof(List<>) || o.GetType().GetGenericTypeDefinition() == typeof(IEnumerable)) { w.WriteStartArray(); foreach (object? elem in (IEnumerable)o) { _elemSerializer.Serialize(ctx, w, elem); } w.WriteEndArray(); return; } throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } ``` ## File: Fauna/Serialization/ModuleSerializer.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Types; namespace Fauna.Serialization; internal class ModuleSerializer : BaseSerializer { public override List GetSupportedTypes() => new List { FaunaType.Module, FaunaType.Null }; public override Module Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Module => reader.GetModule(), _ => throw UnexpectedToken(reader.CurrentTokenType) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case Module module: writer.WriteModuleValue(module); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } } ``` ## File: Fauna/Serialization/NullableSerializer.cs ```csharp using Fauna.Mapping; namespace Fauna.Serialization; internal class NullableSerializer : BaseSerializer { private readonly ISerializer _inner; public NullableSerializer(ISerializer inner) { _inner = inner; } public override List GetSupportedTypes() => _inner.GetSupportedTypes(); public override T? Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) { if (reader.CurrentTokenType == TokenType.Null) { return default(T); } return _inner.Deserialize(ctx, ref reader); } public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { _inner.Serialize(context, writer, (T?)o); } } ``` ## File: Fauna/Serialization/NullableStructSerializer.cs ```csharp using Fauna.Mapping; namespace Fauna.Serialization; internal class NullableStructSerializer : BaseSerializer where T : struct { private readonly ISerializer _inner; public NullableStructSerializer(ISerializer inner) { _inner = inner; } public override List GetSupportedTypes() => _inner.GetSupportedTypes(); public override T? Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) { return reader.CurrentTokenType == TokenType.Null ? new T?() : _inner.Deserialize(ctx, ref reader); } public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { _inner.Serialize(context, writer, (T?)o); } } ``` ## File: Fauna/Serialization/PageSerializer.cs ```csharp using Fauna.Mapping; using Fauna.Types; namespace Fauna.Serialization; internal class PageSerializer : BaseSerializer> { private readonly ISerializer> _dataSerializer; public PageSerializer(ISerializer elemSerializer) { _dataSerializer = new ListSerializer(elemSerializer); } public override List GetSupportedTypes() => [FaunaType.Null, FaunaType.Set]; public override Page Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) { bool wrapInPage = false; var endToken = TokenType.None; switch (reader.CurrentTokenType) { case TokenType.StartPage: endToken = TokenType.EndPage; break; case TokenType.StartObject: endToken = TokenType.EndObject; break; default: wrapInPage = true; break; } if (wrapInPage) { var data = _dataSerializer.Deserialize(ctx, ref reader); return new Page(data, null); } reader.Read(); return reader.CurrentTokenType == TokenType.String ? HandleUnmaterialized(ctx, ref reader, endToken) : HandleMaterialized(ctx, ref reader, endToken); } private Page HandleUnmaterialized(MappingContext ctx, ref Utf8FaunaReader reader, TokenType endToken) { string after = reader.GetString()!; reader.Read(); if (reader.CurrentTokenType != endToken) { throw UnexpectedToken(reader.CurrentTokenType); } return new Page([], after); } private Page HandleMaterialized(MappingContext ctx, ref Utf8FaunaReader reader, TokenType endToken) { List data = []; string? after = null; do { string fieldName = reader.GetString()!; reader.Read(); switch (fieldName) { case "data": data = _dataSerializer.Deserialize(ctx, ref reader); break; case "after": after = reader.GetString()!; break; } } while (reader.Read() && reader.CurrentTokenType != endToken); return new Page(data, after); } public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { throw new NotImplementedException(); } } ``` ## File: Fauna/Serialization/QueryArrSerializer.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; namespace Fauna.Serialization; internal class QueryArrSerializer : BaseSerializer { public override QueryArr Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => throw new NotImplementedException(); public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? obj) { switch (obj) { case null: writer.WriteNullValue(); break; case QueryArr o: writer.WriteStartObject(); writer.WriteFieldName("array"); var ser = Serializer.Generate(context, o.Unwrap.GetType()); ser.Serialize(context, writer, o.Unwrap); writer.WriteEndObject(); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(obj.GetType())); } } } ``` ## File: Fauna/Serialization/QueryExprSerializer.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Types; namespace Fauna.Serialization; internal class QueryExprSerializer : BaseSerializer { public override QueryExpr Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => throw new NotImplementedException(); public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? obj) { switch (obj) { case null: writer.WriteNullValue(); break; case QueryExpr o: writer.WriteStartObject(); writer.WriteFieldName("fql"); writer.WriteStartArray(); foreach (var t in o.Unwrap) { var ser = Serializer.Generate(context, t.GetType()); ser.Serialize(context, writer, t); } writer.WriteEndArray(); writer.WriteEndObject(); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(obj.GetType())); } } } ``` ## File: Fauna/Serialization/QueryLiteralSerializer.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; namespace Fauna.Serialization; internal class QueryLiteralSerializer : BaseSerializer { public override QueryLiteral Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => throw new NotImplementedException(); public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? obj) { switch (obj) { case null: writer.WriteNullValue(); break; case QueryLiteral o: writer.WriteStringValue(o.Unwrap); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(obj.GetType())); } } } ``` ## File: Fauna/Serialization/QueryObjSerializer.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; namespace Fauna.Serialization; internal class QueryObjSerializer : BaseSerializer { public override QueryObj Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => throw new NotImplementedException(); public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? obj) { switch (obj) { case null: writer.WriteNullValue(); break; case QueryObj o: writer.WriteStartObject(); writer.WriteFieldName("object"); var ser = Serializer.Generate(context, o.Unwrap.GetType()); ser.Serialize(context, writer, o.Unwrap); writer.WriteEndObject(); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(obj.GetType())); } } } ``` ## File: Fauna/Serialization/QuerySerializer.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; namespace Fauna.Serialization; internal class QuerySerializer : BaseSerializer { public override Query Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => throw new NotImplementedException(); public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? obj) { switch (obj) { case null: writer.WriteNullValue(); break; case Query o: var ser = Serializer.Generate(context, o.GetType()); ser.Serialize(context, writer, o); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(obj.GetType())); } } } ``` ## File: Fauna/Serialization/QueryValSerializer.cs ```csharp using System.Diagnostics; using Fauna.Exceptions; using Fauna.Mapping; namespace Fauna.Serialization; internal class QueryValSerializer : BaseSerializer { public override QueryObj Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => throw new NotImplementedException(); public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? obj) { switch (obj) { case null: writer.WriteNullValue(); break; case QueryVal v: writer.WriteStartObject(); writer.WriteFieldName("value"); var ser = v.Unwrap is not null ? Serializer.Generate(context, v.Unwrap.GetType()) : DynamicSerializer.Singleton; ser.Serialize(context, writer, v.Unwrap); writer.WriteEndObject(); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(obj.GetType())); } } } ``` ## File: Fauna/Serialization/Serializer.cs ```csharp using System.Runtime.CompilerServices; using Fauna.Mapping; using Fauna.Types; namespace Fauna.Serialization; /// /// Represents methods for serializing objects to and from Fauna's value format. /// public static class Serializer { /// /// The dynamic data serializer. /// public static ISerializer Dynamic => DynamicSerializer.Singleton; private static readonly Dictionary _reg = new(); internal static readonly HashSet Tags = new() { "@int", "@long", "@double", "@date", "@time", "@mod", "@stream", "@ref", "@doc", "@set", "@object", "@bytes" }; private static readonly DynamicSerializer s_object = DynamicSerializer.Singleton; private static readonly StringSerializer s_string = new(); private static readonly ByteSerializer s_byte = new(); private static readonly BytesSerializer s_bytes = new(); private static readonly SByteSerializer s_sbyte = new(); private static readonly ShortSerializer s_short = new(); private static readonly UShortSerializer s_ushort = new(); private static readonly IntSerializer s_int = new(); private static readonly UIntSerializer s_uint = new(); private static readonly LongSerializer s_long = new(); private static readonly FloatSerializer s_float = new(); private static readonly DoubleSerializer s_double = new(); private static readonly DateOnlySerializer s_dateOnly = new(); private static readonly DateTimeSerializer s_dateTime = new(); private static readonly DateTimeOffsetSerializer s_dateTimeOffset = new(); private static readonly BooleanSerializer s_bool = new(); private static readonly ModuleSerializer s_module = new(); private static readonly EventSourceSerializer s_eventSource = new(); private static readonly QuerySerializer s_query = new(); private static readonly QueryExprSerializer s_queryExpr = new(); private static readonly QueryLiteralSerializer s_queryLiteral = new(); private static readonly QueryArrSerializer s_queryArr = new(); private static readonly QueryObjSerializer s_queryObj = new(); private static readonly QueryValSerializer s_queryVal = new(); /// /// Generates a serializer for the specified non-nullable .NET type. /// /// The type of the object to serialize. /// The serialization context. /// An . public static ISerializer Generate(MappingContext context) where T : notnull { var targetType = typeof(T); var ser = (ISerializer)Generate(context, targetType); return ser; } /// /// Generates a serializer for the specified non-nullable .NET type. /// /// The serialization context. /// The type of the object to serialize. /// An . public static ISerializer Generate(MappingContext context, Type targetType) { if (_reg.TryGetValue(targetType, out var s)) return s; if (IsAnonymousType(targetType)) { var info = context.GetInfo(targetType); return info.ClassSerializer; } if (targetType == typeof(object)) return s_object; if (targetType == typeof(string)) return s_string; if (targetType == typeof(byte)) return s_byte; if (targetType == typeof(byte[])) return s_bytes; if (targetType == typeof(sbyte)) return s_sbyte; if (targetType == typeof(short)) return s_short; if (targetType == typeof(ushort)) return s_ushort; if (targetType == typeof(int)) return s_int; if (targetType == typeof(uint)) return s_uint; if (targetType == typeof(long)) return s_long; if (targetType == typeof(float)) return s_float; if (targetType == typeof(double)) return s_double; if (targetType == typeof(DateOnly)) return s_dateOnly; if (targetType == typeof(DateTime)) return s_dateTime; if (targetType == typeof(DateTimeOffset)) return s_dateTimeOffset; if (targetType == typeof(bool)) return s_bool; if (targetType == typeof(Module)) return s_module; if (targetType == typeof(EventSource)) return s_eventSource; if (targetType == typeof(Query)) return s_query; if (targetType == typeof(QueryExpr)) return s_queryExpr; if (targetType == typeof(QueryLiteral)) return s_queryLiteral; if (targetType == typeof(QueryArr)) return s_queryArr; if (targetType == typeof(QueryObj)) return s_queryObj; if (targetType == typeof(QueryVal)) return s_queryVal; if (targetType.IsGenericType) { if (targetType.GetGenericTypeDefinition() == typeof(Nullable<>)) { var args = targetType.GetGenericArguments(); if (args.Length == 1) { var inner = (ISerializer)Generate(context, args[0]); var serType = typeof(NullableStructSerializer<>).MakeGenericType(new[] { args[0] }); object? ser = Activator.CreateInstance(serType, new[] { inner }); return (ISerializer)ser!; } throw new ArgumentException($"Unsupported nullable type. Generic arguments > 1: {args}"); } if (targetType.GetGenericTypeDefinition() == typeof(Dictionary<,>)) { var argTypes = targetType.GetGenericArguments(); var keyType = argTypes[0]; var valueType = argTypes[1]; if (keyType != typeof(string)) throw new ArgumentException( $"Unsupported Dictionary key type. Key must be of type string, but was a {keyType}"); var valueSerializer = Generate(context, valueType); var serType = typeof(DictionarySerializer<>).MakeGenericType(new[] { valueType }); object? ser = Activator.CreateInstance(serType, new[] { valueSerializer }); return (ISerializer)ser!; } if (targetType.GetGenericTypeDefinition() == typeof(List<>) || targetType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { var elemType = targetType.GetGenericArguments()[0]; var elemSerializer = Generate(context, elemType); var serType = typeof(ListSerializer<>).MakeGenericType(new[] { elemType }); object? ser = Activator.CreateInstance(serType, new[] { elemSerializer }); return (ISerializer)ser!; } if (targetType.GetGenericTypeDefinition() == typeof(Page<>)) { var elemType = targetType.GetGenericArguments()[0]; var elemSerializer = Generate(context, elemType); var serType = typeof(PageSerializer<>).MakeGenericType(new[] { elemType }); object? ser = Activator.CreateInstance(serType, new[] { elemSerializer }); return (ISerializer)ser!; } if (targetType.GetGenericTypeDefinition() == typeof(BaseRef<>)) { var docType = targetType.GetGenericArguments()[0]; var docSerializer = Generate(context, docType); var serType = typeof(BaseRefSerializer<>).MakeGenericType(new[] { docType }); object? ser = Activator.CreateInstance(serType, new[] { docSerializer }); return (ISerializer)ser!; } if (targetType.GetGenericTypeDefinition() == typeof(Ref<>)) { var docType = targetType.GetGenericArguments()[0]; var docSerializer = Generate(context, docType); var serType = typeof(RefSerializer<>).MakeGenericType(new[] { docType }); object? ser = Activator.CreateInstance(serType, new[] { docSerializer }); return (ISerializer)ser!; } if (targetType.GetGenericTypeDefinition() == typeof(NamedRef<>)) { var docType = targetType.GetGenericArguments()[0]; var docSerializer = Generate(context, docType); var serType = typeof(NamedRefSerializer<>).MakeGenericType(new[] { docType }); object? ser = Activator.CreateInstance(serType, new[] { docSerializer }); return (ISerializer)ser!; } if (targetType.IsGenericType && targetType.Name.Contains("AnonymousType")) { return DynamicSerializer.Singleton; } } if (targetType.IsClass) { var info = context.GetInfo(targetType); return info.ClassSerializer; } throw new ArgumentException($"Unsupported deserialization target type {targetType}"); } /// /// Generates a serializer which returns values of the specified .NET type, or the default if the underlying query value is null. /// /// The type of the object to serialize. /// The serialization context. /// An . public static ISerializer GenerateNullable(MappingContext context) { var targetType = typeof(T); var ser = (ISerializer)Generate(context, targetType); return new NullableSerializer(ser); } /// /// Generates a serializer which returns values of the specified .NET type, or the default if the underlying query value is null. /// /// The serialization context. /// The type of the object to serialize. /// An . public static ISerializer GenerateNullable(MappingContext context, Type targetType) { var inner = (ISerializer)Generate(context, targetType); var serType = typeof(NullableSerializer<>).MakeGenericType(new[] { targetType }); var ser = Activator.CreateInstance(serType, new[] { inner }); return (ISerializer)ser!; } /// /// Registers a serializer for a type. This serializer will take precedence over the default serializer for the that type. /// /// The type to associate with the serializer. /// The serializer. /// Throws if a serializer is already registered for the type. public static void Register(Type t, ISerializer s) { if (!_reg.TryAdd(t, s)) throw new ArgumentException($"Serializer for type `{t}` already registered"); } /// /// Registers a generic serializer. This serializer will take precedence over the default serializer for the that type. /// /// The generic serializer. /// Throws if a serializer is already registered for the type. public static void Register(ISerializer s) { var success = false; foreach (var i in s.GetType().GetInterfaces()) { if (!i.IsGenericType || i.GetGenericTypeDefinition() != typeof(ISerializer<>)) continue; var t = i.GetGenericArguments()[0]; success = _reg.TryAdd(t, s); if (!success) throw new ArgumentException($"Serializer for type `{t}` already registered"); break; } if (!success) throw new ArgumentException($"Could not infer associated type for `{s.GetType()}`. Use Register(type, serializer)."); } /// /// Deregisters a serializer for a type. If no serializer was registered, no-op. /// /// The associated type to deregister. public static void Deregister(Type t) { if (_reg.ContainsKey(t)) _reg.Remove(t); } private static bool IsAnonymousType(this Type type) { bool hasCompilerGeneratedAttribute = type.GetCustomAttributes(typeof(CompilerGeneratedAttribute), false).Any(); bool nameContainsAnonymousType = type?.FullName?.Contains("AnonymousType") ?? false; return hasCompilerGeneratedAttribute && nameContainsAnonymousType; } } ``` ## File: Fauna/Serialization/StructSerializers.cs ```csharp using Fauna.Exceptions; using Fauna.Mapping; namespace Fauna.Serialization; internal class StringSerializer : BaseSerializer { public override string? Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Null => null, TokenType.String => reader.GetString(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case string s: writer.WriteStringValue(s); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Null, FaunaType.String }; } internal class ByteSerializer : BaseSerializer { public override byte Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Int => reader.GetByte(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case byte i: writer.WriteIntValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Int, FaunaType.Null }; } internal class BytesSerializer : BaseSerializer { public override byte[] Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Bytes => reader.GetBytes(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case byte[] b: writer.WriteBytesValue(b); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Bytes, FaunaType.Null }; } internal class SByteSerializer : BaseSerializer { public override sbyte Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Int => reader.GetUnsignedByte(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case sbyte i: writer.WriteIntValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Int, FaunaType.Null }; } internal class ShortSerializer : BaseSerializer { public override short Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Int => reader.GetShort(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case short i: writer.WriteIntValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Int, FaunaType.Null }; } internal class UShortSerializer : BaseSerializer { public override ushort Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Int => reader.GetUnsignedShort(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case ushort i: writer.WriteIntValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Int, FaunaType.Null }; } internal class IntSerializer : BaseSerializer { public override int Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Int => reader.GetInt(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case int i: writer.WriteIntValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Int, FaunaType.Null }; } internal class UIntSerializer : BaseSerializer { public override uint Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Int or TokenType.Long => reader.GetUnsignedInt(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case uint i: writer.WriteLongValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Int, FaunaType.Long, FaunaType.Null }; } internal class LongSerializer : BaseSerializer { public override long Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Int or TokenType.Long => reader.GetLong(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case long i: writer.WriteLongValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Int, FaunaType.Long, FaunaType.Null }; } internal class FloatSerializer : BaseSerializer { public override float Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Int or TokenType.Long or TokenType.Double => reader.GetFloat(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case float i: writer.WriteDoubleValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Double, FaunaType.Int, FaunaType.Long, FaunaType.Null }; } internal class DoubleSerializer : BaseSerializer { public override double Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Int or TokenType.Long or TokenType.Double => reader.GetDouble(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case double i: writer.WriteDoubleValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Double, FaunaType.Int, FaunaType.Long, FaunaType.Null }; } internal class BooleanSerializer : BaseSerializer { public override bool Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.True or TokenType.False => reader.GetBoolean(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case bool i: writer.WriteBooleanValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Boolean, FaunaType.Null }; } internal class DateOnlySerializer : BaseSerializer { public override DateOnly Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Date => reader.GetDate(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case DateOnly i: writer.WriteDateValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Date, FaunaType.Null }; } internal class DateTimeSerializer : BaseSerializer { public override DateTime Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Time => reader.GetTime(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case DateTime i: writer.WriteTimeValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Null, FaunaType.Time }; } internal class DateTimeOffsetSerializer : BaseSerializer { public override DateTimeOffset Deserialize(MappingContext ctx, ref Utf8FaunaReader reader) => reader.CurrentTokenType switch { TokenType.Time => reader.GetTime(), _ => throw new SerializationException(UnexpectedTypeDecodingMessage(reader.CurrentTokenType.GetFaunaType())) }; public override void Serialize(MappingContext context, Utf8FaunaWriter writer, object? o) { switch (o) { case null: writer.WriteNullValue(); break; case DateTimeOffset i: writer.WriteTimeValue(i); break; default: throw new SerializationException(UnsupportedSerializationTypeMessage(o.GetType())); } } public override List GetSupportedTypes() => new List { FaunaType.Null, FaunaType.Time }; } ``` ## File: Fauna/Serialization/TokenType.cs ```csharp namespace Fauna.Serialization; /// /// Enumerates the types of tokens used in Fauna serialization. /// public enum TokenType { /// There is no value. This is the default token type if no data has been read by the . None, /// The token type is the start of a Fauna object. StartObject, /// The token type is the end of a Fauna object. EndObject, /// The token type is the start of a Fauna array. StartArray, /// The token type is the end of a Fauna array. EndArray, /// The token type is the start of a Fauna set (a.k.a. page). StartPage, /// The token type is the end of a Fauna set (a.k.a. page). EndPage, /// The token type is the start of a Fauna ref. StartRef, /// The token type is the end of a Fauna ref. EndRef, /// The token type is the start of a Fauna document. StartDocument, /// The token type is the end of a Fauna document. EndDocument, /// The token type is a Fauna property name. FieldName, /// The token type is a Fauna string. String, /// The token type is a Fauna byte array. Bytes, /// The token type is a Fauna integer. Int, /// The token type is a Fauna long. Long, /// The token type is a Fauna double. Double, /// The token type is a Fauna date. Date, /// The token type is a Fauna time. Time, /// The token type is the Fauna literal true. True, /// The token type is the Fauna literal false. False, /// The token type is the Fauna literal null. Null, /// The token type is the Fauna module. Module, /// The token type is the Fauna event source token. EventSource, } /// /// A set of extension methods that operate on . /// public static class TokenTypeExtensions { /// /// Maps a to a . /// /// The token type to map to a Fauna type. /// The related Fauna type. /// Thrown when the provided token type does not map to a Fauna type. public static FaunaType GetFaunaType(this TokenType tokenType) { switch (tokenType) { case TokenType.StartObject: case TokenType.EndObject: return FaunaType.Object; case TokenType.StartArray: case TokenType.EndArray: return FaunaType.Array; case TokenType.StartPage: case TokenType.EndPage: return FaunaType.Set; case TokenType.StartRef: case TokenType.EndRef: return FaunaType.Ref; case TokenType.StartDocument: case TokenType.EndDocument: return FaunaType.Document; case TokenType.String: return FaunaType.String; case TokenType.Bytes: return FaunaType.Bytes; case TokenType.Int: return FaunaType.Int; case TokenType.Long: return FaunaType.Long; case TokenType.Double: return FaunaType.Double; case TokenType.Date: return FaunaType.Date; case TokenType.Time: return FaunaType.Time; case TokenType.True: case TokenType.False: return FaunaType.Boolean; case TokenType.Null: return FaunaType.Null; case TokenType.EventSource: return FaunaType.Stream; case TokenType.Module: return FaunaType.Module; default: throw new ArgumentException($"No associated FaunaType for TokenType: {tokenType}"); } } } ``` ## File: Fauna/Serialization/Utf8FaunaReader.cs ```csharp using System.Buffers; using System.Globalization; using System.Text; using System.Text.Json; using Fauna.Exceptions; using Fauna.Types; namespace Fauna.Serialization; /// /// Represents a reader that provides fast, non-cached, forward-only access to serialized data. /// public ref struct Utf8FaunaReader { private Utf8JsonReader _json; private readonly Stack _tokenStack = new(); private TokenType? _bufferedTokenType = null; private object? _bufferedTokenValue = null; private readonly HashSet _closers = new() { TokenType.EndObject, TokenType.EndPage, TokenType.EndDocument, TokenType.EndRef, TokenType.EndArray }; private string? _taggedTokenValue = null; /// /// Gets the type of the current token. /// public TokenType CurrentTokenType { get; private set; } private enum TokenTypeInternal { /// The token type is the start of an escaped Fauna object. StartEscapedObject, StartPageUnmaterialized, } /// /// Initializes a new Utf8FaunaReader to read from a ReadOnlySequence of bytes. /// /// The sequence of bytes to read from. public Utf8FaunaReader(ReadOnlySequence bytes) { _json = new Utf8JsonReader(bytes); CurrentTokenType = TokenType.None; } /// /// Initializes a new Utf8FaunaReader to read from a string. /// /// The string to read from. public Utf8FaunaReader(string str) { var bytes = Encoding.UTF8.GetBytes(str); var seq = new ReadOnlySequence(bytes); _json = new Utf8JsonReader(seq); CurrentTokenType = TokenType.None; } /// /// Skips the value of the current token. /// public void Skip() { switch (CurrentTokenType) { case TokenType.StartObject: case TokenType.StartArray: case TokenType.StartPage: case TokenType.StartRef: case TokenType.StartDocument: SkipInternal(); break; } } private void SkipInternal() { var startCount = _tokenStack.Count; while (Read()) { if (_tokenStack.Count < startCount) break; } } /// /// Reads the next token from the source. /// /// true if the token was read successfully; otherwise, false. public bool Read() { _taggedTokenValue = null; if (_bufferedTokenType != null) { CurrentTokenType = (TokenType)_bufferedTokenType; _bufferedTokenType = null; if (_closers.Contains(CurrentTokenType)) { _tokenStack.Pop(); } return true; } // At this point we're passed a buffered token type so this read should unset this no matter what _bufferedTokenValue = null; if (!Advance()) { return false; } switch (_json.TokenType) { case JsonTokenType.PropertyName: CurrentTokenType = TokenType.FieldName; break; case JsonTokenType.None: break; case JsonTokenType.StartObject: HandleStartObject(); break; case JsonTokenType.EndObject: HandleEndObject(); break; case JsonTokenType.StartArray: _tokenStack.Push(TokenType.StartArray); CurrentTokenType = TokenType.StartArray; break; case JsonTokenType.EndArray: _tokenStack.Pop(); CurrentTokenType = TokenType.EndArray; break; case JsonTokenType.String: CurrentTokenType = TokenType.String; break; case JsonTokenType.True: CurrentTokenType = TokenType.True; break; case JsonTokenType.False: CurrentTokenType = TokenType.False; break; case JsonTokenType.Null: CurrentTokenType = TokenType.Null; break; case JsonTokenType.Comment: case JsonTokenType.Number: default: throw new SerializationException($"Unhandled JSON token type {_json.TokenType}."); } return true; } /// /// Gets the value of the current token. /// /// The value of the current token, or null if no value is associated with the token. /// Thrown when an error occurs during token value retrieval. public object? GetValue() { return CurrentTokenType switch { TokenType.FieldName or TokenType.String => GetString(), TokenType.Int => GetInt(), TokenType.Long => GetLong(), TokenType.Double => GetDouble(), TokenType.Date => GetDate(), TokenType.Time => GetTime(), TokenType.True or TokenType.False => GetBoolean(), TokenType.Module => GetModule(), TokenType.Bytes => GetBytes(), _ => throw new SerializationException($"{CurrentTokenType} does not have an associated value") }; } /// /// Retrieves a string value from the current token. /// /// A string representation of the current token's value. public string? GetString() { if (CurrentTokenType != TokenType.String && CurrentTokenType != TokenType.FieldName) { throw new InvalidOperationException($"Fauna token value isn't a {TokenType.String.ToString()} or a {TokenType.FieldName.ToString()}."); } try { if (_bufferedTokenValue != null) return (string)_bufferedTokenValue; return _json.GetString(); } catch (Exception e) { throw new SerializationException("Failed to get string", e); } } /// /// Retrieves a boolean value from the current JSON token. /// /// A boolean representation of the current token's value. public bool GetBoolean() { try { return _json.GetBoolean(); } catch (Exception e) { throw new SerializationException("Failed to get boolean", e); } } /// /// Retrieves a DateOnly value from the current token. /// /// A DateOnly representation of the current token's value. public DateOnly GetDate() { ValidateTaggedType(TokenType.Date); try { return DateOnly.Parse(_taggedTokenValue!); } catch (Exception e) { throw new SerializationException($"Failed to get date from {_taggedTokenValue}", e); } } /// /// Retrieves a float value from the current token. /// /// A float representation of the current token's value. public float GetFloat() { ValidateTaggedTypes(TokenType.Int, TokenType.Long, TokenType.Double); try { return float.Parse(_taggedTokenValue!, CultureInfo.InvariantCulture); } catch (Exception e) { throw new SerializationException($"Failed to get float from {_taggedTokenValue}", e); } } /// /// Retrieves a double value from the current token. /// /// A double representation of the current token's value. public double GetDouble() { ValidateTaggedTypes(TokenType.Int, TokenType.Long, TokenType.Double); try { return double.Parse(_taggedTokenValue!, CultureInfo.InvariantCulture); } catch (Exception e) { throw new SerializationException($"Failed to get double from {_taggedTokenValue}", e); } } /// /// Retrieves a decimal value from the current token. /// /// A decimal representation of the current token's value. public decimal GetDoubleAsDecimal() { ValidateTaggedType(TokenType.Double); try { return decimal.Parse(_taggedTokenValue!, CultureInfo.InvariantCulture); } catch (Exception e) { throw new SerializationException($"Failed to get decimal from {_taggedTokenValue}", e); } } /// /// Retrieves an byte value from the current token. /// /// A byte representation of the current token's value. public byte GetByte() { ValidateTaggedTypes(TokenType.Int); try { return byte.Parse(_taggedTokenValue!); } catch (Exception e) { throw new SerializationException($"Failed to get byte from {_taggedTokenValue}", e); } } /// /// Retrieves a byte array value from the current token. /// /// A byte array representation of the current token's value. public byte[] GetBytes() { ValidateTaggedTypes(TokenType.Bytes); try { return Convert.FromBase64String(_taggedTokenValue!); } catch (Exception e) { throw new SerializationException($"Failed to get byte array from {_taggedTokenValue}", e); } } /// /// Retrieves an unsigned byte value from the current token. /// /// An unsigned byte representation of the current token's value. public sbyte GetUnsignedByte() { ValidateTaggedTypes(TokenType.Int); try { return sbyte.Parse(_taggedTokenValue!); } catch (Exception e) { throw new SerializationException($"Failed to get sbyte from {_taggedTokenValue}", e); } } /// /// Retrieves an integer value from the current token. /// /// An integer representation of the current token's value. public int GetInt() { ValidateTaggedTypes(TokenType.Int, TokenType.Long); try { return int.Parse(_taggedTokenValue!); } catch (Exception e) { throw new SerializationException($"Failed to get int from {_taggedTokenValue}", e); } } /// /// Retrieves an unsigned integer value from the current token. /// /// An unsigned integer representation of the current token's value. public uint GetUnsignedInt() { ValidateTaggedTypes(TokenType.Int, TokenType.Long); try { return uint.Parse(_taggedTokenValue!); } catch (Exception e) { throw new SerializationException($"Failed to get uint from {_taggedTokenValue}", e); } } /// /// Retrieves an short value from the current token. /// /// An short representation of the current token's value. public short GetShort() { ValidateTaggedTypes(TokenType.Int, TokenType.Long); try { return short.Parse(_taggedTokenValue!); } catch (Exception e) { throw new SerializationException($"Failed to get short from {_taggedTokenValue}", e); } } /// /// Retrieves an unsigned short value from the current token. /// /// An unsigned short representation of the current token's value. public ushort GetUnsignedShort() { ValidateTaggedTypes(TokenType.Int, TokenType.Long); try { return ushort.Parse(_taggedTokenValue!); } catch (Exception e) { throw new SerializationException($"Failed to get ushort from {_taggedTokenValue}", e); } } /// /// Retrieves a long value from the current token. /// /// A long representation of the current token's value. public long GetLong() { ValidateTaggedTypes(TokenType.Int, TokenType.Long); try { return long.Parse(_taggedTokenValue!); } catch (Exception e) { throw new SerializationException($"Failed to get long from {_taggedTokenValue}", e); } } /// /// Retrieves a Module object from the current token. /// /// A Module representation of the current token's value. public Module GetModule() { ValidateTaggedType(TokenType.Module); return new Module(_taggedTokenValue!); } /// /// Retrieves an EventSource token string from the current token. /// /// A EventSource of the current token's value. public EventSource GetEventSource() { ValidateTaggedType(TokenType.EventSource); return new EventSource(_taggedTokenValue!); } /// /// Retrieves a DateTime value from the current token. /// /// A DateTime representation of the current token's value. public DateTime GetTime() { ValidateTaggedType(TokenType.Time); try { return DateTime.Parse(_taggedTokenValue!); } catch (Exception e) { throw new SerializationException($"Failed to get time from {_taggedTokenValue}", e); } } /// /// Tries to retrieve a string value from the current token. /// /// When this method returns, contains the string value, if the conversion succeeded, or null if the conversion failed. /// true if the token's value could be converted to a string; otherwise, false. public string TryGetString(out string value) { throw new NotImplementedException(); } /// /// Tries to retrieve a boolean value from the current token. /// /// When this method returns, contains the boolean value, if the conversion succeeded, or false if the conversion failed. /// true if the token's value could be converted to a boolean; otherwise, false. public bool TryGetBoolean(out bool value) { throw new NotImplementedException(); } /// /// Tries to retrieve a DateTime value from the current token. /// /// When this method returns, contains the DateTime value, if the conversion succeeded, or the default DateTime value if the conversion failed. /// true if the token's value could be converted to a DateTime; otherwise, false. public DateTime TryGetDateTime(out DateTime value) { throw new NotImplementedException(); } /// /// Tries to retrieve a double value from the current token. /// /// When this method returns, contains the double value, if the conversion succeeded, or 0.0 if the conversion failed. /// true if the token's value could be converted to a double; otherwise, false. public double TryGetDouble(out double value) { throw new NotImplementedException(); } /// /// Tries to retrieve an integer value from the current token. /// /// When this method returns, contains the integer value, if the conversion succeeded, or 0 if the conversion failed. /// true if the token's value could be converted to an integer; otherwise, false. public int TryGetInt(out int value) { throw new NotImplementedException(); } /// /// Tries to retrieve a long value from the current token. /// /// When this method returns, contains the long value, if the conversion succeeded, or 0 if the conversion failed. /// true if the token's value could be converted to a long; otherwise, false. public long TryGetLong(out long value) { throw new NotImplementedException(); } /// /// Tries to retrieve a Module object from the current token. /// /// When this method returns, contains the Module object, if the conversion succeeded, or null if the conversion failed. /// true if the token's value could be converted to a Module; otherwise, false. public Module TryGetModule(out Module value) { throw new NotImplementedException(); } private void ValidateTaggedType(TokenType type) { if (CurrentTokenType != type || _taggedTokenValue == null || _taggedTokenValue.GetType() != typeof(string)) { throw new InvalidOperationException($"CurrentTokenType is a {CurrentTokenType.ToString()}, not a {type.ToString()}."); } } private void ValidateTaggedTypes(params TokenType[] types) { if (!types.Contains(CurrentTokenType) || _taggedTokenValue == null || _taggedTokenValue.GetType() != typeof(string)) { throw new InvalidOperationException($"CurrentTokenType is a {CurrentTokenType.ToString()}, not in {types}."); } } private void HandleStartObject() { AdvanceTrue(); switch (_json.TokenType) { case JsonTokenType.PropertyName: switch (_json.GetString()) { case "@date": HandleTaggedString(TokenType.Date); break; case "@doc": AdvanceTrue(); CurrentTokenType = TokenType.StartDocument; _tokenStack.Push(TokenType.StartDocument); break; case "@double": HandleTaggedString(TokenType.Double); break; case "@int": HandleTaggedString(TokenType.Int); break; case "@long": HandleTaggedString(TokenType.Long); break; case "@mod": HandleTaggedString(TokenType.Module); break; case "@stream": HandleTaggedString(TokenType.EventSource); break; case "@object": AdvanceTrue(); CurrentTokenType = TokenType.StartObject; _tokenStack.Push(TokenTypeInternal.StartEscapedObject); break; case "@ref": AdvanceTrue(); CurrentTokenType = TokenType.StartRef; _tokenStack.Push(TokenType.StartRef); break; case "@set": AdvanceTrue(); CurrentTokenType = TokenType.StartPage; if (_json.TokenType == JsonTokenType.String) { _bufferedTokenValue = _json.GetString(); _bufferedTokenType = TokenType.String; _tokenStack.Push(TokenTypeInternal.StartPageUnmaterialized); } else { _tokenStack.Push(TokenType.StartPage); } break; case "@time": HandleTaggedString(TokenType.Time); break; case "@bytes": HandleTaggedString(TokenType.Bytes); break; default: _bufferedTokenType = TokenType.FieldName; _tokenStack.Push(TokenType.StartObject); CurrentTokenType = TokenType.StartObject; break; } break; case JsonTokenType.EndObject: _bufferedTokenType = TokenType.EndObject; _tokenStack.Push(TokenType.StartObject); CurrentTokenType = TokenType.StartObject; break; default: throw new SerializationException($"Unexpected token following StartObject: {_json.TokenType}"); } } private void HandleEndObject() { var startToken = _tokenStack.Pop(); switch (startToken) { case TokenType.StartDocument: CurrentTokenType = TokenType.EndDocument; AdvanceTrue(); break; case TokenType.StartPage: CurrentTokenType = TokenType.EndPage; AdvanceTrue(); break; case TokenTypeInternal.StartPageUnmaterialized: CurrentTokenType = TokenType.EndPage; break; case TokenType.StartRef: CurrentTokenType = TokenType.EndRef; AdvanceTrue(); break; case TokenTypeInternal.StartEscapedObject: CurrentTokenType = TokenType.EndObject; AdvanceTrue(); break; case TokenType.StartObject: CurrentTokenType = TokenType.EndObject; break; default: throw new SerializationException($"Unexpected token {startToken}. This might be a bug."); } } /// /// Method HandleTaggedString is used to advance through a JSON object that represents a tagged type with a /// a string value. For example: /// /// * Given { "@int": "123" } /// * Read JSON until JsonTokenType.PropertyName and you've determined it's an int /// * Call HandleTaggedString(TokenType.Int) /// * The underlying JSON reader is advanced until JsonTokenType.EndObject /// * Access the int via GetInt() /// /// private void HandleTaggedString(TokenType token) { AdvanceTrue(); CurrentTokenType = token; _taggedTokenValue = _json.GetString(); AdvanceTrue(); } private bool Advance() { try { return _json.Read(); } catch (Exception e) { throw new SerializationException("Failed to advance underlying JSON reader.", e); } } private void AdvanceTrue() { if (!Advance()) { throw new SerializationException("Unexpected end of underlying JSON reader."); } } } ``` ## File: Fauna/Serialization/Utf8FaunaWriter.cs ```csharp using System.Buffers; using System.Globalization; using System.Text.Json; using Fauna.Types; using Stream = System.IO.Stream; namespace Fauna.Serialization; /// /// Provides functionality for writing data in a streaming manner to a buffer or a stream. /// public sealed class Utf8FaunaWriter : IAsyncDisposable, IDisposable { private readonly Utf8JsonWriter _writer; /// /// Initializes a new instance of the Utf8FaunaWriter class with a specified buffer writer. /// /// The buffer writer to write to. public Utf8FaunaWriter(IBufferWriter bufferWriter) { _writer = new Utf8JsonWriter(bufferWriter); } /// /// Initializes a new instance of the Utf8FaunaWriter class with a specified stream. /// /// The stream to write to. public Utf8FaunaWriter(Stream stream) { _writer = new Utf8JsonWriter(stream); } /// /// Flushes the written data to the underlying buffer or stream. /// public void Flush() { _writer.Flush(); } /// /// Asynchronously flushes the written data to the underlying buffer or stream. /// public async ValueTask FlushAsync() { await _writer.FlushAsync(); } /// /// Disposes the underlying writer. /// public void Dispose() { _writer.Dispose(); } /// /// Asynchronously disposes the underlying writer. /// public async ValueTask DisposeAsync() { await _writer.DisposeAsync(); } /// /// Writes the beginning of an object. /// public void WriteStartObject() { _writer.WriteStartObject(); } /// /// Writes the end of an object. /// public void WriteEndObject() { _writer.WriteEndObject(); } /// /// Writes the beginning of a specially tagged object. /// public void WriteStartEscapedObject() { _writer.WriteStartObject(); WriteFieldName("@object"); _writer.WriteStartObject(); } /// /// Writes the end of a specially tagged object. /// public void WriteEndEscapedObject() { _writer.WriteEndObject(); _writer.WriteEndObject(); } /// /// Writes the beginning of an array. /// public void WriteStartArray() { _writer.WriteStartArray(); } /// /// Writes the end of an array. /// public void WriteEndArray() { _writer.WriteEndArray(); } /// /// Writes the beginning of a reference object. /// public void WriteStartRef() { _writer.WriteStartObject(); WriteFieldName("@ref"); _writer.WriteStartObject(); } /// /// Writes the end of a reference object. /// public void WriteEndRef() { _writer.WriteEndObject(); _writer.WriteEndObject(); } /// /// Writes a double value with a specific field name. /// /// The name of the field. /// The decimal value to write. public void WriteDouble(string fieldName, decimal value) { WriteFieldName(fieldName); WriteDoubleValue(value); } /// /// Writes a double value with a specific field name. /// /// The name of the field. /// The double value to write. public void WriteDouble(string fieldName, double value) { WriteFieldName(fieldName); WriteDoubleValue(value); } /// /// Writes an integer value with a specific field name. /// /// The name of the field. /// The integer value to write. public void WriteInt(string fieldName, int value) { WriteFieldName(fieldName); WriteIntValue(value); } /// /// Writes a long integer value with a specific field name. /// /// The name of the field. /// The long integer value to write. public void WriteLong(string fieldName, long value) { WriteFieldName(fieldName); WriteLongValue(value); } /// /// Writes a byte array value with a specific field name. /// /// The name of the field. /// The byte array value to write. public void WriteBytes(string fieldName, byte[] value) { WriteFieldName(fieldName); WriteBytesValue(value); } /// /// Writes a string value with a specific field name. /// /// The name of the field. /// The string value to write. public void WriteString(string fieldName, string value) { WriteFieldName(fieldName); WriteStringValue(value); } /// /// Writes a date value with a specific field name. /// /// The name of the field. /// The DateTime value to write. public void WriteDate(string fieldName, DateTime value) { WriteFieldName(fieldName); WriteDateValue(value); } /// /// Writes a time value with a specific field name. /// /// The name of the field. /// The DateTime value to write. public void WriteTime(string fieldName, DateTime value) { WriteFieldName(fieldName); WriteTimeValue(value); } /// /// Writes a boolean value with a specific field name. /// /// The name of the field. /// The boolean value to write. public void WriteBoolean(string fieldName, bool value) { WriteFieldName(fieldName); WriteBooleanValue(value); } /// /// Writes a null value with a specific field name. /// /// The name of the field. public void WriteNull(string fieldName) { WriteFieldName(fieldName); WriteNullValue(); } /// /// Writes a module value with a specific field name. /// /// The name of the field. /// The module value to write. public void WriteModule(string fieldName, Module value) { WriteFieldName(fieldName); WriteModuleValue(value); } /// /// Writes a field name for the next value. /// /// The name of the field. public void WriteFieldName(string value) { _writer.WritePropertyName(value); } /// /// Writes a tagged value in an object. /// /// The tag to use for the value. /// The value associated with the tag. public void WriteTaggedValue(string tag, string value) { WriteStartObject(); WriteString(tag, value); WriteEndObject(); } /// /// Writes a double value as a tagged element. /// /// The double value to write. public void WriteDoubleValue(decimal value) { WriteTaggedValue("@double", value.ToString(CultureInfo.InvariantCulture)); } /// /// Writes a double value as a tagged element. /// /// The double value to write. public void WriteDoubleValue(double value) { WriteTaggedValue("@double", value.ToString(CultureInfo.InvariantCulture)); } /// /// Writes an integer value as a tagged element. /// /// The integer value to write. public void WriteIntValue(int value) { WriteTaggedValue("@int", value.ToString()); } /// /// Writes a byte array value as a tagged element. /// /// The byte array value to write. public void WriteBytesValue(byte[] value) { WriteTaggedValue("@bytes", Convert.ToBase64String(value)); } /// /// Writes a long integer value as a tagged element. /// /// The long integer value to write. public void WriteLongValue(long value) { WriteTaggedValue("@long", value.ToString()); } /// /// Writes a string value as a tagged element. /// /// The string value to write. public void WriteStringValue(string value) { _writer.WriteStringValue(value); } /// /// Writes a date value as a tagged element. /// /// The date value to write. public void WriteDateValue(DateTime value) { var str = value.ToString("yyyy-MM-dd"); WriteTaggedValue("@date", str); } /// /// Writes a date value as a tagged element. /// /// The date value to write. public void WriteDateValue(DateOnly value) { var str = value.ToString("yyyy-MM-dd"); WriteTaggedValue("@date", str); } /// /// Writes a date value as a tagged element. /// /// The date value to write. public void WriteDateValue(DateTimeOffset value) { var str = value.ToString("yyyy-MM-dd"); WriteTaggedValue("@date", str); } /// /// Writes a date value as a tagged element. /// /// The date value to write. public void WriteTimeValue(DateTime value) { var str = value.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); WriteTaggedValue("@time", str); } /// /// Writes a date value as a tagged element. /// /// The date value to write. public void WriteTimeValue(DateTimeOffset value) { var str = value.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); WriteTaggedValue("@time", str); } /// /// Writes a boolean value to the stream. /// /// The boolean value to write. public void WriteBooleanValue(bool value) { _writer.WriteBooleanValue(value); } /// /// Writes a null value to the stream. /// public void WriteNullValue() { _writer.WriteNullValue(); } /// /// Writes a module value as a tagged element. /// /// The module value to write. public void WriteModuleValue(Module value) { WriteTaggedValue("@mod", value.Name); } } ``` ## File: Fauna/Types/BaseRef.cs ```csharp using Fauna.Linq; namespace Fauna.Types; /// /// An abstract class representing a reference that can wrap an instance of the referenced document. /// /// The referenced document type. public abstract class BaseRef { /// /// Gets the materialized document represented by the Ref. Is null unless IsLoaded is true /// and Exists is true. /// protected readonly T? Doc; /// /// Gets the cause when exists is false. Is null unless IsLoaded is true and Exists is false. /// public string? Cause { get; } /// /// Gets the collection to which the ref belongs. /// public Module Collection { get; } /// /// Gets a boolean indicating whether the doc exists. Is null unless IsLoaded is true. /// public bool? Exists { get; } /// /// Gets a boolean indicating whether the document represented by the ref has been loaded. /// public bool IsLoaded { get; } internal BaseRef(DataContext.ICollection col) { Collection = new Module(col.Name); } internal BaseRef(DataContext.ICollection col, T doc) { Collection = new Module(col.Name); Doc = doc; IsLoaded = true; Exists = true; } internal BaseRef(DataContext.ICollection col, string cause) { Collection = new Module(col.Name); Exists = false; Cause = cause; IsLoaded = true; } internal BaseRef(Module coll) { Collection = coll; } internal BaseRef(Module coll, T doc) { Collection = coll; Exists = true; Doc = doc; IsLoaded = true; } internal BaseRef(Module coll, string cause) { Collection = coll; Exists = false; Cause = cause; IsLoaded = true; } /// /// Gets the underlying document if it's loaded and exists. /// /// An instance of . public abstract T Get(); } internal class UnloadedRefException : Exception { } ``` ## File: Fauna/Types/BaseRefBuilder.cs ```csharp namespace Fauna.Types; internal class BaseRefBuilder { public string? Id { get; set; } public string? Name { get; set; } public Module? Collection { get; set; } public string? Cause { get; set; } public bool? Exists { get; set; } public T? Doc { get; set; } public BaseRef Build() { if (Collection is null) throw new ArgumentNullException(nameof(Collection)); if (Id is not null) { if (Exists != null && !Exists.Value) return new Ref(Id, Collection, Cause ?? ""); if (Doc != null) return new Ref(Id, Collection, Doc); return new Ref(Id, Collection); } if (Name is not null) { if (Exists != null && !Exists.Value) return new NamedRef(Name, Collection, Cause ?? ""); if (Doc != null) return new NamedRef(Name, Collection, Doc); return new NamedRef(Name, Collection); } throw new ArgumentException("Id and Name cannot both be null"); } } ``` ## File: Fauna/Types/Event.cs ```csharp using System.Text.Json; using Fauna.Core; using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Serialization; using static Fauna.Core.ResponseFields; namespace Fauna.Types; /// /// An enum representing Fauna event types. /// public enum EventType { /// /// An add event. Emitted when a document is added to the tracked Set. /// Add, /// /// An update event. Emitted when a document is updated in the tracked Set. /// Update, /// /// A remove event. Emitted when a document is removed from the tracked Set. /// Remove, /// /// A status event. Typically used as an implementation detail for a driver. Indicates a status change on the stream. Not emitted for event feeds. /// Status } /// /// A class representing an event from an event feed or event stream. /// /// public class Event where T : notnull { /// /// The type of the event. /// public EventType Type { get; private init; } /// /// The transaction time of the event. /// public long TxnTime { get; private init; } /// /// A cursor for the event. Used to resume an event feed or event stream after the event. /// public string Cursor { get; private init; } = null!; /// /// Document data for the event. /// public T? Data { get; private init; } /// /// Query stats related to the event. /// public QueryStats Stats { get; private init; } /// /// A helper method for converting a JSON element into an Event. /// /// JSON Element to convert. /// A mapping context to influence deserialization. /// An instance of . /// Thrown when the event includes a Fauna error. internal static Event From(JsonElement json, MappingContext ctx) { var err = GetError(json); if (err != null) { throw new EventException(err.Value); } var evt = new Event { TxnTime = GetTxnTime(json), Cursor = GetCursor(json), Type = GetType(json), Stats = GetStats(json), Data = GetData(json, ctx), }; return evt; } /// /// A helper method for converting a JSON string into an Event. /// /// The string of raw JSON. /// A mapping context to influence deserialization. /// An instance of . /// Thrown when the event includes a Fauna error. internal static Event From(string body, MappingContext ctx) { var json = JsonSerializer.Deserialize(body); return From(json, ctx); } private static long GetTxnTime(JsonElement json) { if (!json.TryGetProperty(LastSeenTxnFieldName, out var elem)) { return default; } return elem.TryGetInt64(out long i) ? i : default; } private static string GetCursor(JsonElement json) { if (!json.TryGetProperty(CursorFieldName, out var elem)) { throw new InvalidDataException($"Missing required field: cursor - {json.ToString()}"); } return elem.Deserialize()!; } private static EventType GetType(JsonElement json) { if (!json.TryGetProperty("type", out var elem)) { throw new InvalidDataException($"Missing required field: type - {json.ToString()}"); } string? evtType = elem.Deserialize(); EventType type = evtType switch { "add" => EventType.Add, "update" => EventType.Update, "remove" => EventType.Remove, "status" => EventType.Status, _ => throw new InvalidOperationException($"Unknown event type: {evtType}") }; return type; } private static QueryStats GetStats(JsonElement json) { return json.TryGetProperty(StatsFieldName, out var elem) ? elem.Deserialize() : default; } private static T? GetData(JsonElement json, MappingContext ctx) { if (!json.TryGetProperty(DataFieldName, out var elem)) { return default; } var reader = new Utf8FaunaReader(elem.GetRawText()); reader.Read(); return Serializer.Generate(ctx).Deserialize(ctx, ref reader); } private static ErrorInfo? GetError(JsonElement json) { return json.TryGetProperty(ErrorFieldName, out var elem) ? elem.Deserialize() : null; } } ``` ## File: Fauna/Types/EventSource.cs ```csharp using System.Text.Json; namespace Fauna.Types; /// /// Represents a Fauna EventSource for initializing Streams and Feeds. /// public sealed class EventSource : IEquatable { /// /// Gets the string value of the stream token. /// internal string Token { get; } internal EventOptions Options { get; set; } /// /// Initializes an . /// /// An event source. public EventSource(string token) { Token = token; Options = new EventOptions(); } /// /// Serializes the event source to the provided . /// /// public void Serialize(Stream stream) { var writer = new Utf8JsonWriter(stream); writer.WriteStartObject(); writer.WriteString("token", Token); if (Options.Cursor != null) { writer.WriteString("cursor", Options.Cursor); } else if (Options.StartTs != null) { writer.WriteNumber("start_ts", Options.StartTs.Value); } if (Options.PageSize is > 0) { writer.WriteNumber("page_size", Options.PageSize.Value); } writer.WriteEndObject(); writer.Flush(); } /// /// Determines whether the specified Stream is equal to the current Stream. /// /// The Stream to compare with the current Stream. /// true if the specified Stream is equal to the current Stream; otherwise, false. public bool Equals(EventSource? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return Token == other.Token; } /// /// Determines whether the specified object is equal to the current Stream. /// /// The object to compare with the current Stream. /// true if the specified object is equal to the current Stream; otherwise, false. public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; return Equals((EventSource)obj); } /// /// The default hash function. /// /// A hash code for the current Stream. public override int GetHashCode() { return Token.GetHashCode(); } } /// /// Represents the options for a Fauna EventSource. /// public class EventOptions { /// /// Cursor returned from Fauna /// /// public string? Cursor { get; internal set; } /// /// Start timestamp returned for the feed. Used to resume the Feed. /// public long? StartTs { get; protected init; } /// /// Limit page size for the Feed /// public int? PageSize { get; protected init; } } ``` ## File: Fauna/Types/Module.cs ```csharp namespace Fauna.Types; /// /// Represents a module, a singleton object grouping related functionalities. /// Modules are serialized as \@mod values in tagged formats, organizing and encapsulating specific functionalities. /// public sealed class Module : IEquatable { /// /// Gets the name of the module. The name is used to identify and reference the module. /// public string Name { get; } /// /// Initializes a new instance of the Module class with the specified name. /// /// The name of the module. public Module(string name) { Name = name; } /// /// Determines whether the specified Module is equal to the current Module. /// /// The Module to compare with the current Module. /// true if the specified Module is equal to the current Module; otherwise, false. public bool Equals(Module? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return Name == other.Name; } /// /// Determines whether the specified object is equal to the current Module. /// /// The object to compare with the current Module. /// true if the specified object is equal to the current Module; otherwise, false. public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != GetType()) return false; return Equals((Module)obj); } /// /// The default hash function. /// /// A hash code for the current Module. public override int GetHashCode() { return Name.GetHashCode(); } } ``` ## File: Fauna/Types/NamedRef.cs ```csharp using Fauna.Exceptions; using Fauna.Linq; namespace Fauna.Types; /// /// Represents a document ref that has a "name" instead of an "id". For example, a Role document reference is /// represented as a NamedRef. /// public class NamedRef : BaseRef { /// /// Gets the string value of the ref name. /// public string Name { get; } /// /// Initializes a new instance of an unloaded class. /// /// The name of the document. /// The collection to which the document belongs. public NamedRef(string name, DataContext.ICollection col) : base(col) { Name = name; } /// /// Initializes a new instance of a loaded class. /// /// The name of the document. /// The collection to which the document belongs. /// The instance of referenced. public NamedRef(string name, DataContext.ICollection col, T doc) : base(col, doc) { Name = name; } /// /// Initializes a new instance of a loaded and non-existent class. /// /// The name of the document. /// The collection to which the document belongs. /// A string representing the cause for non-existence. public NamedRef(string name, DataContext.ICollection col, string cause) : base(col, cause) { Name = name; } /// /// Initializes a new instance of an unloaded class. /// /// The name of the document. /// The collection to which the document belongs. public NamedRef(string name, Module col) : base(col) { Name = name; } /// /// Initializes a new instance of a loaded and non-existent class. /// /// The name of the document. /// The collection to which the document belongs. /// A string representing the cause for non-existence. public NamedRef(string name, Module col, string cause) : base(col, cause) { Name = name; } /// /// Initializes a new instance of a loaded class. /// /// The name of the document. /// The collection to which the document belongs. /// The instance of referenced. public NamedRef(string name, Module col, T doc) : base(col, doc) { Name = name; } /// /// Thrown when IsLoaded is false. /// Thrown when Exists is false. public override T Get() { if (!IsLoaded) throw new UnloadedRefException(); if (Exists.HasValue && !Exists.Value) throw new NullDocumentException(null, Name, Collection, Cause ?? ""); return Doc!; } } ``` ## File: Fauna/Types/Page.cs ```csharp namespace Fauna.Types; /// /// Represents a page in a dataset for pagination. /// /// The type of data contained in the page. /// The of data items on the page. /// The cursor for the next page, if available. /// /// Used for segmenting large datasets into pages with cursors to navigate between them. /// public record Page(IReadOnlyList Data, string? After); ``` ## File: Fauna/Types/Ref.cs ```csharp using Fauna.Exceptions; using Fauna.Linq; namespace Fauna.Types; /// /// Represents a document ref. /// public class Ref : BaseRef { /// /// Gets the string value of the ref ID. /// public string Id { get; } /// /// Initializes a new instance of an unloaded class. /// /// The ID of the document. /// The collection to which the document belongs. public Ref(string id, DataContext.ICollection col) : base(col) { Id = id; } /// /// Initializes a new instance of a loaded class. /// /// The ID of the document. /// The collection to which the document belongs. /// The instance of referenced. public Ref(string id, DataContext.ICollection col, T doc) : base(col, doc) { Id = id; } /// /// Initializes a new instance of a loaded and non-existent class. /// /// The ID of the document. /// The collection to which the document belongs. /// A string representing the cause for non-existence. public Ref(string id, DataContext.ICollection col, string cause) : base(col, cause) { Id = id; } /// /// Initializes a new instance of an unloaded class. /// /// The ID of the document. /// The collection to which the document belongs. public Ref(string id, Module col) : base(col) { Id = id; } /// /// Initializes a new instance of a loaded and non-existent class. /// /// The ID of the document. /// The collection to which the document belongs. /// A string representing the cause for non-existence. public Ref(string id, Module col, string cause) : base(col, cause) { Id = id; } /// /// Initializes a new instance of a loaded class. /// /// The ID of the document. /// The collection to which the document belongs. /// The instance of referenced. public Ref(string id, Module col, T doc) : base(col, doc) { Id = id; } /// /// Thrown when IsLoaded is false. /// Thrown when Exists is false. public override T Get() { if (!IsLoaded) throw new UnloadedRefException(); if (Exists.HasValue && !Exists.Value) throw new NullDocumentException(Id, null, Collection, Cause ?? ""); return Doc!; } } ``` ## File: Fauna/Util/Extensions/PaginationExtensions.cs ```csharp using Fauna.Types; namespace Fauna.Util.Extensions; /// /// Provides extension methods for pagination. /// public static class PaginationExtensions { /// /// Flattens all pages into a stream of items. /// /// Data type. /// Pages to flatten. /// An that enumerates each item across all pages. public static async IAsyncEnumerable FlattenAsync(this IAsyncEnumerable> pages) { await foreach (var page in pages) { foreach (var item in page.Data) { yield return item; } } } } ``` ## File: Fauna/Util/Extensions/TypeExtensions.cs ```csharp using System.Reflection; using System.Runtime.CompilerServices; namespace Fauna.Util.Extensions; internal static class TypeExtensions { public static bool IsClosureType(this Type ty) { var compilerGen = ty.GetCustomAttribute() != null; // check for the closure class name pattern. see // https://stackoverflow.com/questions/2508828/where-to-learn-about-vs-debugger-magic-names/2509524#2509524 var dcName = ty.Name.StartsWith("<>c__DisplayClass"); return compilerGen && dcName; } public static Type? GetGenInst(this Type ty, Type genTy) { if (!genTy.IsGenericTypeDefinition) { throw new ArgumentException($"{nameof(genTy)} is not a generic type definition."); } if (genTy.IsInterface) { foreach (var iface in ty.GetInterfaces()) { if (iface.IsGenericType && iface.GetGenericTypeDefinition() == genTy) { return iface; } } } else { Type? curr = ty; while (curr is not null) { if (curr.IsGenericType && curr.GetGenericTypeDefinition() == genTy) { return curr; } curr = curr.BaseType; } } return null; } } ``` ## File: Fauna/Util/Expressions.cs ```csharp using System.Linq.Expressions; namespace Fauna.Util; internal static class Expressions { public static (Expression, Expression[], bool) GetCalleeAndArgs(MethodCallExpression expr) => expr.Object switch { null => (expr.Arguments.First(), expr.Arguments.Skip(1).ToArray(), true), var c => (c, expr.Arguments.ToArray(), false), }; public static LambdaExpression? UnwrapLambda(Expression expr) => expr.NodeType switch { ExpressionType.Lambda => (LambdaExpression)expr, ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.Quote => UnwrapLambda(((UnaryExpression)expr).Operand), _ => null, }; } ``` ## File: Fauna/Util/ExpressionSwitch.cs ```csharp using System.Linq.Expressions; namespace Fauna.Util; internal abstract class ExpressionSwitch { // if true, will transparently handle certain node types (Quote, Convert, etc.) protected virtual bool Simplified { get => true; } public IEnumerable ApplyAll(IEnumerable exprs) => exprs.Select(e => Apply(e)); // Apply this switch to an expression public TResult Apply(Expression? expr) { if (expr is null) return NullExpr(); return expr.NodeType switch { ExpressionType.Add or ExpressionType.AddAssign or ExpressionType.AddAssignChecked or ExpressionType.AddChecked or ExpressionType.And or ExpressionType.AndAssign or ExpressionType.AndAlso or ExpressionType.ArrayIndex or ExpressionType.Assign or ExpressionType.Coalesce or ExpressionType.Divide or ExpressionType.DivideAssign or ExpressionType.Equal or ExpressionType.ExclusiveOr or ExpressionType.ExclusiveOrAssign or ExpressionType.GreaterThan or ExpressionType.GreaterThanOrEqual or ExpressionType.LeftShift or ExpressionType.LeftShiftAssign or ExpressionType.LessThan or ExpressionType.LessThanOrEqual or ExpressionType.Modulo or ExpressionType.ModuloAssign or ExpressionType.Multiply or ExpressionType.MultiplyAssign or ExpressionType.MultiplyAssignChecked or ExpressionType.MultiplyChecked or ExpressionType.NotEqual or ExpressionType.Or or ExpressionType.OrAssign or ExpressionType.OrElse or ExpressionType.Power or ExpressionType.PowerAssign or ExpressionType.RightShift or ExpressionType.RightShiftAssign or ExpressionType.Subtract or ExpressionType.SubtractAssign or ExpressionType.SubtractAssignChecked or ExpressionType.SubtractChecked => BinaryExpr((BinaryExpression)expr), ExpressionType.Block => BlockExpr((BlockExpression)expr), ExpressionType.Call => CallExpr((MethodCallExpression)expr), ExpressionType.Conditional => ConditionalExpr((ConditionalExpression)expr), ExpressionType.Constant => ConstantExpr((ConstantExpression)expr), ExpressionType.DebugInfo => DebugInfoExpr((DebugInfoExpression)expr), ExpressionType.Default => DefaultExpr((DefaultExpression)expr), ExpressionType.Dynamic => DynamicExpr((DynamicExpression)expr), ExpressionType.Goto => GotoExpr((GotoExpression)expr), ExpressionType.Index => IndexExpr((IndexExpression)expr), ExpressionType.Invoke => InvokeExpr((InvocationExpression)expr), ExpressionType.Label => LabelExpr((LabelExpression)expr), ExpressionType.Lambda => LambdaExpr((LambdaExpression)expr), ExpressionType.Loop => LoopExpr((LoopExpression)expr), ExpressionType.ListInit => ListInitExpr((ListInitExpression)expr), ExpressionType.MemberAccess => MemberAccessExpr((MemberExpression)expr), ExpressionType.MemberInit => MemberInitExpr((MemberInitExpression)expr), ExpressionType.New => NewExpr((NewExpression)expr), ExpressionType.NewArrayBounds or ExpressionType.NewArrayInit => NewArrayExpr((NewArrayExpression)expr), ExpressionType.Parameter => ParameterExpr((ParameterExpression)expr), ExpressionType.RuntimeVariables => RuntimeVariablesExpr((RuntimeVariablesExpression)expr), ExpressionType.Switch => SwitchExpr((SwitchExpression)expr), ExpressionType.Try => TryExpr((TryExpression)expr), ExpressionType.TypeEqual or ExpressionType.TypeIs => TypeBinaryExpr((TypeBinaryExpression)expr), ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.Quote when Simplified => Apply(((UnaryExpression)expr).Operand), ExpressionType.ArrayLength or ExpressionType.Convert or ExpressionType.ConvertChecked or ExpressionType.Decrement or ExpressionType.Increment or ExpressionType.IsFalse or ExpressionType.IsTrue or ExpressionType.Negate or ExpressionType.NegateChecked or ExpressionType.Not or ExpressionType.OnesComplement or ExpressionType.PostDecrementAssign or ExpressionType.PostIncrementAssign or ExpressionType.PreDecrementAssign or ExpressionType.PreIncrementAssign or ExpressionType.Quote or ExpressionType.Throw or ExpressionType.TypeAs or ExpressionType.UnaryPlus or ExpressionType.Unbox => UnaryExpr((UnaryExpression)expr), // not sure what to do with this one ExpressionType.Extension => UnknownExpr(expr) }; } protected abstract TResult NullExpr(); protected abstract TResult BinaryExpr(BinaryExpression expr); protected abstract TResult BlockExpr(BlockExpression expr); protected abstract TResult ConditionalExpr(ConditionalExpression expr); protected abstract TResult CallExpr(MethodCallExpression expr); protected abstract TResult ConstantExpr(ConstantExpression expr); protected abstract TResult DebugInfoExpr(DebugInfoExpression expr); protected abstract TResult DefaultExpr(DefaultExpression expr); protected abstract TResult DynamicExpr(DynamicExpression expr); protected abstract TResult GotoExpr(GotoExpression expr); protected abstract TResult IndexExpr(IndexExpression expr); protected abstract TResult InvokeExpr(InvocationExpression expr); protected abstract TResult LabelExpr(LabelExpression expr); protected abstract TResult LambdaExpr(LambdaExpression expr); protected abstract TResult ListInitExpr(ListInitExpression expr); protected abstract TResult LoopExpr(LoopExpression expr); protected abstract TResult MemberAccessExpr(MemberExpression expr); protected abstract TResult MemberInitExpr(MemberInitExpression expr); protected abstract TResult NewArrayExpr(NewArrayExpression expr); protected abstract TResult NewExpr(NewExpression expr); protected abstract TResult ParameterExpr(ParameterExpression expr); protected abstract TResult RuntimeVariablesExpr(RuntimeVariablesExpression expr); protected abstract TResult SwitchExpr(SwitchExpression expr); protected abstract TResult TryExpr(TryExpression expr); protected abstract TResult TypeBinaryExpr(TypeBinaryExpression expr); protected abstract TResult UnaryExpr(UnaryExpression expr); protected abstract TResult UnknownExpr(Expression expr); } internal class DefaultExpressionSwitch : ExpressionSwitch { protected virtual TResult ApplyDefault(Expression? expr) => throw new NotSupportedException($"Unsupported expression: {expr}"); protected override TResult NullExpr() => ApplyDefault(null); protected override TResult BinaryExpr(BinaryExpression expr) => ApplyDefault(expr); protected override TResult BlockExpr(BlockExpression expr) => ApplyDefault(expr); protected override TResult ConditionalExpr(ConditionalExpression expr) => ApplyDefault(expr); protected override TResult CallExpr(MethodCallExpression expr) => ApplyDefault(expr); protected override TResult ConstantExpr(ConstantExpression expr) => ApplyDefault(expr); protected override TResult DebugInfoExpr(DebugInfoExpression expr) => ApplyDefault(expr); protected override TResult DefaultExpr(DefaultExpression expr) => ApplyDefault(expr); protected override TResult DynamicExpr(DynamicExpression expr) => ApplyDefault(expr); protected override TResult GotoExpr(GotoExpression expr) => ApplyDefault(expr); protected override TResult IndexExpr(IndexExpression expr) => ApplyDefault(expr); protected override TResult InvokeExpr(InvocationExpression expr) => ApplyDefault(expr); protected override TResult LabelExpr(LabelExpression expr) => ApplyDefault(expr); protected override TResult LambdaExpr(LambdaExpression expr) => ApplyDefault(expr); protected override TResult ListInitExpr(ListInitExpression expr) => ApplyDefault(expr); protected override TResult LoopExpr(LoopExpression expr) => ApplyDefault(expr); protected override TResult MemberAccessExpr(MemberExpression expr) => ApplyDefault(expr); protected override TResult MemberInitExpr(MemberInitExpression expr) => ApplyDefault(expr); protected override TResult NewArrayExpr(NewArrayExpression expr) => ApplyDefault(expr); protected override TResult NewExpr(NewExpression expr) => ApplyDefault(expr); protected override TResult ParameterExpr(ParameterExpression expr) => ApplyDefault(expr); protected override TResult RuntimeVariablesExpr(RuntimeVariablesExpression expr) => ApplyDefault(expr); protected override TResult SwitchExpr(SwitchExpression expr) => ApplyDefault(expr); protected override TResult TryExpr(TryExpression expr) => ApplyDefault(expr); protected override TResult TypeBinaryExpr(TypeBinaryExpression expr) => ApplyDefault(expr); protected override TResult UnaryExpr(UnaryExpression expr) => ApplyDefault(expr); protected override TResult UnknownExpr(Expression expr) => ApplyDefault(expr); } ``` ## File: Fauna/Util/Logger.cs ```csharp using Microsoft.Extensions.Logging; namespace Fauna.Util; /// /// This class encapsulates an object for logging throughout /// the Fauna .NET driver business logic. /// internal static class Logger { private static ILogger? s_logger; /// /// The singleton instance to use for logging /// public static ILogger Instance { get { if (s_logger == null) { s_logger = InitializeDefaultLogger(); } return s_logger; } } /// /// Optionally initialize the internal with a custom one /// /// The instance to use for logging public static void Initialize(ILogger logger) { s_logger = logger; } /// /// Initializes a default Console logger with single-line output, UTC timestamps, and /// minimum log-level based on the FAUNA_DEBUG environment variable; if the variable is /// not set, the default minimum log-level is /// /// An instance of created with /// Throws if FAUNA_DEBUG is outside of acceptable values private static ILogger InitializeDefaultLogger() { var logLevel = Environment.GetEnvironmentVariable("FAUNA_DEBUG"); var minLogLevel = LogLevel.None; if (!string.IsNullOrEmpty(logLevel) && int.TryParse(logLevel, out var level)) { if (level < (int)LogLevel.Trace || level > (int)LogLevel.None) { throw new ArgumentException( $"Invalid FAUNA_DEBUG value of {level}; must be between 0 and 6 inclusive. Set to 0 for highest verbosity, default is 6 (no logging)."); } minLogLevel = (LogLevel)level; } using ILoggerFactory factory = LoggerFactory.Create(builder => builder .AddConsole(options => options.LogToStandardErrorThreshold = LogLevel.Trace) .AddSimpleConsole(options => { options.IncludeScopes = true; options.SingleLine = true; options.TimestampFormat = "yyyy-MM-ddTHH:mm:ss.fffZ "; options.UseUtcTimestamp = true; }) .SetMinimumLevel(minLogLevel)); return factory.CreateLogger("fauna-dotnet"); } } ``` ## File: Fauna/Client.cs ```csharp using System.Globalization; using Fauna.Core; using Fauna.Exceptions; using Fauna.Linq; using Fauna.Mapping; using Fauna.Serialization; using Fauna.Types; using Stream = System.IO.Stream; namespace Fauna; /// /// Represents a client for interacting with a Fauna. /// public class Client : BaseClient, IDisposable { private const string QueryUriPath = "/query/1"; private const string StreamUriPath = "/stream/1"; private const string FeedUriPath = "/feed/1"; private readonly Configuration _config; private readonly IConnection _connection; private readonly MappingContext _defaultCtx = new(); private readonly Dictionary _dbCtxs = new(); private bool _disposed; internal override MappingContext MappingCtx { get => _defaultCtx; } /// /// Provides collection and aggregation of query statistics. Can be set to null in the . /// public readonly IStatsCollector? StatsCollector; /// /// Gets the timestamp of the last transaction seen by this client. /// public long LastSeenTxn { get; private set; } /// /// Initializes a new instance of a Client with the default configuration. /// Assumes the environment variable FAUNA_SECRET is set. /// public Client() : this(new Configuration()) { } /// /// Initializes a new instance of a Client with a secret. /// /// The secret key for authentication. public Client(string secret) : this(new Configuration(secret)) { } /// /// Initializes a new instance of the Client with a custom . /// /// The configuration settings for the client. public Client(Configuration config) { config.Validate(); _config = config; StatsCollector = config.StatsCollector; _connection = new Connection(config); } /// /// Create and return a new database context which uses the instance. /// /// The DataContext subtype to instantiate. /// An instance of . public DB DataContext() where DB : DataContext { var dbCtxType = typeof(DB); DataContext? ctx; lock (_dbCtxs) { if (!_dbCtxs.TryGetValue(dbCtxType, out ctx)) { var builder = new DataContextBuilder(); ctx = builder.Build(this); _dbCtxs[dbCtxType] = ctx; } } return (DB)ctx; } /// /// Disposes the resources used by the class. /// public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } /// /// /// https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/finalizers /// https://stackoverflow.com/questions/151051/when-should-i-use-gc-suppressfinalize /// ~Client() { Dispose(false); } internal override async Task> QueryAsyncInternal( Query query, ISerializer serializer, MappingContext ctx, QueryOptions? queryOptions, CancellationToken cancel) { if (query == null) { throw new ArgumentNullException(nameof(query)); } var finalOptions = QueryOptions.GetFinalQueryOptions(_config.DefaultQueryOptions, queryOptions); var headers = GetRequestHeaders(finalOptions); using var stream = new MemoryStream(); Serialize(stream, query, ctx); using var httpResponse = await _connection.DoPostAsync( QueryUriPath, stream, headers, GetRequestTimeoutWithBuffer(finalOptions.QueryTimeout), cancel); var body = await httpResponse.Content.ReadAsStringAsync(cancel); var res = QueryResponse.GetFromResponseBody(ctx, serializer, httpResponse.StatusCode, body); switch (res) { case QuerySuccess success: LastSeenTxn = res.LastSeenTxn; StatsCollector?.Add(res.Stats); return success; case QueryFailure failure: StatsCollector?.Add(res.Stats); throw ExceptionHandler.FromQueryFailure(ctx, failure); default: throw ExceptionHandler.FromRawResponse(body, httpResponse); } } internal override async IAsyncEnumerator> SubscribeStreamInternal( Types.EventSource eventSource, MappingContext ctx, CancellationToken cancel = default) { var finalOptions = QueryOptions.GetFinalQueryOptions(_config.DefaultQueryOptions, null); var headers = GetRequestHeaders(finalOptions); await foreach (var evt in _connection.OpenStream( StreamUriPath, eventSource, headers, ctx, cancel)) { LastSeenTxn = evt.TxnTime; eventSource.Options.Cursor = evt.Cursor; StatsCollector?.Add(evt.Stats); yield return evt; } } internal override async IAsyncEnumerator> SubscribeFeedInternal( Types.EventSource eventSource, MappingContext ctx, CancellationToken cancel = default) { cancel.ThrowIfCancellationRequested(); var finalOptions = _config.DefaultQueryOptions; var headers = GetRequestHeaders(finalOptions); while (!cancel.IsCancellationRequested) { var feedData = new MemoryStream(); eventSource.Serialize(feedData); using var httpResponse = await _connection.DoPostAsync( FeedUriPath, feedData, headers, GetRequestTimeoutWithBuffer(finalOptions.QueryTimeout), cancel); string body = await httpResponse.Content.ReadAsStringAsync(cancel); var res = FeedPage.From(body, ctx); eventSource.Options.Cursor = res.Cursor; StatsCollector?.Add(res.Stats); yield return res; if (!res.HasNext) { break; } } } private void Serialize(Stream stream, Query query, MappingContext ctx) { using var writer = new Utf8FaunaWriter(stream); writer.WriteStartObject(); writer.WriteFieldName("query"); query.Serialize(ctx, writer); writer.WriteEndObject(); writer.Flush(); } private Dictionary GetRequestHeaders(QueryOptions queryOptions) { var headers = new Dictionary { { Headers.Authorization, $"Bearer {_config.Secret}"}, { Headers.Format, "tagged" }, { Headers.Driver, "C#" } }; if (LastSeenTxn > long.MinValue) { headers.Add(Headers.LastTxnTs, LastSeenTxn.ToString()); } if (queryOptions.QueryTimeout != TimeSpan.Zero) { headers.Add( Headers.QueryTimeoutMs, queryOptions.QueryTimeout.TotalMilliseconds.ToString(CultureInfo.InvariantCulture)); } if (queryOptions.QueryTags != null) { headers.Add(Headers.QueryTags, EncodeQueryTags(queryOptions.QueryTags)); } if (!string.IsNullOrEmpty(queryOptions.TraceParent)) { headers.Add(Headers.TraceParent, queryOptions.TraceParent); } if (queryOptions.Linearized != null) { headers.Add(Headers.Linearized, queryOptions.Linearized.ToString()!); } if (queryOptions.TypeCheck != null) { headers.Add(Headers.TypeCheck, queryOptions.TypeCheck.ToString()!); } return headers; } private TimeSpan GetRequestTimeoutWithBuffer(TimeSpan queryTimeout) { return queryTimeout.Add(_config.ClientBufferTimeout); } private static string EncodeQueryTags(Dictionary tags) { return string.Join(",", tags.Select(entry => entry.Key + "=" + entry.Value)); } private void Dispose(bool disposing) { if (_disposed) return; if (disposing) { _connection.Dispose(); GC.SuppressFinalize(this); } _disposed = true; } } ``` ## File: Fauna/Configuration.cs ```csharp using Fauna.Core; using Fauna.Util; using Microsoft.Extensions.Logging; namespace Fauna; /// /// Configuration is a class used to configure a Fauna . It encapsulates various settings such as the , /// secret, query timeout, and others. /// public record class Configuration { /// /// Whether the should dispose of the on Dispose. /// public bool DisposeHttpClient { get; } = true; /// /// Additional buffer to add to when setting the HTTP request /// timeout on the ; default is 5 seconds. /// public TimeSpan ClientBufferTimeout { get; init; } = TimeSpan.FromSeconds(5); /// /// The HTTP Client to use for requests. /// public HttpClient HttpClient { get; init; } /// /// The secret key used for authentication. /// public string Secret { get; init; } = Environment.GetEnvironmentVariable("FAUNA_SECRET") ?? string.Empty; /// /// The endpoint URL of the Fauna server. /// public Uri Endpoint { get; init; } = Endpoints.GetFaunaEndpoint(); /// /// Default options for queries sent to Fauna. /// public QueryOptions DefaultQueryOptions { get; init; } = new(); /// /// The retry configuration to apply to requests. /// public RetryConfiguration RetryConfiguration { get; init; } = new(3, TimeSpan.FromSeconds(20)); /// /// StatsCollector for the client. /// public IStatsCollector? StatsCollector { get; init; } = new StatsCollector(); /// /// Initializes a new instance of the record with the specified secret key. /// /// The secret used for authentication. If null or empty, attempt to use the FAUNA_SECRET env var. /// The to use. If null, a default HttpClient is used. /// A logger. If null, a default logger is used. public Configuration(string secret = "", HttpClient? httpClient = null, ILogger? logger = null) { if (!string.IsNullOrEmpty(secret)) { Secret = secret; } if (httpClient != null) { HttpClient = httpClient; DisposeHttpClient = false; } else { HttpClient = new HttpClient { Timeout = Timeout.InfiniteTimeSpan }; } if (logger != null) { Logger.Initialize(logger); } } internal void Validate() { if (string.IsNullOrEmpty(Secret)) throw new ArgumentNullException( nameof(Secret), "Need to set FAUNA_SECRET environment variable or pass a secret as a parameter when creating the Client." ); } } ``` ## File: Fauna/Fauna.csproj ``` net8.0 1.0.1 Fauna .NET Driver for Fauna Fauna, Inc. .NET Driver for Fauna. See https://fauna.com for more information. enable enable true true ``` ## File: Fauna/IClient.cs ```csharp using System.Runtime.CompilerServices; using Fauna.Core; using Fauna.Exceptions; using Fauna.Mapping; using Fauna.Serialization; using Fauna.Types; using static Fauna.Query; namespace Fauna; /// /// Represents a client for interacting with a Fauna. /// public interface IClient { /// /// Asynchronously executes a specified FQL query against the Fauna database and returns the typed result. /// /// The type of the result expected from the query, corresponding to the structure of the FQL query's expected response. /// The FQL query object representing the query to be executed against the Fauna database. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use /// A Task representing the asynchronous operation, which upon completion contains the result of the query execution as . /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public Task> QueryAsync( Query query, QueryOptions? queryOptions = null, CancellationToken cancel = default) where T : notnull; /// /// Asynchronously executes a specified FQL query against the Fauna database. /// /// The FQL query object representing the query to be executed against the Fauna database. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use. /// A Task representing the asynchronous operation, which upon completion contains the result of the query execution. /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public Task> QueryAsync( Query query, QueryOptions? queryOptions = null, CancellationToken cancel = default); /// /// Asynchronously executes a specified FQL query against the Fauna database and returns the typed result. /// /// The type of the result expected from the query, corresponding to the structure of the FQL query's expected response. /// The FQL query object representing the query to be executed against the Fauna database. /// A serializer for the success data type. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use. /// A Task representing the asynchronous operation, which upon completion contains the result of the query execution as . /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public Task> QueryAsync( Query query, ISerializer serializer, QueryOptions? queryOptions = null, CancellationToken cancel = default); /// /// Asynchronously executes a specified FQL query against the Fauna database and returns the typed result. /// /// The FQL query object representing the query to be executed against the Fauna database. /// A serializer for the success data type. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation toke to use. /// A Task representing the asynchronous operation, which upon completion contains the result of the query execution. /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public Task> QueryAsync( Query query, ISerializer serializer, QueryOptions? queryOptions = null, CancellationToken cancel = default); /// /// Asynchronously iterates over pages of a Fauna query result, automatically fetching subsequent pages using the 'after' cursor. /// /// The type of the data expected in each page. /// The FQL query object representing the query to be executed against the Fauna database. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use. /// An asynchronous enumerable of pages, each containing a list of items of type . /// /// This method handles pagination by sending multiple requests to Fauna as needed, based on the presence of an 'after' cursor in the query results. /// /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public IAsyncEnumerable> PaginateAsync( Query query, QueryOptions? queryOptions = null, CancellationToken cancel = default) where T : notnull; /// /// Asynchronously iterates over pages of a Fauna query result, automatically fetching subsequent pages using the 'after' cursor. /// The provided page is the first page yielded. /// /// The type of the data expected in each page. /// The initial page. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use. /// An asynchronous enumerable of pages, each containing a list of items of type . /// /// This method handles pagination by sending multiple requests to Fauna as needed, based on the presence of an 'after' cursor in the query results. /// /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public IAsyncEnumerable> PaginateAsync( Page page, QueryOptions? queryOptions = null, CancellationToken cancel = default) where T : notnull; /// /// Asynchronously iterates over pages of a Fauna query result, automatically fetching subsequent pages using the 'after' cursor. /// /// The FQL query object representing the query to be executed against the Fauna database. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use. /// An asynchronous enumerable of pages, each containing a list of items. /// /// This method handles pagination by sending multiple requests to Fauna as needed, based on the presence of an 'after' cursor in the query results. /// /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public IAsyncEnumerable> PaginateAsync( Query query, QueryOptions? queryOptions = null, CancellationToken cancel = default); /// /// Asynchronously iterates over pages of a Fauna query result, automatically fetching subsequent pages using the 'after' cursor. /// The provided page is the first page yielded. /// /// The initial page. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use. /// An asynchronous enumerable of pages, each containing a list of items. /// /// This method handles pagination by sending multiple requests to Fauna as needed, based on the presence of an 'after' cursor in the query results. /// /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public IAsyncEnumerable> PaginateAsync( Page page, QueryOptions? queryOptions = null, CancellationToken cancel = default); /// /// Asynchronously iterates over pages of a Fauna query result, automatically fetching subsequent pages using the 'after' cursor. /// /// The type of the data expected in each page. /// The FQL query object representing the query to be executed against the Fauna database. /// A data serializer for the page element type. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use. /// An asynchronous enumerable of pages, each containing a list of items of type . /// /// This method handles pagination by sending multiple requests to Fauna as needed, based on the presence of an 'after' cursor in the query results. /// /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public IAsyncEnumerable> PaginateAsync( Query query, ISerializer elemSerializer, QueryOptions? queryOptions = null, CancellationToken cancel = default); /// /// Asynchronously iterates over pages of a Fauna query result, automatically fetching subsequent pages using the 'after' cursor. /// The provided page is the first page yielded. /// /// The type of the data expected in each page. /// The initial page. /// A data serializer for the page element type. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use. /// An asynchronous enumerable of pages, each containing a list of items of type . /// /// This method handles pagination by sending multiple requests to Fauna as needed, based on the presence of an 'after' cursor in the query results. /// /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public IAsyncEnumerable> PaginateAsync( Page page, ISerializer elemSerializer, QueryOptions? queryOptions = null, CancellationToken cancel = default); /// /// Asynchronously iterates over pages of a Fauna query result, automatically fetching subsequent pages using the 'after' cursor. /// /// The FQL query object representing the query to be executed against the Fauna database. /// A data serializer for the page element type. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use. /// A Task representing the asynchronous operation, which upon completion contains the result of the query execution. /// /// This method handles pagination by sending multiple requests to Fauna as needed, based on the presence of an 'after' cursor in the query results. /// /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public IAsyncEnumerable> PaginateAsync( Query query, ISerializer elemSerializer, QueryOptions? queryOptions = null, CancellationToken cancel = default); /// /// Asynchronously iterates over pages of a Fauna query result, automatically fetching subsequent pages using the 'after' cursor. /// The provided page is the first page yielded. /// /// The FQL query object representing the query to be executed against the Fauna database. /// A data serializer for the page element type. /// Optional parameters to customize the query execution, such as timeout settings and custom headers. /// A cancellation token to use. /// A Task representing the asynchronous operation, which upon completion contains the result of the query execution. /// /// This method handles pagination by sending multiple requests to Fauna as needed, based on the presence of an 'after' cursor in the query results. /// /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. public IAsyncEnumerable> PaginateAsync( Page page, ISerializer elemSerializer, QueryOptions? queryOptions = null, CancellationToken cancel = default); /// /// Asynchronously executes a specified FQL query against the Fauna database and returns the typed result. /// /// The type of the result expected from the query, corresponding to the structure of the FQL query's expected response. /// The reference to load. /// A cancellation token to use /// A Task representing the asynchronous operation, which upon completion contains the result of the query execution as . /// Thrown when authentication fails due to invalid credentials or other authentication issues. /// Thrown when the client lacks sufficient permissions to execute the query. /// Thrown when the query has syntax errors or is otherwise malformed. /// Thrown when runtime errors occur during query execution, such as invalid arguments or operational failures. /// Thrown when the FQL `abort` function is called within the query, containing the data provided during the abort operation. /// Thrown for improperly formatted requests or requests that Fauna cannot process. /// Thrown when a transaction is aborted due to concurrent modification or contention issues. /// Thrown when the query exceeds established rate limits for the Fauna service. /// Thrown when the query execution time exceeds the specified or default timeout period. /// Thrown in response to internal Fauna service errors, indicating issues on the server side. /// Thrown for unexpected or miscellaneous errors not covered by the other specific exception types. /// Thrown when the provided reference does not exist. public Task LoadRefAsync( BaseRef reference, CancellationToken cancel = default) where T : notnull; } /// /// The base class for Client and DataContext. /// public abstract class BaseClient : IClient { internal BaseClient() { } internal abstract MappingContext MappingCtx { get; } internal abstract Task> QueryAsyncInternal( Query query, ISerializer serializer, MappingContext ctx, QueryOptions? queryOptions, CancellationToken cancel ); #region IClient /// public Task> QueryAsync( Query query, QueryOptions? queryOptions = null, CancellationToken cancel = default) where T : notnull => QueryAsync(query, Serializer.Generate(MappingCtx), queryOptions, cancel); /// public Task> QueryAsync( Query query, QueryOptions? queryOptions = null, CancellationToken cancel = default) => QueryAsync(query, Serializer.Dynamic, queryOptions, cancel); /// public Task> QueryAsync( Query query, ISerializer serializer, QueryOptions? queryOptions = null, CancellationToken cancel = default) => QueryAsyncInternal(query, serializer, MappingCtx, queryOptions, cancel); /// public Task> QueryAsync( Query query, ISerializer serializer, QueryOptions? queryOptions = null, CancellationToken cancel = default) => QueryAsync(query, (ISerializer)serializer, queryOptions, cancel); /// public IAsyncEnumerable> PaginateAsync( Query query, QueryOptions? queryOptions = null, CancellationToken cancel = default) where T : notnull => PaginateAsync(query, Serializer.Generate(MappingCtx), queryOptions, cancel); /// public IAsyncEnumerable> PaginateAsync( Page page, QueryOptions? queryOptions = null, CancellationToken cancel = default) where T : notnull => PaginateAsync(page, Serializer.Generate(MappingCtx), queryOptions, cancel); /// public IAsyncEnumerable> PaginateAsync( Query query, QueryOptions? queryOptions = null, CancellationToken cancel = default) => PaginateAsync(query, Serializer.Dynamic, queryOptions, cancel); /// public IAsyncEnumerable> PaginateAsync( Page page, QueryOptions? queryOptions = null, CancellationToken cancel = default) => PaginateAsync(page, Serializer.Dynamic, queryOptions, cancel); /// public IAsyncEnumerable> PaginateAsync( Query query, ISerializer elemSerializer, QueryOptions? queryOptions = null, CancellationToken cancel = default) { var serializer = new PageSerializer(elemSerializer); return PaginateAsyncInternal(query, serializer, queryOptions, cancel); } /// public IAsyncEnumerable> PaginateAsync( Page page, ISerializer elemSerializer, QueryOptions? queryOptions = null, CancellationToken cancel = default) { var serializer = new PageSerializer(elemSerializer); return PaginateAsyncInternal(page, serializer, queryOptions, cancel); } /// public IAsyncEnumerable> PaginateAsync( Query query, ISerializer elemSerializer, QueryOptions? queryOptions = null, CancellationToken cancel = default) { var elemObjSer = (ISerializer)elemSerializer; var serializer = new PageSerializer(elemObjSer); return PaginateAsyncInternal(query, serializer, queryOptions, cancel); } /// public IAsyncEnumerable> PaginateAsync( Page page, ISerializer elemSerializer, QueryOptions? queryOptions = null, CancellationToken cancel = default) { var elemObjSer = (ISerializer)elemSerializer; var serializer = new PageSerializer(elemObjSer); return PaginateAsyncInternal(page, serializer, queryOptions, cancel); } /// public async Task LoadRefAsync( BaseRef reference, CancellationToken cancel = default) where T : notnull { if (reference.IsLoaded) { return reference.Get(); } var q = FQL($"{reference}"); var res = await QueryAsync(q, Serializer.Generate>(MappingCtx), null, cancel); return res.Data.Get(); } #endregion // Internally accessible for QuerySource use internal async IAsyncEnumerable> PaginateAsyncInternal( Query query, PageSerializer serializer, QueryOptions? queryOptions, [EnumeratorCancellation] CancellationToken cancel = default) { var p = await QueryAsyncInternal(query, serializer, MappingCtx, queryOptions, cancel); await foreach (var page in PaginateAsyncInternal(p.Data, serializer, queryOptions, cancel)) { yield return page; } } private async IAsyncEnumerable> PaginateAsyncInternal( Page page, PageSerializer serializer, QueryOptions? queryOptions, [EnumeratorCancellation] CancellationToken cancel = default) { yield return page; while (page.After is not null) { var q = new QueryExpr(new QueryLiteral($"Set.paginate('{page.After}')")); var response = await QueryAsyncInternal(q, serializer, MappingCtx, queryOptions, cancel); page = response.Data; yield return page; } } #region Streaming /// /// Opens the stream with Fauna and returns an enumerator for the stream events. /// /// The type of event data that will be deserialized from the stream. /// The event source to subscribe to. /// The mapping context to use for deserializing stream events. /// The cancellation token for the operation. /// An async enumerator of stream events. /// Implementation internal abstract IAsyncEnumerator> SubscribeStreamInternal( EventSource eventSource, MappingContext ctx, CancellationToken cancel = default) where T : notnull; /// /// Retrieves a Stream token from Fauna and returns a StreamEnumerable for the stream events. /// /// Event Data will be deserialized to this type. /// The query to create the stream from Fauna. /// The options for the query. /// The options for the stream. /// The cancellation token. /// A task that represents the asynchronous operation. The task result contains a stream of events. public async Task> EventStreamAsync( Query query, QueryOptions? queryOptions = null, StreamOptions? streamOptions = null, CancellationToken cancellationToken = default) where T : notnull { EventSource eventSource = streamOptions?.Token != null ? new EventSource(streamOptions.Token) { Options = streamOptions } : await GetEventSourceFromQueryAsync(query, queryOptions, cancellationToken); return new StreamEnumerable(this, eventSource, cancellationToken); } /// /// Returns a StreamEnumerable for the stream events. /// /// /// /// Which Type to map the Events to. /// public async Task> EventStreamAsync( EventSource eventSource, CancellationToken cancellationToken = default) where T : notnull { await Task.CompletedTask; return new StreamEnumerable(this, eventSource, cancellationToken); } /// /// Opens the event feed with Fauna and returns an enumerator for the events. /// /// The type of event data that will be deserialized from the stream. /// The event source to subscribe to. /// The mapping context to use for deserializing stream events. /// The cancellation token for the operation. /// An async enumerator of stream events. /// Implementation internal abstract IAsyncEnumerator> SubscribeFeedInternal( EventSource eventSource, MappingContext ctx, CancellationToken cancel = default) where T : notnull; /// /// Opens the event feed with Fauna and returns an enumerator for the events. /// /// /// The options for the feed. /// The cancellation token for the operation. /// Which Type to map the Events to. /// public async Task> EventFeedAsync( EventSource eventSource, FeedOptions? feedOptions = null, CancellationToken cancellationToken = default) where T : notnull { await Task.CompletedTask; if (feedOptions != null) eventSource.Options = feedOptions; return new FeedEnumerable(this, eventSource, cancellationToken); } /// /// Opens the event feed with Fauna and returns an enumerator for the events. /// /// The query to create the stream from Fauna. /// The options for the feed. /// The cancellation token for the operation. /// Which Type to map the Events to. /// public async Task> EventFeedAsync( Query query, FeedOptions? feedOptions = null, CancellationToken cancellationToken = default) where T : notnull { EventSource eventSource = await GetEventSourceFromQueryAsync(query, null, cancellationToken); if (feedOptions != null) eventSource.Options = feedOptions; return new FeedEnumerable(this, eventSource, cancellationToken); } /// /// Retrieves an EventSource from Fauna Query /// /// /// /// /// EventSource returned from Query private async Task GetEventSourceFromQueryAsync( Query query, QueryOptions? queryOptions, CancellationToken cancellationToken) { try { var response = await QueryAsync( query, queryOptions, cancellationToken); return response.Data; } catch (SerializationException ex) { throw new InvalidOperationException("Query must return an EventSource.", ex); } } /// /// Opens the stream with Fauna and returns an enumerator for the stream events. /// /// Event Data will be deserialized to this type. /// The stream to subscribe to. /// Mapping context for stream. /// The cancellation token. /// An async enumerator of stream events. public IAsyncEnumerator> SubscribeStream( EventSource eventSource, MappingContext ctx, CancellationToken cancel = default) where T : notnull { return SubscribeStreamInternal(eventSource, ctx, cancel); } /// /// Opens an event feed with Fauna and returns an enumerator for the events. /// /// Event Data will be deserialized to this type. /// The stream to subscribe to. /// Mapping context for stream. /// The cancellation token. /// An async enumerator of stream events. public IAsyncEnumerator> SubscribeFeed( EventSource eventSource, MappingContext ctx, CancellationToken cancel = default) where T : notnull { return SubscribeFeedInternal(eventSource, ctx, cancel); } #endregion } ```