InfluxDB.Client.Linq 4.17.0-dev.14350

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.14350                
NuGet\Install-Package InfluxDB.Client.Linq -Version 4.17.0-dev.14350                
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.14350" />                
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.14350                
#r "nuget: InfluxDB.Client.Linq, 4.17.0-dev.14350"                
#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.14350&prerelease

// Install InfluxDB.Client.Linq as a Cake Tool
#tool nuget:?package=InfluxDB.Client.Linq&version=4.17.0-dev.14350&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.19.0-dev.14811 36 9/13/2024
4.18.0 66 9/13/2024
4.18.0-dev.14769 44 9/4/2024
4.18.0-dev.14743 43 9/3/2024
4.18.0-dev.14694 39 9/3/2024
4.18.0-dev.14693 37 9/3/2024
4.18.0-dev.14692 37 9/3/2024
4.18.0-dev.14618 34 9/2/2024
4.18.0-dev.14609 34 9/2/2024
4.18.0-dev.14592 35 9/2/2024
4.18.0-dev.14446 65 8/19/2024
4.18.0-dev.14414 56 8/12/2024
4.17.0 1,582 8/12/2024
4.17.0-dev.headers.read.1 65 7/22/2024
4.17.0-dev.14350 31 8/5/2024
4.17.0-dev.14333 32 8/5/2024
4.17.0-dev.14300 31 8/5/2024
4.17.0-dev.14291 30 8/5/2024
4.17.0-dev.14189 41 7/23/2024
4.17.0-dev.14179 43 7/22/2024
4.17.0-dev.14101 97 7/1/2024
4.17.0-dev.14100 52 7/1/2024
4.17.0-dev.14044 54 6/24/2024
4.16.0 3,045 6/24/2024
4.16.0-dev.13990 57 6/3/2024
4.16.0-dev.13973 47 6/3/2024
4.16.0-dev.13972 47 6/3/2024
4.16.0-dev.13963 55 6/3/2024
4.16.0-dev.13962 49 6/3/2024
4.16.0-dev.13881 52 6/3/2024
4.16.0-dev.13775 65 5/17/2024
4.16.0-dev.13702 55 5/17/2024
4.15.0 1,858 5/17/2024
4.15.0-dev.13674 62 5/14/2024
4.15.0-dev.13567 70 4/2/2024
4.15.0-dev.13558 52 4/2/2024
4.15.0-dev.13525 55 4/2/2024
4.15.0-dev.13524 51 4/2/2024
4.15.0-dev.13433 58 3/7/2024
4.15.0-dev.13432 61 3/7/2024
4.15.0-dev.13407 61 3/7/2024
4.15.0-dev.13390 57 3/7/2024
4.15.0-dev.13388 53 3/7/2024
4.15.0-dev.13282 60 3/6/2024
4.15.0-dev.13257 53 3/6/2024
4.15.0-dev.13113 224 2/1/2024
4.15.0-dev.13104 51 2/1/2024
4.15.0-dev.13081 58 2/1/2024
4.15.0-dev.13040 54 2/1/2024
4.15.0-dev.13039 55 2/1/2024
4.15.0-dev.12863 100 1/8/2024
4.15.0-dev.12846 77 1/8/2024
4.15.0-dev.12837 60 1/8/2024
4.15.0-dev.12726 147 12/1/2023
4.15.0-dev.12725 63 12/1/2023
4.15.0-dev.12724 69 12/1/2023
4.15.0-dev.12691 71 12/1/2023
4.15.0-dev.12658 64 12/1/2023
4.15.0-dev.12649 69 12/1/2023
4.15.0-dev.12624 66 12/1/2023
4.15.0-dev.12471 95 11/7/2023
4.15.0-dev.12462 69 11/7/2023
4.14.0 40,986 11/7/2023
4.14.0-dev.12437 70 11/7/2023
4.14.0-dev.12343 81 11/2/2023
4.14.0-dev.12310 67 11/2/2023
4.14.0-dev.12284 73 11/1/2023
4.14.0-dev.12235 71 11/1/2023
4.14.0-dev.12226 68 11/1/2023
4.14.0-dev.11972 198 8/8/2023
4.14.0-dev.11915 104 7/31/2023
4.14.0-dev.11879 115 7/28/2023
4.13.0 20,652 7/28/2023
4.13.0-dev.11854 87 7/28/2023
4.13.0-dev.11814 99 7/21/2023
4.13.0-dev.11771 89 7/19/2023
4.13.0-dev.11770 94 7/19/2023
4.13.0-dev.11728 86 7/18/2023
4.13.0-dev.11686 83 7/17/2023
4.13.0-dev.11685 83 7/17/2023
4.13.0-dev.11676 99 7/17/2023
4.13.0-dev.11479 79 6/27/2023
4.13.0-dev.11478 85 6/27/2023
4.13.0-dev.11477 79 6/27/2023
4.13.0-dev.11396 93 6/19/2023
4.13.0-dev.11395 78 6/19/2023
4.13.0-dev.11342 86 6/15/2023
4.13.0-dev.11330 90 6/12/2023
4.13.0-dev.11305 89 6/12/2023
4.13.0-dev.11296 84 6/12/2023
4.13.0-dev.11217 92 6/6/2023
4.13.0-dev.11089 85 5/30/2023
4.13.0-dev.11064 86 5/30/2023
4.13.0-dev.10998 87 5/29/2023
4.13.0-dev.10989 92 5/29/2023
4.13.0-dev.10871 85 5/8/2023
4.13.0-dev.10870 76 5/8/2023
4.13.0-dev.10819 104 4/28/2023
4.12.0 11,944 4/28/2023
4.12.0-dev.10777 86 4/27/2023
4.12.0-dev.10768 97 4/27/2023
4.12.0-dev.10759 97 4/27/2023
4.12.0-dev.10742 90 4/27/2023
4.12.0-dev.10685 83 4/27/2023
4.12.0-dev.10684 87 4/27/2023
4.12.0-dev.10643 89 4/27/2023
4.12.0-dev.10642 83 4/27/2023
4.12.0-dev.10569 89 4/27/2023
4.12.0-dev.10193 121 2/23/2023
4.11.0 18,518 2/23/2023
4.11.0-dev.10176 100 2/23/2023
4.11.0-dev.10059 205 1/26/2023
4.10.0 5,596 1/26/2023
4.10.0-dev.10033 118 1/25/2023
4.10.0-dev.10032 120 1/25/2023
4.10.0-dev.10031 117 1/25/2023
4.10.0-dev.9936 2,184 12/26/2022
4.10.0-dev.9935 113 12/26/2022
4.10.0-dev.9881 107 12/21/2022
4.10.0-dev.9880 105 12/21/2022
4.10.0-dev.9818 114 12/16/2022
4.10.0-dev.9773 104 12/12/2022
4.10.0-dev.9756 110 12/12/2022
4.10.0-dev.9693 100 12/6/2022
4.9.0 9,100 12/6/2022
4.9.0-dev.9684 101 12/6/2022
4.9.0-dev.9666 113 12/6/2022
4.9.0-dev.9617 102 12/6/2022
4.9.0-dev.9478 101 12/5/2022
4.9.0-dev.9469 118 12/5/2022
4.9.0-dev.9444 100 12/5/2022
4.9.0-dev.9411 95 12/5/2022
4.9.0-dev.9350 105 12/1/2022
4.8.0 1,575 12/1/2022
4.8.0-dev.9324 100 11/30/2022
4.8.0-dev.9232 105 11/28/2022
4.8.0-dev.9223 106 11/28/2022
4.8.0-dev.9222 109 11/28/2022
4.8.0-dev.9117 120 11/21/2022
4.8.0-dev.9108 105 11/21/2022
4.8.0-dev.9099 111 11/21/2022
4.8.0-dev.9029 107 11/16/2022
4.8.0-dev.8971 111 11/15/2022
4.8.0-dev.8961 110 11/14/2022
4.8.0-dev.8928 109 11/14/2022
4.8.0-dev.8899 119 11/14/2022
4.8.0-dev.8898 113 11/14/2022
4.8.0-dev.8839 124 11/14/2022
4.8.0-dev.8740 103 11/7/2022
4.8.0-dev.8725 108 11/7/2022
4.8.0-dev.8648 107 11/3/2022
4.7.0 23,504 11/3/2022
4.7.0-dev.8625 115 11/2/2022
4.7.0-dev.8594 115 10/31/2022
4.7.0-dev.8579 115 10/31/2022
4.7.0-dev.8557 107 10/31/2022
4.7.0-dev.8540 99 10/31/2022
4.7.0-dev.8518 103 10/31/2022
4.7.0-dev.8517 112 10/31/2022
4.7.0-dev.8509 109 10/31/2022
4.7.0-dev.8377 108 10/26/2022
4.7.0-dev.8360 121 10/25/2022
4.7.0-dev.8350 114 10/24/2022
4.7.0-dev.8335 117 10/24/2022
4.7.0-dev.8334 112 10/24/2022
4.7.0-dev.8223 152 10/19/2022
4.7.0-dev.8178 112 10/17/2022
4.7.0-dev.8170 110 10/17/2022
4.7.0-dev.8148 118 10/17/2022
4.7.0-dev.8133 116 10/17/2022
4.7.0-dev.8097 104 10/17/2022
4.7.0-dev.8034 122 10/11/2022
4.7.0-dev.8025 110 10/11/2022
4.7.0-dev.8009 128 10/10/2022
4.7.0-dev.8001 127 10/10/2022
4.7.0-dev.7959 110 10/4/2022
4.7.0-dev.7905 115 9/30/2022
4.7.0-dev.7875 106 9/29/2022
4.6.0 2,667 9/29/2022
4.6.0-dev.7832 120 9/29/2022
4.6.0-dev.7817 119 9/29/2022
4.6.0-dev.7779 134 9/27/2022
4.6.0-dev.7778 129 9/27/2022
4.6.0-dev.7734 121 9/26/2022
4.6.0-dev.7733 121 9/26/2022
4.6.0-dev.7677 122 9/20/2022
4.6.0-dev.7650 128 9/16/2022
4.6.0-dev.7626 176 9/14/2022
4.6.0-dev.7618 173 9/14/2022
4.6.0-dev.7574 113 9/13/2022
4.6.0-dev.7572 107 9/13/2022
4.6.0-dev.7528 105 9/12/2022
4.6.0-dev.7502 120 9/9/2022
4.6.0-dev.7479 133 9/8/2022
4.6.0-dev.7471 124 9/8/2022
4.6.0-dev.7447 110 9/7/2022
4.6.0-dev.7425 109 9/7/2022
4.6.0-dev.7395 103 9/6/2022
4.6.0-dev.7344 114 8/31/2022
4.6.0-dev.7329 108 8/31/2022
4.6.0-dev.7292 100 8/30/2022
4.6.0-dev.7240 115 8/29/2022
4.5.0 2,336 8/29/2022
4.5.0-dev.7216 112 8/27/2022
4.5.0-dev.7147 115 8/22/2022
4.5.0-dev.7134 116 8/17/2022
4.5.0-dev.7096 116 8/15/2022
4.5.0-dev.7070 128 8/11/2022
4.5.0-dev.7040 147 8/10/2022
4.5.0-dev.7011 126 8/3/2022
4.5.0-dev.6987 123 8/1/2022
4.5.0-dev.6962 132 7/29/2022
4.4.0 14,687 7/29/2022
4.4.0-dev.6901 130 7/25/2022
4.4.0-dev.6843 124 7/19/2022
4.4.0-dev.6804 126 7/19/2022
4.4.0-dev.6789 126 7/19/2022
4.4.0-dev.6760 122 7/19/2022
4.4.0-dev.6705 130 7/14/2022
4.4.0-dev.6663 156 6/24/2022
4.4.0-dev.6655 120 6/24/2022
4.3.0 9,311 6/24/2022
4.3.0-dev.multiple.buckets3 150 6/21/2022
4.3.0-dev.multiple.buckets2 116 6/17/2022
4.3.0-dev.multiple.buckets1 117 6/17/2022
4.3.0-dev.6631 117 6/22/2022
4.3.0-dev.6623 125 6/22/2022
4.3.0-dev.6374 128 6/13/2022
4.3.0-dev.6286 124 5/20/2022
4.2.0 2,384 5/20/2022
4.2.0-dev.6257 132 5/13/2022
4.2.0-dev.6248 129 5/12/2022
4.2.0-dev.6233 134 5/12/2022
4.2.0-dev.6194 125 5/10/2022
4.2.0-dev.6193 125 5/10/2022
4.2.0-dev.6158 2,840 5/6/2022
4.2.0-dev.6135 136 5/6/2022
4.2.0-dev.6091 131 4/28/2022
4.2.0-dev.6048 137 4/28/2022
4.2.0-dev.6047 131 4/28/2022
4.2.0-dev.5966 139 4/25/2022
4.2.0-dev.5938 140 4/19/2022
4.1.0 3,374 4/19/2022
4.1.0-dev.5910 329 4/13/2022
4.1.0-dev.5888 133 4/13/2022
4.1.0-dev.5887 141 4/13/2022
4.1.0-dev.5794 141 4/6/2022
4.1.0-dev.5725 146 3/18/2022
4.0.0 6,954 3/18/2022
4.0.0-rc3 374 3/4/2022
4.0.0-rc2 527 2/25/2022
4.0.0-rc1 188 2/18/2022
4.0.0-dev.5709 139 3/18/2022
4.0.0-dev.5684 149 3/15/2022
4.0.0-dev.5630 149 3/4/2022
4.0.0-dev.5607 141 3/3/2022
4.0.0-dev.5579 144 2/25/2022
4.0.0-dev.5556 142 2/24/2022
4.0.0-dev.5555 137 2/24/2022
4.0.0-dev.5497 135 2/23/2022
4.0.0-dev.5489 140 2/23/2022
4.0.0-dev.5460 142 2/23/2022
4.0.0-dev.5444 136 2/22/2022
4.0.0-dev.5333 140 2/17/2022
4.0.0-dev.5303 135 2/16/2022
4.0.0-dev.5280 142 2/16/2022
4.0.0-dev.5279 142 2/16/2022
4.0.0-dev.5241 243 2/15/2022
4.0.0-dev.5225 137 2/15/2022
4.0.0-dev.5217 142 2/15/2022
4.0.0-dev.5209 134 2/15/2022
4.0.0-dev.5200 134 2/14/2022
4.0.0-dev.5188 139 2/10/2022
4.0.0-dev.5180 138 2/10/2022
4.0.0-dev.5172 135 2/10/2022
4.0.0-dev.5130 133 2/10/2022
4.0.0-dev.5122 141 2/9/2022
4.0.0-dev.5103 148 2/9/2022
4.0.0-dev.5097 147 2/9/2022
4.0.0-dev.5091 140 2/9/2022
4.0.0-dev.5084 142 2/8/2022
3.4.0-dev.5263 147 2/15/2022
3.4.0-dev.4986 142 2/7/2022
3.4.0-dev.4968 157 2/4/2022
3.3.0 8,569 2/4/2022
3.3.0-dev.4889 145 2/3/2022
3.3.0-dev.4865 153 2/1/2022
3.3.0-dev.4823 156 1/19/2022
3.3.0-dev.4691 148 1/7/2022
3.3.0-dev.4557 1,364 11/26/2021
3.2.0 5,831 11/26/2021
3.2.0-dev.4533 4,859 11/24/2021
3.2.0-dev.4484 221 11/11/2021
3.2.0-dev.4475 193 11/10/2021
3.2.0-dev.4387 169 10/26/2021
3.2.0-dev.4363 184 10/22/2021
3.2.0-dev.4356 182 10/22/2021
3.1.0 1,765 10/22/2021
3.1.0-dev.4303 183 10/18/2021
3.1.0-dev.4293 186 10/15/2021
3.1.0-dev.4286 165 10/15/2021
3.1.0-dev.4240 202 10/12/2021
3.1.0-dev.4202 161 10/11/2021
3.1.0-dev.4183 197 10/11/2021
3.1.0-dev.4131 165 10/8/2021
3.1.0-dev.3999 180 10/5/2021
3.1.0-dev.3841 252 9/29/2021
3.1.0-dev.3798 179 9/17/2021
3.0.0 1,184 9/17/2021
3.0.0-dev.3726 519 8/31/2021
3.0.0-dev.3719 166 8/31/2021
3.0.0-dev.3671 178 8/20/2021
2.2.0-dev.3652 174 8/20/2021
2.1.0 1,526 8/20/2021
2.1.0-dev.3605 173 8/17/2021
2.1.0-dev.3584 179 8/16/2021
2.1.0-dev.3558 162 8/16/2021
2.1.0-dev.3527 214 7/29/2021
2.1.0-dev.3519 216 7/29/2021
2.1.0-dev.3490 160 7/20/2021
2.1.0-dev.3445 192 7/12/2021
2.1.0-dev.3434 226 7/9/2021
2.0.0 8,997 7/9/2021
2.0.0-dev.3401 206 6/25/2021
2.0.0-dev.3368 193 6/23/2021
2.0.0-dev.3361 197 6/23/2021
2.0.0-dev.3330 200 6/17/2021
2.0.0-dev.3291 201 6/16/2021
1.20.0-dev.3218 220 6/4/2021
1.19.0 892 6/4/2021
1.19.0-dev.3204 187 6/3/2021
1.19.0-dev.3160 172 6/2/2021
1.19.0-dev.3159 170 6/2/2021
1.19.0-dev.3084 829 5/7/2021
1.19.0-dev.3051 187 5/5/2021
1.19.0-dev.3044 192 5/5/2021
1.19.0-dev.3008 186 4/30/2021
1.18.0 1,215 4/30/2021
1.18.0-dev.2973 196 4/27/2021
1.18.0-dev.2930 185 4/16/2021
1.18.0-dev.2919 175 4/13/2021
1.18.0-dev.2893 168 4/12/2021
1.18.0-dev.2880 180 4/12/2021
1.18.0-dev.2856 181 4/7/2021
1.18.0-dev.2830 275 4/1/2021
1.18.0-dev.2816 181 4/1/2021
1.17.0 727 4/1/2021
1.17.0-dev.linq.17 788 3/18/2021
1.17.0-dev.linq.16 173 3/16/2021
1.17.0-dev.linq.15 208 3/15/2021
1.17.0-dev.linq.14 212 3/12/2021
1.17.0-dev.linq.13 235 3/11/2021
1.17.0-dev.linq.12 192 3/10/2021
1.17.0-dev.linq.11 182 3/8/2021
1.17.0-dev.2776 212 3/26/2021
1.17.0-dev.2713 223 3/25/2021
1.16.0-dev.linq.10 1,229 2/4/2021
1.15.0-dev.linq.9 209 2/4/2021
1.15.0-dev.linq.8 174 1/28/2021
1.15.0-dev.linq.7 193 1/27/2021
1.15.0-dev.linq.6 216 1/20/2021
1.15.0-dev.linq.5 233 1/19/2021
1.15.0-dev.linq.4 198 1/15/2021
1.15.0-dev.linq.3 174 1/14/2021
1.15.0-dev.linq.2 189 1/13/2021
1.15.0-dev.linq.1 206 1/12/2021