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.
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.
// Install InfluxDB.Client.Linq as a Cake Addin
#addin nuget:?package=InfluxDB.Client.Linq&version=4.17.0-dev.headers.read.1&prerelease

// Install InfluxDB.Client.Linq as a Cake Tool
#tool nuget:?package=InfluxDB.Client.Linq&version=4.17.0-dev.headers.read.1&prerelease                

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. 
.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 (4)

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

Package Downloads
SpmisNet.Data

Package Description

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.18.0-dev.14769 30 9/4/2024
4.18.0-dev.14743 35 9/3/2024
4.18.0-dev.14694 33 9/3/2024
4.18.0-dev.14693 31 9/3/2024
4.18.0-dev.14692 29 9/3/2024
4.18.0-dev.14618 27 9/2/2024
4.18.0-dev.14609 28 9/2/2024
4.18.0-dev.14592 29 9/2/2024
4.18.0-dev.14446 45 8/19/2024
4.18.0-dev.14414 35 8/12/2024
4.17.0 1,309 8/12/2024
4.17.0-dev.headers.read.1 60 7/22/2024
4.17.0-dev.14350 25 8/5/2024
4.17.0-dev.14333 28 8/5/2024
4.17.0-dev.14300 27 8/5/2024
4.17.0-dev.14291 26 8/5/2024
4.17.0-dev.14189 36 7/23/2024
4.17.0-dev.14179 38 7/22/2024
4.17.0-dev.14101 75 7/1/2024
4.17.0-dev.14100 47 7/1/2024
4.17.0-dev.14044 50 6/24/2024
4.16.0 2,740 6/24/2024
4.16.0-dev.13990 53 6/3/2024
4.16.0-dev.13973 42 6/3/2024
4.16.0-dev.13972 42 6/3/2024
4.16.0-dev.13963 51 6/3/2024
4.16.0-dev.13962 45 6/3/2024
4.16.0-dev.13881 47 6/3/2024
4.16.0-dev.13775 62 5/17/2024
4.16.0-dev.13702 52 5/17/2024
4.15.0 1,791 5/17/2024
4.15.0-dev.13674 53 5/14/2024
4.15.0-dev.13567 66 4/2/2024
4.15.0-dev.13558 48 4/2/2024
4.15.0-dev.13525 51 4/2/2024
4.15.0-dev.13524 47 4/2/2024
4.15.0-dev.13433 55 3/7/2024
4.15.0-dev.13432 58 3/7/2024
4.15.0-dev.13407 58 3/7/2024
4.15.0-dev.13390 54 3/7/2024
4.15.0-dev.13388 50 3/7/2024
4.15.0-dev.13282 57 3/6/2024
4.15.0-dev.13257 50 3/6/2024
4.15.0-dev.13113 220 2/1/2024
4.15.0-dev.13104 47 2/1/2024
4.15.0-dev.13081 54 2/1/2024
4.15.0-dev.13040 50 2/1/2024
4.15.0-dev.13039 51 2/1/2024
4.15.0-dev.12863 96 1/8/2024
4.15.0-dev.12846 73 1/8/2024
4.15.0-dev.12837 56 1/8/2024
4.15.0-dev.12726 143 12/1/2023
4.15.0-dev.12725 59 12/1/2023
4.15.0-dev.12724 65 12/1/2023
4.15.0-dev.12691 67 12/1/2023
4.15.0-dev.12658 60 12/1/2023
4.15.0-dev.12649 65 12/1/2023
4.15.0-dev.12624 62 12/1/2023
4.15.0-dev.12471 91 11/7/2023
4.15.0-dev.12462 65 11/7/2023
4.14.0 40,279 11/7/2023
4.14.0-dev.12437 66 11/7/2023
4.14.0-dev.12343 73 11/2/2023
4.14.0-dev.12310 63 11/2/2023
4.14.0-dev.12284 69 11/1/2023
4.14.0-dev.12235 67 11/1/2023
4.14.0-dev.12226 64 11/1/2023
4.14.0-dev.11972 196 8/8/2023
4.14.0-dev.11915 102 7/31/2023
4.14.0-dev.11879 113 7/28/2023
4.13.0 20,579 7/28/2023
4.13.0-dev.11854 85 7/28/2023
4.13.0-dev.11814 96 7/21/2023
4.13.0-dev.11771 87 7/19/2023
4.13.0-dev.11770 92 7/19/2023
4.13.0-dev.11728 84 7/18/2023
4.13.0-dev.11686 81 7/17/2023
4.13.0-dev.11685 81 7/17/2023
4.13.0-dev.11676 97 7/17/2023
4.13.0-dev.11479 77 6/27/2023
4.13.0-dev.11478 83 6/27/2023
4.13.0-dev.11477 77 6/27/2023
4.13.0-dev.11396 91 6/19/2023
4.13.0-dev.11395 76 6/19/2023
4.13.0-dev.11342 84 6/15/2023
4.13.0-dev.11330 88 6/12/2023
4.13.0-dev.11305 87 6/12/2023
4.13.0-dev.11296 82 6/12/2023
4.13.0-dev.11217 90 6/6/2023
4.13.0-dev.11089 83 5/30/2023
4.13.0-dev.11064 84 5/30/2023
4.13.0-dev.10998 79 5/29/2023
4.13.0-dev.10989 84 5/29/2023
4.13.0-dev.10871 83 5/8/2023
4.13.0-dev.10870 74 5/8/2023
4.13.0-dev.10819 102 4/28/2023
4.12.0 11,850 4/28/2023
4.12.0-dev.10777 84 4/27/2023
4.12.0-dev.10768 95 4/27/2023
4.12.0-dev.10759 95 4/27/2023
4.12.0-dev.10742 88 4/27/2023
4.12.0-dev.10685 81 4/27/2023
4.12.0-dev.10684 85 4/27/2023
4.12.0-dev.10643 87 4/27/2023
4.12.0-dev.10642 81 4/27/2023
4.12.0-dev.10569 87 4/27/2023
4.12.0-dev.10193 119 2/23/2023
4.11.0 18,381 2/23/2023
4.11.0-dev.10176 98 2/23/2023
4.11.0-dev.10059 203 1/26/2023
4.10.0 5,578 1/26/2023
4.10.0-dev.10033 116 1/25/2023
4.10.0-dev.10032 118 1/25/2023
4.10.0-dev.10031 115 1/25/2023
4.10.0-dev.9936 2,182 12/26/2022
4.10.0-dev.9935 111 12/26/2022
4.10.0-dev.9881 105 12/21/2022
4.10.0-dev.9880 103 12/21/2022
4.10.0-dev.9818 112 12/16/2022
4.10.0-dev.9773 102 12/12/2022
4.10.0-dev.9756 102 12/12/2022
4.10.0-dev.9693 98 12/6/2022
4.9.0 9,059 12/6/2022
4.9.0-dev.9684 99 12/6/2022
4.9.0-dev.9666 111 12/6/2022
4.9.0-dev.9617 100 12/6/2022
4.9.0-dev.9478 99 12/5/2022
4.9.0-dev.9469 116 12/5/2022
4.9.0-dev.9444 98 12/5/2022
4.9.0-dev.9411 93 12/5/2022
4.9.0-dev.9350 103 12/1/2022
4.8.0 1,573 12/1/2022
4.8.0-dev.9324 98 11/30/2022
4.8.0-dev.9232 103 11/28/2022
4.8.0-dev.9223 104 11/28/2022
4.8.0-dev.9222 107 11/28/2022
4.8.0-dev.9117 118 11/21/2022
4.8.0-dev.9108 103 11/21/2022
4.8.0-dev.9099 109 11/21/2022
4.8.0-dev.9029 105 11/16/2022
4.8.0-dev.8971 109 11/15/2022
4.8.0-dev.8961 108 11/14/2022
4.8.0-dev.8928 107 11/14/2022
4.8.0-dev.8899 117 11/14/2022
4.8.0-dev.8898 111 11/14/2022
4.8.0-dev.8839 122 11/14/2022
4.8.0-dev.8740 101 11/7/2022
4.8.0-dev.8725 106 11/7/2022
4.8.0-dev.8648 105 11/3/2022
4.7.0 23,470 11/3/2022
4.7.0-dev.8625 113 11/2/2022
4.7.0-dev.8594 113 10/31/2022
4.7.0-dev.8579 107 10/31/2022
4.7.0-dev.8557 105 10/31/2022
4.7.0-dev.8540 97 10/31/2022
4.7.0-dev.8518 101 10/31/2022
4.7.0-dev.8517 110 10/31/2022
4.7.0-dev.8509 107 10/31/2022
4.7.0-dev.8377 106 10/26/2022
4.7.0-dev.8360 119 10/25/2022
4.7.0-dev.8350 112 10/24/2022
4.7.0-dev.8335 115 10/24/2022
4.7.0-dev.8334 110 10/24/2022
4.7.0-dev.8223 150 10/19/2022
4.7.0-dev.8178 110 10/17/2022
4.7.0-dev.8170 108 10/17/2022
4.7.0-dev.8148 112 10/17/2022
4.7.0-dev.8133 114 10/17/2022
4.7.0-dev.8097 102 10/17/2022
4.7.0-dev.8034 120 10/11/2022
4.7.0-dev.8025 108 10/11/2022
4.7.0-dev.8009 120 10/10/2022
4.7.0-dev.8001 125 10/10/2022
4.7.0-dev.7959 108 10/4/2022
4.7.0-dev.7905 113 9/30/2022
4.7.0-dev.7875 104 9/29/2022
4.6.0 2,665 9/29/2022
4.6.0-dev.7832 118 9/29/2022
4.6.0-dev.7817 117 9/29/2022
4.6.0-dev.7779 132 9/27/2022
4.6.0-dev.7778 127 9/27/2022
4.6.0-dev.7734 119 9/26/2022
4.6.0-dev.7733 119 9/26/2022
4.6.0-dev.7677 120 9/20/2022
4.6.0-dev.7650 126 9/16/2022
4.6.0-dev.7626 174 9/14/2022
4.6.0-dev.7618 171 9/14/2022
4.6.0-dev.7574 106 9/13/2022
4.6.0-dev.7572 105 9/13/2022
4.6.0-dev.7528 103 9/12/2022
4.6.0-dev.7502 118 9/9/2022
4.6.0-dev.7479 131 9/8/2022
4.6.0-dev.7471 122 9/8/2022
4.6.0-dev.7447 108 9/7/2022
4.6.0-dev.7425 107 9/7/2022
4.6.0-dev.7395 101 9/6/2022
4.6.0-dev.7344 112 8/31/2022
4.6.0-dev.7329 106 8/31/2022
4.6.0-dev.7292 98 8/30/2022
4.6.0-dev.7240 113 8/29/2022
4.5.0 2,328 8/29/2022
4.5.0-dev.7216 110 8/27/2022
4.5.0-dev.7147 113 8/22/2022
4.5.0-dev.7134 114 8/17/2022
4.5.0-dev.7096 114 8/15/2022
4.5.0-dev.7070 126 8/11/2022
4.5.0-dev.7040 145 8/10/2022
4.5.0-dev.7011 124 8/3/2022
4.5.0-dev.6987 121 8/1/2022
4.5.0-dev.6962 130 7/29/2022
4.4.0 14,682 7/29/2022
4.4.0-dev.6901 128 7/25/2022
4.4.0-dev.6843 122 7/19/2022
4.4.0-dev.6804 124 7/19/2022
4.4.0-dev.6789 124 7/19/2022
4.4.0-dev.6760 120 7/19/2022
4.4.0-dev.6705 128 7/14/2022
4.4.0-dev.6663 154 6/24/2022
4.4.0-dev.6655 118 6/24/2022
4.3.0 9,099 6/24/2022
4.3.0-dev.multiple.buckets3 148 6/21/2022
4.3.0-dev.multiple.buckets2 114 6/17/2022
4.3.0-dev.multiple.buckets1 115 6/17/2022
4.3.0-dev.6631 115 6/22/2022
4.3.0-dev.6623 123 6/22/2022
4.3.0-dev.6374 126 6/13/2022
4.3.0-dev.6286 122 5/20/2022
4.2.0 2,382 5/20/2022
4.2.0-dev.6257 130 5/13/2022
4.2.0-dev.6248 127 5/12/2022
4.2.0-dev.6233 132 5/12/2022
4.2.0-dev.6194 123 5/10/2022
4.2.0-dev.6193 123 5/10/2022
4.2.0-dev.6158 2,833 5/6/2022
4.2.0-dev.6135 134 5/6/2022
4.2.0-dev.6091 129 4/28/2022
4.2.0-dev.6048 135 4/28/2022
4.2.0-dev.6047 129 4/28/2022
4.2.0-dev.5966 137 4/25/2022
4.2.0-dev.5938 132 4/19/2022
4.1.0 3,372 4/19/2022
4.1.0-dev.5910 327 4/13/2022
4.1.0-dev.5888 131 4/13/2022
4.1.0-dev.5887 139 4/13/2022
4.1.0-dev.5794 139 4/6/2022
4.1.0-dev.5725 144 3/18/2022
4.0.0 6,867 3/18/2022
4.0.0-rc3 372 3/4/2022
4.0.0-rc2 525 2/25/2022
4.0.0-rc1 181 2/18/2022
4.0.0-dev.5709 137 3/18/2022
4.0.0-dev.5684 147 3/15/2022
4.0.0-dev.5630 147 3/4/2022
4.0.0-dev.5607 133 3/3/2022
4.0.0-dev.5579 142 2/25/2022
4.0.0-dev.5556 140 2/24/2022
4.0.0-dev.5555 135 2/24/2022
4.0.0-dev.5497 133 2/23/2022
4.0.0-dev.5489 138 2/23/2022
4.0.0-dev.5460 134 2/23/2022
4.0.0-dev.5444 134 2/22/2022
4.0.0-dev.5333 138 2/17/2022
4.0.0-dev.5303 133 2/16/2022
4.0.0-dev.5280 140 2/16/2022
4.0.0-dev.5279 140 2/16/2022
4.0.0-dev.5241 241 2/15/2022
4.0.0-dev.5225 135 2/15/2022
4.0.0-dev.5217 140 2/15/2022
4.0.0-dev.5209 132 2/15/2022
4.0.0-dev.5200 132 2/14/2022
4.0.0-dev.5188 137 2/10/2022
4.0.0-dev.5180 136 2/10/2022
4.0.0-dev.5172 133 2/10/2022
4.0.0-dev.5130 131 2/10/2022
4.0.0-dev.5122 139 2/9/2022
4.0.0-dev.5103 146 2/9/2022
4.0.0-dev.5097 145 2/9/2022
4.0.0-dev.5091 138 2/9/2022
4.0.0-dev.5084 140 2/8/2022
3.4.0-dev.5263 145 2/15/2022
3.4.0-dev.4986 140 2/7/2022
3.4.0-dev.4968 155 2/4/2022
3.3.0 8,557 2/4/2022
3.3.0-dev.4889 143 2/3/2022
3.3.0-dev.4865 151 2/1/2022
3.3.0-dev.4823 154 1/19/2022
3.3.0-dev.4691 146 1/7/2022
3.3.0-dev.4557 1,362 11/26/2021
3.2.0 5,827 11/26/2021
3.2.0-dev.4533 4,857 11/24/2021
3.2.0-dev.4484 219 11/11/2021
3.2.0-dev.4475 191 11/10/2021
3.2.0-dev.4387 167 10/26/2021
3.2.0-dev.4363 182 10/22/2021
3.2.0-dev.4356 180 10/22/2021
3.1.0 1,762 10/22/2021
3.1.0-dev.4303 181 10/18/2021
3.1.0-dev.4293 184 10/15/2021
3.1.0-dev.4286 163 10/15/2021
3.1.0-dev.4240 200 10/12/2021
3.1.0-dev.4202 159 10/11/2021
3.1.0-dev.4183 195 10/11/2021
3.1.0-dev.4131 163 10/8/2021
3.1.0-dev.3999 178 10/5/2021
3.1.0-dev.3841 250 9/29/2021
3.1.0-dev.3798 177 9/17/2021
3.0.0 1,182 9/17/2021
3.0.0-dev.3726 517 8/31/2021
3.0.0-dev.3719 164 8/31/2021
3.0.0-dev.3671 176 8/20/2021
2.2.0-dev.3652 172 8/20/2021
2.1.0 1,524 8/20/2021
2.1.0-dev.3605 171 8/17/2021
2.1.0-dev.3584 177 8/16/2021
2.1.0-dev.3558 160 8/16/2021
2.1.0-dev.3527 206 7/29/2021
2.1.0-dev.3519 214 7/29/2021
2.1.0-dev.3490 158 7/20/2021
2.1.0-dev.3445 190 7/12/2021
2.1.0-dev.3434 224 7/9/2021
2.0.0 8,995 7/9/2021
2.0.0-dev.3401 204 6/25/2021
2.0.0-dev.3368 190 6/23/2021
2.0.0-dev.3361 195 6/23/2021
2.0.0-dev.3330 198 6/17/2021
2.0.0-dev.3291 199 6/16/2021
1.20.0-dev.3218 217 6/4/2021
1.19.0 890 6/4/2021
1.19.0-dev.3204 185 6/3/2021
1.19.0-dev.3160 170 6/2/2021
1.19.0-dev.3159 168 6/2/2021
1.19.0-dev.3084 827 5/7/2021
1.19.0-dev.3051 185 5/5/2021
1.19.0-dev.3044 190 5/5/2021
1.19.0-dev.3008 184 4/30/2021
1.18.0 1,213 4/30/2021
1.18.0-dev.2973 194 4/27/2021
1.18.0-dev.2930 183 4/16/2021
1.18.0-dev.2919 173 4/13/2021
1.18.0-dev.2893 166 4/12/2021
1.18.0-dev.2880 178 4/12/2021
1.18.0-dev.2856 179 4/7/2021
1.18.0-dev.2830 273 4/1/2021
1.18.0-dev.2816 179 4/1/2021
1.17.0 725 4/1/2021
1.17.0-dev.linq.17 786 3/18/2021
1.17.0-dev.linq.16 171 3/16/2021
1.17.0-dev.linq.15 206 3/15/2021
1.17.0-dev.linq.14 210 3/12/2021
1.17.0-dev.linq.13 233 3/11/2021
1.17.0-dev.linq.12 190 3/10/2021
1.17.0-dev.linq.11 180 3/8/2021
1.17.0-dev.2776 210 3/26/2021
1.17.0-dev.2713 221 3/25/2021
1.16.0-dev.linq.10 1,227 2/4/2021
1.15.0-dev.linq.9 201 2/4/2021
1.15.0-dev.linq.8 172 1/28/2021
1.15.0-dev.linq.7 191 1/27/2021
1.15.0-dev.linq.6 214 1/20/2021
1.15.0-dev.linq.5 231 1/19/2021
1.15.0-dev.linq.4 196 1/15/2021
1.15.0-dev.linq.3 172 1/14/2021
1.15.0-dev.linq.2 187 1/13/2021
1.15.0-dev.linq.1 204 1/12/2021