InfluxDB.Client.Linq 4.17.0-dev.headers.read.1

This is a prerelease version of InfluxDB.Client.Linq.
There is a newer version of this package available.
See the version list below for details.
dotnet add package InfluxDB.Client.Linq --version 4.17.0-dev.headers.read.1
                    
NuGet\Install-Package InfluxDB.Client.Linq -Version 4.17.0-dev.headers.read.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="InfluxDB.Client.Linq" Version="4.17.0-dev.headers.read.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="InfluxDB.Client.Linq" Version="4.17.0-dev.headers.read.1" />
                    
Directory.Packages.props
<PackageReference Include="InfluxDB.Client.Linq" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add InfluxDB.Client.Linq --version 4.17.0-dev.headers.read.1
                    
#r "nuget: InfluxDB.Client.Linq, 4.17.0-dev.headers.read.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package InfluxDB.Client.Linq@4.17.0-dev.headers.read.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=InfluxDB.Client.Linq&version=4.17.0-dev.headers.read.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=InfluxDB.Client.Linq&version=4.17.0-dev.headers.read.1&prerelease
                    
Install as a Cake Tool

InfluxDB.Client.Linq

The library supports to use a LINQ expression to query the InfluxDB.

Documentation

This section contains links to the client library documentation.

Usage

How to start

First, add the library as a dependency for your project:

# For actual version please check: https://www.nuget.org/packages/InfluxDB.Client.Linq/

dotnet add package InfluxDB.Client.Linq --version 1.17.0-dev.linq.17

Next, you should add additional using statement to your program:

using InfluxDB.Client.Linq;

The LINQ query depends on QueryApiSync, you could create an instance of QueryApiSync by:

var client = new InfluxDBClient("http://localhost:8086", "my-token");
var queryApi = client.GetQueryApiSync();

In the following examples we assume that the Sensor entity is defined as:

class Sensor
{
    [Column("sensor_id", IsTag = true)] 
    public string SensorId { get; set; }

    /// <summary>
    /// "production" or "testing"
    /// </summary>
    [Column("deployment", IsTag = true)]
    public string Deployment { get; set; }

    /// <summary>
    /// Value measured by sensor
    /// </summary>
    [Column("data")]
    public float Value { get; set; }

    [Column(IsTimestamp = true)] 
    public DateTime Timestamp { get; set; }
}

Time Series

The InfluxDB uses concept of TimeSeries - a collection of data that shares a measurement, tag set, and bucket. You always operate on each time-series, if you querying data with Flux.

Imagine that you have following data:

sensor,deployment=production,sensor_id=id-1 data=15
sensor,deployment=testing,sensor_id=id-1 data=28
sensor,deployment=testing,sensor_id=id-1 data=12
sensor,deployment=production,sensor_id=id-1 data=89

The corresponding time series are:

  • sensor,deployment=production,sensor_id=id-1
  • sensor,deployment=testing,sensor_id=id-1

If you query your data with following Flux:

from(bucket: "my-bucket")
  |> range(start: 0)
  |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> drop(columns: ["_start", "_stop", "_measurement"])
  |> limit(n:1)

The result will be one item for each time-series:

sensor,deployment=production,sensor_id=id-1 data=15
sensor,deployment=testing,sensor_id=id-1 data=28

and this is also way how this LINQ driver works.

The driver supposes that you are querying over one time-series.

There is a way how to change this configuration:

Enable querying multiple time-series

var settings = new QueryableOptimizerSettings{QueryMultipleTimeSeries = true};
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", _queryApi, settings)
    select s;

The group() function is way how to query multiple time-series and gets correct results.

The following query works correctly:

from(bucket: "my-bucket")
  |> range(start: 0)
  |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> drop(columns: ["_start", "_stop", "_measurement"])
  |> group()
  |> limit(n:1)

and corresponding result:

sensor,deployment=production,sensor_id=id-1 data=15

Do not used this functionality if it is not required because it brings a performance costs caused by sorting:

Group does not guarantee sort order

The group() does not guarantee sort order of output records. To ensure data is sorted correctly, use orderby expression.

Client Side Evaluation

The library attempts to evaluate a query on the server as much as possible. The client side evaluations is required for aggregation function if there is more then one time series.

If you want to count your data with following Flux:

from(bucket: "my-bucket")
  |> range(start: 0)
  |> drop(columns: ["_start", "_stop", "_measurement"])
  |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
  |> stateCount(fn: (r) => true, column: "linq_result_column") 
  |> last(column: "linq_result_column") 
  |> keep(columns: ["linq_result_column"])

The result will be one count for each time-series:

#group,false,false,false
#datatype,string,long,long
#default,_result,,
,result,table,linq_result_column
,,0,1
,,0,1

and client has to aggregate this multiple results into one scalar value.

Operators that could cause client side evaluation:

  • Count
  • CountLong

TL;DR

Perform Query

The LINQ query requires bucket and organization as a source of data. Both of them could be name or ID.

var query = (from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.SensorId == "id-1"
    where s.Value > 12
    where s.Timestamp > new DateTime(2019, 11, 16, 8, 20, 15, DateTimeKind.Utc)
    where s.Timestamp < new DateTime(2021, 01, 10, 5, 10, 0, DateTimeKind.Utc)
    orderby s.Timestamp
    select s)
    .Take(2)
    .Skip(2);

var sensors = query.ToList();

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 2019-11-16T08:20:15Z, stop: 2021-01-10T05:10:00Z) 
    |> filter(fn: (r) => (r["sensor_id"] == "id-1")) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> filter(fn: (r) => (r["data"] > 12)) 
    |> limit(n: 2, offset: 2)

Filtering

The range() and filter() are pushdown functions that allow push their data manipulation down to the underlying data source rather than storing and manipulating data in memory. Using pushdown functions at the beginning of query we greatly reduce the amount of server memory necessary to run a query.

The LINQ provider needs to aligns fields within each input table that have the same timestamp to column-wise format:

From
_time _value _measurement _field
1970-01-01T00:00:00.000000001Z 1.0 "m1" "f1"
1970-01-01T00:00:00.000000001Z 2.0 "m1" "f2"
1970-01-01T00:00:00.000000002Z 3.0 "m1" "f1"
1970-01-01T00:00:00.000000002Z 4.0 "m1" "f2"
To
_time _measurement f1 f2
1970-01-01T00:00:00.000000001Z "m1" 1.0 2.0
1970-01-01T00:00:00.000000002Z "m1" 3.0 4.0

For that reason we need to use the pivot() function. The pivot is heavy and should be used at the end of our Flux query.

There is an also possibility to disable appending pivot by:

var optimizerSettings =
    new QueryableOptimizerSettings
    {
        AlignFieldsWithPivot = false
    };
    
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi, optimizerSettings)
    select s;

Mapping LINQ filters

For the best performance on the both side - server, LINQ provider we maps the LINQ expressions to FLUX query following way:

Filter by Timestamp

Mapped to range().

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Timestamp >= new DateTime(2019, 11, 16, 8, 20, 15, DateTimeKind.Utc)
    select s;

var sensors = query.ToList();

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 2019-11-16T08:20:15ZZ) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> drop(columns: ["_start", "_stop", "_measurement"])
Filter by Tag

Mapped to filter() before pivot().

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.SensorId == "id-1"
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0)
    |> filter(fn: (r) => (r["sensor_id"] == "id-1"))  
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> drop(columns: ["_start", "_stop", "_measurement"])
Filter by Field

The filter by field has to be after the pivot() because we want to select all fields from pivoted table.

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Value < 28
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0)
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")  
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> filter(fn: (r) => (r["data"] < 28))

If we move the filter() for fields before the pivot() then we will gets wrong results:

Data
m1 f1=1,f2=2 1
m1 f1=3,f2=4 2
Without filter
from(bucket: "my-bucket") 
    |> range(start: 0)
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])

Results:

_time f1 f2
1970-01-01T00:00:00.000000001Z 1.0 2.0
1970-01-01T00:00:00.000000002Z 3.0 4.0
Filter before pivot()

filter: f1 > 0

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> filter(fn: (r) => (r["_field"] == "f1" and r["_value"] > 0))
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])

Results:

_time f1
1970-01-01T00:00:00.000000001Z 1.0
1970-01-01T00:00:00.000000002Z 3.0

Time Range Filtering

The time filtering expressions are mapped to Flux range() function. This function has start and stop parameters with following behaviour: start <= _time < stop:

Results include records with _time values greater than or equal to the specified start time and less than the specified stop time.

This means that we have to add one nanosecond to start if we want timestamp greater than and also add one nanosecond to stop if we want to timestamp lesser or equal than.

Example 1:
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Timestamp > new DateTime(2019, 11, 16, 8, 20, 15, DateTimeKind.Utc)
    where s.Timestamp < new DateTime(2021, 01, 10, 5, 10, 0, DateTimeKind.Utc)
    select s;

var sensors = query.ToList();

Flux Query:

start_shifted = int(v: time(v: "2019-11-16T08:20:15Z")) + 1

from(bucket: "my-bucket") 
    |> range(start: time(v: start_shifted), stop: 2021-01-10T05:10:00Z)
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
Example 2:
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Timestamp >= new DateTime(2019, 11, 16, 8, 20, 15, DateTimeKind.Utc)
    where s.Timestamp <= new DateTime(2021, 01, 10, 5, 10, 0, DateTimeKind.Utc)
    select s;

var sensors = query.ToList();

Flux Query:

stop_shifted = int(v: time(v: "2021-01-10T05:10:00Z")) + 1

from(bucket: "my-bucket") 
    |> range(start: 2019-11-16T08:20:15Z, stop: time(v: stop_shifted)) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> drop(columns: ["_start", "_stop", "_measurement"])
Example 3:
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Timestamp >= new DateTime(2019, 11, 16, 8, 20, 15, DateTimeKind.Utc)
    select s;

var sensors = query.ToList();

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 2019-11-16T08:20:15ZZ) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> drop(columns: ["_start", "_stop", "_measurement"])
Example 4:
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Timestamp <= new DateTime(2021, 01, 10, 5, 10, 0, DateTimeKind.Utc)
    select s;

var sensors = query.ToList();

Flux Query:

stop_shifted = int(v: time(v: "2021-01-10T05:10:00Z")) + 1

from(bucket: "my-bucket") 
    |> range(start: 0, stop: time(v: stop_shifted))
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
Example 5:
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Timestamp == new DateTime(2019, 11, 16, 8, 20, 15, DateTimeKind.Utc)
    select s;

var sensors = query.ToList();

Flux Query:

stop_shifted = int(v: time(v: "2019-11-16T08:20:15Z")) + 1

from(bucket: "my-bucket") 
    |> range(start: 2019-11-16T08:20:15Z, stop: time(v: stop_shifted)) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> drop(columns: ["_start", "_stop", "_measurement"])

There is also a possibility to specify the default value for start and stop parameter. This is useful when you need to include data with future timestamps when no time bounds are explicitly set.

var settings = new QueryableOptimizerSettings
{
    RangeStartValue = DateTime.UtcNow.AddHours(-24),
    RangeStopValue = DateTime.UtcNow.AddHours(1)
};
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi, settings)
    select s;

TD;LR

Supported LINQ operators

Equal

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.SensorId == "id-1"
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0)
    |> filter(fn: (r) => (r["sensor_id"] == "id-1"))  
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> drop(columns: ["_start", "_stop", "_measurement"])

Not Equal

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.SensorId != "id-1"
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0)
    |> filter(fn: (r) => (r["sensor_id"] != "id-1")) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> drop(columns: ["_start", "_stop", "_measurement"])

Less Than

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Value < 28
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> filter(fn: (r) => (r["data"] < 28))

Less Than Or Equal

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Value <= 28
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> filter(fn: (r) => (r["data"] <= 28))

Greater Than

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Value > 28
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> filter(fn: (r) => (r["data"] > 28))

Greater Than Or Equal

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Value >= 28
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> filter(fn: (r) => (r["data"] >= 28))

And

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Value >= 28 && s.SensorId != "id-1"
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> filter(fn: (r) => (r["sensor_id"] != "id-1"))
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> filter(fn: (r) => (r["data"] >= 28))

Or

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Value >= 28 || s.Value <= 5
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> filter(fn: (r) => ((r["data"] >= 28) or (r["data"] <=> 28)))

Any

The following code demonstrates how to use the Any operator to determine whether a collection contains any elements. By default the InfluxDB.Client doesn't supports to store a subcollection in your DomainObject.

Imagine that you have following entities:

class SensorCustom
{
    public Guid Id { get; set; }
    
    public float Data { get; set; }
    
    public DateTimeOffset Time { get; set; }
    
    public virtual ICollection<SensorAttribute> Attributes { get; set; }
}

class SensorAttribute
{
    public string Name { get; set; }
    public string Value { get; set; }
}

To be able to store SensorCustom entity in InfluxDB and retrieve it from database you should implement IDomainObjectMapper. The converter tells to the Client how to map DomainObject into PointData and how to map FluxRecord to DomainObject.

Entity Converter:

private class SensorEntityConverter : IDomainObjectMapper
{
    //
    // Parse incoming FluxRecord to DomainObject
    //
    public T ConvertToEntity<T>(FluxRecord fluxRecord)
    {
        if (typeof(T) != typeof(SensorCustom))
        {
            throw new NotSupportedException($"This converter doesn't supports: {typeof(SensorCustom)}");
        }

        //
        // Create SensorCustom entity and parse `SeriesId`, `Value` and `Time`
        //
        var customEntity = new SensorCustom
        {
            Id = Guid.Parse(Convert.ToString(fluxRecord.GetValueByKey("series_id"))!),
            Data = Convert.ToDouble(fluxRecord.GetValueByKey("data")),
            Time = fluxRecord.GetTime().GetValueOrDefault().ToDateTimeUtc(),
            Attributes = new List<SensorAttribute>()
        };
        
        foreach (var (key, value) in fluxRecord.Values)
        {
            //
            // Parse SubCollection values
            //
            if (key.StartsWith("property_"))
            {
                var attribute = new SensorAttribute
                {
                    Name = key.Replace("property_", string.Empty), Value = Convert.ToString(value)
                };
                
                customEntity.Attributes.Add(attribute);
            }
        }

        return (T) Convert.ChangeType(customEntity, typeof(T));
    }

    //
    // Convert DomainObject into PointData
    //
    public PointData ConvertToPointData<T>(T entity, WritePrecision precision)
    {
        if (!(entity is SensorCustom ce))
        {
            throw new NotSupportedException($"This converter doesn't supports: {typeof(SensorCustom)}");
        }

        //
        // Map `SeriesId`, `Value` and `Time` to Tag, Field and Timestamp
        //
        var point = PointData
            .Measurement("custom_measurement")
            .Tag("series_id", ce.Id.ToString())
            .Field("data", ce.Data)
            .Timestamp(ce.Time, precision);

        //
        // Map subattributes to Fields
        //
        foreach (var attribute in ce.Attributes ?? new List<SensorAttribute>())
        {
            point = point.Field($"property_{attribute.Name}", attribute.Value);
        }

        return point;
    }
}

The Converter could be passed to QueryApiSync, QueryApi or WriteApi by:

// Create Converter
var converter = new SensorEntityConverter();

// Get Query and Write API
var queryApi = client.GetQueryApiSync(converter);
var writeApi = client.GetWriteApi(converter);

The LINQ provider needs to know how properties of DomainObject are stored in InfluxDB - their name and type (tag, field, timestamp).

If you use a IDomainObjectMapper instead of InfluxDB Attributes you should implement IMemberNameResolver:

private class SensorMemberResolver: IMemberNameResolver
{
    //
    // Tell to LINQ providers how is property of DomainObject mapped - Tag, Field, Timestamp, ... ?
    //
    public MemberType ResolveMemberType(MemberInfo memberInfo)
    {
        //
        // Mapping of subcollection
        //
        if (memberInfo.DeclaringType == typeof(SensorAttribute))
        {
            return memberInfo.Name switch
            {
                "Name" => MemberType.NamedField,
                "Value" => MemberType.NamedFieldValue,
                _ => MemberType.Field
            };
        }

        //
        // Mapping of "root" domain
        //
        return memberInfo.Name switch
        {
            "Time" => MemberType.Timestamp,
            "Id" => MemberType.Tag,
            _ => MemberType.Field
        };
    }

    //
    // Tell to LINQ provider how is property of DomainObject named 
    //
    public string GetColumnName(MemberInfo memberInfo)
    {
        return memberInfo.Name switch
        {
            "Id" => "series_id",
            "Data" => "data",
            _ => memberInfo.Name
        };
    }

    //
    // Tell to LINQ provider how is named property that is flattened
    //
    public string GetNamedFieldName(MemberInfo memberInfo, object value)
    {
        return "attribute_" + Convert.ToString(value);
    }
}

Now We are able to provide a required information to the LINQ provider by memberResolver parameter:

var memberResolver = new SensorMemberResolver();

var query = from s in InfluxDBQueryable<SensorCustom>.Queryable("my-bucket", "my-org", queryApi, memberResolver)
    where s.Attributes.Any(a => a.Name == "quality" && a.Value == "good")
    select s;

Flux Query:

from(bucket: "my-bucket")
    |> range(start: 0)
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> filter(fn: (r) => (r["attribute_quality"] == "good"))

For more info see CustomDomainMappingAndLinq example.

Take

var query = (from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    select s)
    .Take(10);

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> limit(n: 10)

Note: the limit() function can be align before pivot() function by:

var optimizerSettings =
    new QueryableOptimizerSettings
    {
        AlignLimitFunctionAfterPivot = false
    };

Performance: The pivot() is a “heavy” function. Using limit() before pivot() is much faster but works only if you have consistent data series. See #318 for more details.

TakeLast

var query = (from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    select s)
    .TakeLast(10);

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> tail(n: 10)

Note: the tail() function can be align before pivot() function by:

var optimizerSettings =
    new QueryableOptimizerSettings
    {
        AlignLimitFunctionAfterPivot = false
    };

Performance: The pivot() is a “heavy” function. Using tail() before pivot() is much faster but works only if you have consistent data series. See #318 for more details.

Skip

var query = (from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    select s)
    .Take(10)
    .Skip(50);

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> limit(n: 10, offset: 50)

OrderBy

Example 1:
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    orderby s.Deployment
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> sort(columns: ["deployment"], desc: false)
Example 2:
var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    orderby s.Timestamp descending 
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> sort(columns: ["_time"], desc: true)

Count

Possibility of partial client side evaluation

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    select s;

var sensors = query.Count();

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> stateCount(fn: (r) => true, column: "linq_result_column") 
    |> last(column: "linq_result_column") 
    |> keep(columns: ["linq_result_column"])

LongCount

Possibility of partial client side evaluation

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    select s;

var sensors = query.LongCount();

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0)
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> stateCount(fn: (r) => true, column: "linq_result_column") 
    |> last(column: "linq_result_column") 
    |> keep(columns: ["linq_result_column"])

Contains

int[] values = {15, 28};

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where values.Contains(s.Value)
    select s;

var sensors = query.Count();

Flux Query:

from(bucket: "my-bucket")
    |> range(start: 0)
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value")
    |> drop(columns: ["_start", "_stop", "_measurement"])
    |> filter(fn: (r) => contains(value: r["data"], set: [15, 28]))

Custom LINQ operators

AggregateWindow

The AggregateWindow applies an aggregate function to fixed windows of time. Can be used only for a field which is defined as timestamp - [Column(IsTimestamp = true)]. For more info about aggregateWindow() function see Flux's documentation - https://docs.influxdata.com/flux/v0.x/stdlib/universe/aggregatewindow/.

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    where s.Timestamp.AggregateWindow(TimeSpan.FromSeconds(20), TimeSpan.FromSeconds(40), "mean")
    select s;

Flux Query:

from(bucket: "my-bucket") 
    |> range(start: 0) 
    |> aggregateWindow(every: 20s, period: 40s, fn: mean) 
    |> pivot(rowKey:["_time"], columnKey: ["_field"], valueColumn: "_value") 
    |> drop(columns: ["_start", "_stop", "_measurement"])

Domain Converter

There is also possibility to use custom domain converter to transform data from/to your DomainObject.

Instead of following Influx attributes:

[Measurement("temperature")]
private class Temperature
{
    [Column("location", IsTag = true)] public string Location { get; set; }

    [Column("value")] public double Value { get; set; }

    [Column(IsTimestamp = true)] public DateTime Time { get; set; }
}

you could create own instance of IDomainObjectMapper and use it with QueryApiSync, QueryApi and WriteApi.

var converter = new DomainEntityConverter();
var queryApi = client.GetQueryApiSync(converter)

To satisfy LINQ Query Provider you have to implement IMemberNameResolver:

var resolver = new MemberNameResolver();

var query = from s in InfluxDBQueryable<SensorCustom>.Queryable("my-bucket", "my-org", queryApi, nameResolver)
    where s.Attributes.Any(a => a.Name == "quality" && a.Value == "good")
    select s;

for more details see Any operator and for full example see: CustomDomainMappingAndLinq.

How to debug output Flux Query

var query = (from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", _queryApi)
        where s.SensorId == "id-1"
        where s.Value > 12
        where s.Timestamp > new DateTime(2019, 11, 16, 8, 20, 15, DateTimeKind.Utc)
        where s.Timestamp < new DateTime(2021, 01, 10, 5, 10, 0, DateTimeKind.Utc)
        orderby s.Timestamp
        select s)
    .Take(2)
    .Skip(2);
    
Console.WriteLine("==== Debug LINQ Queryable Flux output ====");
var influxQuery = ((InfluxDBQueryable<Sensor>) query).ToDebugQuery();
foreach (var statement in influxQuery.Extern.Body)
{
    var os = statement as OptionStatement;
    var va = os?.Assignment as VariableAssignment;
    var name = va?.Id.Name;
    var value = va?.Init.GetType().GetProperty("Value")?.GetValue(va.Init, null);

    Console.WriteLine($"{name}={value}");
}
Console.WriteLine();
Console.WriteLine(influxQuery._Query);

How to filter by Measurement

By default, as an optimization step, Flux queries generated by LINQ will automatically drop the Start, Stop and Measurement columns:

from(bucket: "my-bucket")
  |> range(start: 0)
  |> drop(columns: ["_start", "_stop", "_measurement"])
  ...

This is because typical POCO classes do not include them:

[Measurement("temperature")]
private class Temperature
{
    [Column("location", IsTag = true)] public string Location { get; set; }
    [Column("value")] public double Value { get; set; }
    [Column(IsTimestamp = true)] public DateTime Time { get; set; }
}

It is, however, possible to utilize the Measurement column in LINQ queries by enabling it in the query optimization settings:

var optimizerSettings =
    new QueryableOptimizerSettings
    {
        DropMeasurementColumn = false,
        
        // Note we can also enable the start and stop columns
        //DropStartColumn = false,
        //DropStopColumn = false
    };

var queryable =
    new InfluxDBQueryable<InfluxPoint>("my-bucket", "my-org", queryApi, new DefaultMemberNameResolver(), optimizerSettings);

var latest =
    await queryable.Where(p => p.Measurement == "temperature")
                   .OrderByDescending(p => p.Time)
                   .ToInfluxQueryable()
                   .GetAsyncEnumerator()
                   .FirstOrDefaultAsync();

private class InfluxPoint
{
    [Column(IsMeasurement = true)] public string Measurement { get; set; }
    [Column("location", IsTag = true)] public string Location { get; set; }
    [Column("value")] public double Value { get; set; }
    [Column(IsTimestamp = true)] public DateTime Time { get; set; }
}

Asynchronous Queries

The LINQ driver also supports asynchronous querying. For asynchronous queries you have to initialize InfluxDBQueryable with asynchronous version of QueryApi and transform IQueryable<T> to IAsyncEnumerable<T>:

var client = new InfluxDBClient("http://localhost:8086", "my-token");
var queryApi = client.GetQueryApi();

var query = from s in InfluxDBQueryable<Sensor>.Queryable("my-bucket", "my-org", queryApi)
    select s;

IAsyncEnumerable<Sensor> enumerable = query
    .ToInfluxQueryable()
    .GetAsyncEnumerator();
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 is compatible. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (3)

Showing the top 3 NuGet packages that depend on InfluxDB.Client.Linq:

Package Downloads
DeerNet.InfluxDb2

Package Description

MicroHeart.InfluxDB

Package Description

ToolNET.InfluxDB.SDK

时序数据库InfluxDB操作SDK

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
4.19.0-dev.15190 286 12/5/2024
4.19.0-dev.15189 77 12/5/2024
4.19.0-dev.15188 86 12/5/2024
4.19.0-dev.15178 88 12/5/2024
4.19.0-dev.15177 91 12/5/2024
4.19.0-dev.14906 155 10/2/2024
4.19.0-dev.14897 89 10/2/2024
4.19.0-dev.14896 86 10/2/2024
4.19.0-dev.14895 95 10/2/2024
4.19.0-dev.14811 148 9/13/2024
4.18.0 153,126 9/13/2024
4.18.0-dev.14769 104 9/4/2024
4.18.0-dev.14743 92 9/3/2024
4.18.0-dev.14694 82 9/3/2024
4.18.0-dev.14693 79 9/3/2024
4.18.0-dev.14692 85 9/3/2024
4.18.0-dev.14618 90 9/2/2024
4.18.0-dev.14609 84 9/2/2024
4.18.0-dev.14592 82 9/2/2024
4.18.0-dev.14446 108 8/19/2024
4.18.0-dev.14414 102 8/12/2024
4.17.0 14,268 8/12/2024
4.17.0-dev.headers.read.1 125 7/22/2024
4.17.0-dev.14350 85 8/5/2024
4.17.0-dev.14333 76 8/5/2024
4.17.0-dev.14300 78 8/5/2024
4.17.0-dev.14291 81 8/5/2024
4.17.0-dev.14189 98 7/23/2024
4.17.0-dev.14179 98 7/22/2024
4.17.0-dev.14101 178 7/1/2024
4.17.0-dev.14100 98 7/1/2024
4.17.0-dev.14044 107 6/24/2024
4.16.0 13,493 6/24/2024
4.16.0-dev.13990 103 6/3/2024
4.16.0-dev.13973 91 6/3/2024
4.16.0-dev.13972 95 6/3/2024
4.16.0-dev.13963 101 6/3/2024
4.16.0-dev.13962 92 6/3/2024
4.16.0-dev.13881 91 6/3/2024
4.16.0-dev.13775 122 5/17/2024
4.16.0-dev.13702 100 5/17/2024
4.15.0 3,736 5/17/2024
4.15.0-dev.13674 107 5/14/2024
4.15.0-dev.13567 113 4/2/2024
4.15.0-dev.13558 117 4/2/2024
4.15.0-dev.13525 103 4/2/2024
4.15.0-dev.13524 97 4/2/2024
4.15.0-dev.13433 114 3/7/2024
4.15.0-dev.13432 110 3/7/2024
4.15.0-dev.13407 110 3/7/2024
4.15.0-dev.13390 93 3/7/2024
4.15.0-dev.13388 94 3/7/2024
4.15.0-dev.13282 97 3/6/2024
4.15.0-dev.13257 106 3/6/2024
4.15.0-dev.13113 271 2/1/2024
4.15.0-dev.13104 91 2/1/2024
4.15.0-dev.13081 105 2/1/2024
4.15.0-dev.13040 96 2/1/2024
4.15.0-dev.13039 95 2/1/2024
4.15.0-dev.12863 154 1/8/2024
4.15.0-dev.12846 118 1/8/2024
4.15.0-dev.12837 99 1/8/2024
4.15.0-dev.12726 188 12/1/2023
4.15.0-dev.12725 110 12/1/2023
4.15.0-dev.12724 109 12/1/2023
4.15.0-dev.12691 119 12/1/2023
4.15.0-dev.12658 106 12/1/2023
4.15.0-dev.12649 116 12/1/2023
4.15.0-dev.12624 108 12/1/2023
4.15.0-dev.12471 126 11/7/2023
4.15.0-dev.12462 103 11/7/2023
4.14.0 70,167 11/7/2023
4.14.0-dev.12437 119 11/7/2023
4.14.0-dev.12343 125 11/2/2023
4.14.0-dev.12310 115 11/2/2023
4.14.0-dev.12284 113 11/1/2023
4.14.0-dev.12235 110 11/1/2023
4.14.0-dev.12226 107 11/1/2023
4.14.0-dev.11972 286 8/8/2023
4.14.0-dev.11915 173 7/31/2023
4.14.0-dev.11879 187 7/28/2023
4.13.0 23,784 7/28/2023
4.13.0-dev.11854 152 7/28/2023
4.13.0-dev.11814 159 7/21/2023
4.13.0-dev.11771 155 7/19/2023
4.13.0-dev.11770 168 7/19/2023
4.13.0-dev.11728 157 7/18/2023
4.13.0-dev.11686 146 7/17/2023
4.13.0-dev.11685 158 7/17/2023
4.13.0-dev.11676 162 7/17/2023
4.13.0-dev.11479 159 6/27/2023
4.13.0-dev.11478 156 6/27/2023
4.13.0-dev.11477 162 6/27/2023
4.13.0-dev.11396 170 6/19/2023
4.13.0-dev.11395 152 6/19/2023
4.13.0-dev.11342 165 6/15/2023
4.13.0-dev.11330 177 6/12/2023
4.13.0-dev.11305 169 6/12/2023
4.13.0-dev.11296 169 6/12/2023
4.13.0-dev.11217 174 6/6/2023
4.13.0-dev.11089 164 5/30/2023
4.13.0-dev.11064 168 5/30/2023
4.13.0-dev.10998 158 5/29/2023
4.13.0-dev.10989 172 5/29/2023
4.13.0-dev.10871 172 5/8/2023
4.13.0-dev.10870 149 5/8/2023
4.13.0-dev.10819 178 4/28/2023
4.12.0 14,143 4/28/2023
4.12.0-dev.10777 169 4/27/2023
4.12.0-dev.10768 168 4/27/2023
4.12.0-dev.10759 170 4/27/2023
4.12.0-dev.10742 169 4/27/2023
4.12.0-dev.10685 156 4/27/2023
4.12.0-dev.10684 161 4/27/2023
4.12.0-dev.10643 166 4/27/2023
4.12.0-dev.10642 168 4/27/2023
4.12.0-dev.10569 168 4/27/2023
4.12.0-dev.10193 207 2/23/2023
4.11.0 25,967 2/23/2023
4.11.0-dev.10176 173 2/23/2023
4.11.0-dev.10059 291 1/26/2023
4.10.0 9,005 1/26/2023
4.10.0-dev.10033 202 1/25/2023
4.10.0-dev.10032 202 1/25/2023
4.10.0-dev.10031 202 1/25/2023
4.10.0-dev.9936 2,277 12/26/2022
4.10.0-dev.9935 195 12/26/2022
4.10.0-dev.9881 186 12/21/2022
4.10.0-dev.9880 187 12/21/2022
4.10.0-dev.9818 191 12/16/2022
4.10.0-dev.9773 180 12/12/2022
4.10.0-dev.9756 181 12/12/2022
4.10.0-dev.9693 177 12/6/2022
4.9.0 10,489 12/6/2022
4.9.0-dev.9684 191 12/6/2022
4.9.0-dev.9666 187 12/6/2022
4.9.0-dev.9617 196 12/6/2022
4.9.0-dev.9478 187 12/5/2022
4.9.0-dev.9469 194 12/5/2022
4.9.0-dev.9444 180 12/5/2022
4.9.0-dev.9411 175 12/5/2022
4.9.0-dev.9350 182 12/1/2022
4.8.0 1,688 12/1/2022
4.8.0-dev.9324 186 11/30/2022
4.8.0-dev.9232 188 11/28/2022
4.8.0-dev.9223 177 11/28/2022
4.8.0-dev.9222 192 11/28/2022
4.8.0-dev.9117 198 11/21/2022
4.8.0-dev.9108 187 11/21/2022
4.8.0-dev.9099 196 11/21/2022
4.8.0-dev.9029 196 11/16/2022
4.8.0-dev.8971 185 11/15/2022
4.8.0-dev.8961 197 11/14/2022
4.8.0-dev.8928 196 11/14/2022
4.8.0-dev.8899 209 11/14/2022
4.8.0-dev.8898 194 11/14/2022
4.8.0-dev.8839 210 11/14/2022
4.8.0-dev.8740 184 11/7/2022
4.8.0-dev.8725 183 11/7/2022
4.8.0-dev.8648 187 11/3/2022
4.7.0 25,581 11/3/2022
4.7.0-dev.8625 193 11/2/2022
4.7.0-dev.8594 201 10/31/2022
4.7.0-dev.8579 191 10/31/2022
4.7.0-dev.8557 186 10/31/2022
4.7.0-dev.8540 169 10/31/2022
4.7.0-dev.8518 184 10/31/2022
4.7.0-dev.8517 199 10/31/2022
4.7.0-dev.8509 189 10/31/2022
4.7.0-dev.8377 202 10/26/2022
4.7.0-dev.8360 194 10/25/2022
4.7.0-dev.8350 207 10/24/2022
4.7.0-dev.8335 203 10/24/2022
4.7.0-dev.8334 198 10/24/2022
4.7.0-dev.8223 244 10/19/2022
4.7.0-dev.8178 186 10/17/2022
4.7.0-dev.8170 185 10/17/2022
4.7.0-dev.8148 191 10/17/2022
4.7.0-dev.8133 197 10/17/2022
4.7.0-dev.8097 181 10/17/2022
4.7.0-dev.8034 213 10/11/2022
4.7.0-dev.8025 190 10/11/2022
4.7.0-dev.8009 208 10/10/2022
4.7.0-dev.8001 204 10/10/2022
4.7.0-dev.7959 184 10/4/2022
4.7.0-dev.7905 194 9/30/2022
4.7.0-dev.7875 188 9/29/2022
4.6.0 2,795 9/29/2022
4.6.0-dev.7832 204 9/29/2022
4.6.0-dev.7817 202 9/29/2022
4.6.0-dev.7779 208 9/27/2022
4.6.0-dev.7778 217 9/27/2022
4.6.0-dev.7734 204 9/26/2022
4.6.0-dev.7733 203 9/26/2022
4.6.0-dev.7677 206 9/20/2022
4.6.0-dev.7650 204 9/16/2022
4.6.0-dev.7626 265 9/14/2022
4.6.0-dev.7618 259 9/14/2022
4.6.0-dev.7574 187 9/13/2022
4.6.0-dev.7572 199 9/13/2022
4.6.0-dev.7528 182 9/12/2022
4.6.0-dev.7502 205 9/9/2022
4.6.0-dev.7479 229 9/8/2022
4.6.0-dev.7471 206 9/8/2022
4.6.0-dev.7447 203 9/7/2022
4.6.0-dev.7425 194 9/7/2022
4.6.0-dev.7395 192 9/6/2022
4.6.0-dev.7344 195 8/31/2022
4.6.0-dev.7329 199 8/31/2022
4.6.0-dev.7292 192 8/30/2022
4.6.0-dev.7240 203 8/29/2022
4.5.0 2,961 8/29/2022
4.5.0-dev.7216 191 8/27/2022
4.5.0-dev.7147 203 8/22/2022
4.5.0-dev.7134 206 8/17/2022
4.5.0-dev.7096 205 8/15/2022
4.5.0-dev.7070 217 8/11/2022
4.5.0-dev.7040 238 8/10/2022
4.5.0-dev.7011 206 8/3/2022
4.5.0-dev.6987 222 8/1/2022
4.5.0-dev.6962 220 7/29/2022
4.4.0 14,835 7/29/2022
4.4.0-dev.6901 213 7/25/2022
4.4.0-dev.6843 205 7/19/2022
4.4.0-dev.6804 222 7/19/2022
4.4.0-dev.6789 208 7/19/2022
4.4.0-dev.6760 211 7/19/2022
4.4.0-dev.6705 224 7/14/2022
4.4.0-dev.6663 253 6/24/2022
4.4.0-dev.6655 213 6/24/2022
4.3.0 18,854 6/24/2022
4.3.0-dev.multiple.buckets3 239 6/21/2022
4.3.0-dev.multiple.buckets2 214 6/17/2022
4.3.0-dev.multiple.buckets1 204 6/17/2022
4.3.0-dev.6631 209 6/22/2022
4.3.0-dev.6623 216 6/22/2022
4.3.0-dev.6374 220 6/13/2022
4.3.0-dev.6286 224 5/20/2022
4.2.0 2,563 5/20/2022
4.2.0-dev.6257 223 5/13/2022
4.2.0-dev.6248 222 5/12/2022
4.2.0-dev.6233 225 5/12/2022
4.2.0-dev.6194 230 5/10/2022
4.2.0-dev.6193 219 5/10/2022
4.2.0-dev.6158 2,930 5/6/2022
4.2.0-dev.6135 221 5/6/2022
4.2.0-dev.6091 226 4/28/2022
4.2.0-dev.6048 222 4/28/2022
4.2.0-dev.6047 239 4/28/2022
4.2.0-dev.5966 248 4/25/2022
4.2.0-dev.5938 238 4/19/2022
4.1.0 3,507 4/19/2022
4.1.0-dev.5910 421 4/13/2022
4.1.0-dev.5888 234 4/13/2022
4.1.0-dev.5887 234 4/13/2022
4.1.0-dev.5794 237 4/6/2022
4.1.0-dev.5725 244 3/18/2022
4.0.0 9,872 3/18/2022
4.0.0-rc3 478 3/4/2022
4.0.0-rc2 646 2/25/2022
4.0.0-rc1 293 2/18/2022
4.0.0-dev.5709 234 3/18/2022
4.0.0-dev.5684 246 3/15/2022
4.0.0-dev.5630 237 3/4/2022
4.0.0-dev.5607 235 3/3/2022
4.0.0-dev.5579 246 2/25/2022
4.0.0-dev.5556 243 2/24/2022
4.0.0-dev.5555 235 2/24/2022
4.0.0-dev.5497 231 2/23/2022
4.0.0-dev.5489 234 2/23/2022
4.0.0-dev.5460 242 2/23/2022
4.0.0-dev.5444 239 2/22/2022
4.0.0-dev.5333 230 2/17/2022
4.0.0-dev.5303 232 2/16/2022
4.0.0-dev.5280 248 2/16/2022
4.0.0-dev.5279 246 2/16/2022
4.0.0-dev.5241 349 2/15/2022
4.0.0-dev.5225 234 2/15/2022
4.0.0-dev.5217 247 2/15/2022
4.0.0-dev.5209 231 2/15/2022
4.0.0-dev.5200 237 2/14/2022
4.0.0-dev.5188 239 2/10/2022
4.0.0-dev.5180 238 2/10/2022
4.0.0-dev.5172 232 2/10/2022
4.0.0-dev.5130 234 2/10/2022
4.0.0-dev.5122 232 2/9/2022
4.0.0-dev.5103 250 2/9/2022
4.0.0-dev.5097 241 2/9/2022
4.0.0-dev.5091 240 2/9/2022
4.0.0-dev.5084 239 2/8/2022
3.4.0-dev.5263 252 2/15/2022
3.4.0-dev.4986 245 2/7/2022
3.4.0-dev.4968 258 2/4/2022
3.3.0 8,804 2/4/2022
3.3.0-dev.4889 245 2/3/2022
3.3.0-dev.4865 246 2/1/2022
3.3.0-dev.4823 265 1/19/2022
3.3.0-dev.4691 257 1/7/2022
3.3.0-dev.4557 1,460 11/26/2021
3.2.0 6,020 11/26/2021
3.2.0-dev.4533 4,960 11/24/2021
3.2.0-dev.4484 320 11/11/2021
3.2.0-dev.4475 292 11/10/2021
3.2.0-dev.4387 269 10/26/2021
3.2.0-dev.4363 287 10/22/2021
3.2.0-dev.4356 288 10/22/2021
3.1.0 1,900 10/22/2021
3.1.0-dev.4303 279 10/18/2021
3.1.0-dev.4293 285 10/15/2021
3.1.0-dev.4286 267 10/15/2021
3.1.0-dev.4240 299 10/12/2021
3.1.0-dev.4202 270 10/11/2021
3.1.0-dev.4183 306 10/11/2021
3.1.0-dev.4131 269 10/8/2021
3.1.0-dev.3999 272 10/5/2021
3.1.0-dev.3841 359 9/29/2021
3.1.0-dev.3798 277 9/17/2021
3.0.0 1,321 9/17/2021
3.0.0-dev.3726 623 8/31/2021
3.0.0-dev.3719 266 8/31/2021
3.0.0-dev.3671 279 8/20/2021
2.2.0-dev.3652 279 8/20/2021
2.1.0 1,671 8/20/2021
2.1.0-dev.3605 290 8/17/2021
2.1.0-dev.3584 282 8/16/2021
2.1.0-dev.3558 270 8/16/2021
2.1.0-dev.3527 312 7/29/2021
2.1.0-dev.3519 320 7/29/2021
2.1.0-dev.3490 266 7/20/2021
2.1.0-dev.3445 295 7/12/2021
2.1.0-dev.3434 326 7/9/2021
2.0.0 9,136 7/9/2021
2.0.0-dev.3401 306 6/25/2021
2.0.0-dev.3368 293 6/23/2021
2.0.0-dev.3361 306 6/23/2021
2.0.0-dev.3330 299 6/17/2021
2.0.0-dev.3291 302 6/16/2021
1.20.0-dev.3218 309 6/4/2021
1.19.0 1,048 6/4/2021
1.19.0-dev.3204 288 6/3/2021
1.19.0-dev.3160 278 6/2/2021
1.19.0-dev.3159 270 6/2/2021
1.19.0-dev.3084 934 5/7/2021
1.19.0-dev.3051 300 5/5/2021
1.19.0-dev.3044 284 5/5/2021
1.19.0-dev.3008 282 4/30/2021
1.18.0 1,353 4/30/2021
1.18.0-dev.2973 301 4/27/2021
1.18.0-dev.2930 289 4/16/2021
1.18.0-dev.2919 284 4/13/2021
1.18.0-dev.2893 277 4/12/2021
1.18.0-dev.2880 294 4/12/2021
1.18.0-dev.2856 284 4/7/2021
1.18.0-dev.2830 374 4/1/2021
1.18.0-dev.2816 289 4/1/2021
1.17.0 914 4/1/2021
1.17.0-dev.linq.17 897 3/18/2021
1.17.0-dev.linq.16 283 3/16/2021
1.17.0-dev.linq.15 311 3/15/2021
1.17.0-dev.linq.14 322 3/12/2021
1.17.0-dev.linq.13 352 3/11/2021
1.17.0-dev.linq.12 300 3/10/2021
1.17.0-dev.linq.11 299 3/8/2021
1.17.0-dev.2776 320 3/26/2021
1.17.0-dev.2713 333 3/25/2021
1.16.0-dev.linq.10 1,347 2/4/2021
1.15.0-dev.linq.9 323 2/4/2021
1.15.0-dev.linq.8 293 1/28/2021
1.15.0-dev.linq.7 305 1/27/2021
1.15.0-dev.linq.6 321 1/20/2021
1.15.0-dev.linq.5 352 1/19/2021
1.15.0-dev.linq.4 302 1/15/2021
1.15.0-dev.linq.3 284 1/14/2021
1.15.0-dev.linq.2 298 1/13/2021
1.15.0-dev.linq.1 323 1/12/2021