InfluxDB.Client.Linq 4.19.0-dev.14906

This is a prerelease version of InfluxDB.Client.Linq.
dotnet add package InfluxDB.Client.Linq --version 4.19.0-dev.14906                
NuGet\Install-Package InfluxDB.Client.Linq -Version 4.19.0-dev.14906                
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.19.0-dev.14906" />                
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add InfluxDB.Client.Linq --version 4.19.0-dev.14906                
#r "nuget: InfluxDB.Client.Linq, 4.19.0-dev.14906"                
#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.19.0-dev.14906&prerelease

// Install InfluxDB.Client.Linq as a Cake Tool
#tool nuget:?package=InfluxDB.Client.Linq&version=4.19.0-dev.14906&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.14906 75 10/2/2024
4.19.0-dev.14897 50 10/2/2024
4.19.0-dev.14896 45 10/2/2024
4.19.0-dev.14895 47 10/2/2024
4.19.0-dev.14811 63 9/13/2024
4.18.0 4,235 9/13/2024
4.18.0-dev.14769 64 9/4/2024
4.18.0-dev.14743 57 9/3/2024
4.18.0-dev.14694 54 9/3/2024
4.18.0-dev.14693 51 9/3/2024
4.18.0-dev.14692 49 9/3/2024
4.18.0-dev.14618 48 9/2/2024
4.18.0-dev.14609 48 9/2/2024
4.18.0-dev.14592 49 9/2/2024
4.18.0-dev.14446 74 8/19/2024
4.18.0-dev.14414 67 8/12/2024
4.17.0 4,468 8/12/2024
4.17.0-dev.headers.read.1 76 7/22/2024
4.17.0-dev.14350 47 8/5/2024
4.17.0-dev.14333 42 8/5/2024
4.17.0-dev.14300 40 8/5/2024
4.17.0-dev.14291 40 8/5/2024
4.17.0-dev.14189 53 7/23/2024
4.17.0-dev.14179 54 7/22/2024
4.17.0-dev.14101 131 7/1/2024
4.17.0-dev.14100 61 7/1/2024
4.17.0-dev.14044 64 6/24/2024
4.16.0 5,785 6/24/2024
4.16.0-dev.13990 66 6/3/2024
4.16.0-dev.13973 57 6/3/2024
4.16.0-dev.13972 56 6/3/2024
4.16.0-dev.13963 64 6/3/2024
4.16.0-dev.13962 58 6/3/2024
4.16.0-dev.13881 62 6/3/2024
4.16.0-dev.13775 74 5/17/2024
4.16.0-dev.13702 66 5/17/2024
4.15.0 2,441 5/17/2024
4.15.0-dev.13674 74 5/14/2024
4.15.0-dev.13567 78 4/2/2024
4.15.0-dev.13558 60 4/2/2024
4.15.0-dev.13525 69 4/2/2024
4.15.0-dev.13524 61 4/2/2024
4.15.0-dev.13433 72 3/7/2024
4.15.0-dev.13432 71 3/7/2024
4.15.0-dev.13407 69 3/7/2024
4.15.0-dev.13390 65 3/7/2024
4.15.0-dev.13388 61 3/7/2024
4.15.0-dev.13282 72 3/6/2024
4.15.0-dev.13257 72 3/6/2024
4.15.0-dev.13113 232 2/1/2024
4.15.0-dev.13104 67 2/1/2024
4.15.0-dev.13081 68 2/1/2024
4.15.0-dev.13040 65 2/1/2024
4.15.0-dev.13039 70 2/1/2024
4.15.0-dev.12863 115 1/8/2024
4.15.0-dev.12846 84 1/8/2024
4.15.0-dev.12837 76 1/8/2024
4.15.0-dev.12726 155 12/1/2023
4.15.0-dev.12725 78 12/1/2023
4.15.0-dev.12724 76 12/1/2023
4.15.0-dev.12691 80 12/1/2023
4.15.0-dev.12658 73 12/1/2023
4.15.0-dev.12649 78 12/1/2023
4.15.0-dev.12624 75 12/1/2023
4.15.0-dev.12471 101 11/7/2023
4.15.0-dev.12462 76 11/7/2023
4.14.0 48,193 11/7/2023
4.14.0-dev.12437 78 11/7/2023
4.14.0-dev.12343 90 11/2/2023
4.14.0-dev.12310 77 11/2/2023
4.14.0-dev.12284 79 11/1/2023
4.14.0-dev.12235 78 11/1/2023
4.14.0-dev.12226 75 11/1/2023
4.14.0-dev.11972 210 8/8/2023
4.14.0-dev.11915 113 7/31/2023
4.14.0-dev.11879 122 7/28/2023
4.13.0 21,632 7/28/2023
4.13.0-dev.11854 94 7/28/2023
4.13.0-dev.11814 106 7/21/2023
4.13.0-dev.11771 97 7/19/2023
4.13.0-dev.11770 105 7/19/2023
4.13.0-dev.11728 93 7/18/2023
4.13.0-dev.11686 92 7/17/2023
4.13.0-dev.11685 90 7/17/2023
4.13.0-dev.11676 106 7/17/2023
4.13.0-dev.11479 93 6/27/2023
4.13.0-dev.11478 93 6/27/2023
4.13.0-dev.11477 99 6/27/2023
4.13.0-dev.11396 100 6/19/2023
4.13.0-dev.11395 85 6/19/2023
4.13.0-dev.11342 94 6/15/2023
4.13.0-dev.11330 105 6/12/2023
4.13.0-dev.11305 96 6/12/2023
4.13.0-dev.11296 98 6/12/2023
4.13.0-dev.11217 99 6/6/2023
4.13.0-dev.11089 91 5/30/2023
4.13.0-dev.11064 98 5/30/2023
4.13.0-dev.10998 95 5/29/2023
4.13.0-dev.10989 98 5/29/2023
4.13.0-dev.10871 99 5/8/2023
4.13.0-dev.10870 83 5/8/2023
4.13.0-dev.10819 111 4/28/2023
4.12.0 12,936 4/28/2023
4.12.0-dev.10777 100 4/27/2023
4.12.0-dev.10768 105 4/27/2023
4.12.0-dev.10759 103 4/27/2023
4.12.0-dev.10742 98 4/27/2023
4.12.0-dev.10685 89 4/27/2023
4.12.0-dev.10684 93 4/27/2023
4.12.0-dev.10643 95 4/27/2023
4.12.0-dev.10642 95 4/27/2023
4.12.0-dev.10569 95 4/27/2023
4.12.0-dev.10193 135 2/23/2023
4.11.0 19,782 2/23/2023
4.11.0-dev.10176 106 2/23/2023
4.11.0-dev.10059 211 1/26/2023
4.10.0 6,072 1/26/2023
4.10.0-dev.10033 126 1/25/2023
4.10.0-dev.10032 126 1/25/2023
4.10.0-dev.10031 123 1/25/2023
4.10.0-dev.9936 2,196 12/26/2022
4.10.0-dev.9935 120 12/26/2022
4.10.0-dev.9881 113 12/21/2022
4.10.0-dev.9880 111 12/21/2022
4.10.0-dev.9818 120 12/16/2022
4.10.0-dev.9773 110 12/12/2022
4.10.0-dev.9756 116 12/12/2022
4.10.0-dev.9693 112 12/6/2022
4.9.0 9,425 12/6/2022
4.9.0-dev.9684 114 12/6/2022
4.9.0-dev.9666 119 12/6/2022
4.9.0-dev.9617 114 12/6/2022
4.9.0-dev.9478 107 12/5/2022
4.9.0-dev.9469 124 12/5/2022
4.9.0-dev.9444 106 12/5/2022
4.9.0-dev.9411 101 12/5/2022
4.9.0-dev.9350 111 12/1/2022
4.8.0 1,591 12/1/2022
4.8.0-dev.9324 113 11/30/2022
4.8.0-dev.9232 117 11/28/2022
4.8.0-dev.9223 113 11/28/2022
4.8.0-dev.9222 121 11/28/2022
4.8.0-dev.9117 126 11/21/2022
4.8.0-dev.9108 111 11/21/2022
4.8.0-dev.9099 117 11/21/2022
4.8.0-dev.9029 113 11/16/2022
4.8.0-dev.8971 117 11/15/2022
4.8.0-dev.8961 123 11/14/2022
4.8.0-dev.8928 121 11/14/2022
4.8.0-dev.8899 125 11/14/2022
4.8.0-dev.8898 119 11/14/2022
4.8.0-dev.8839 131 11/14/2022
4.8.0-dev.8740 109 11/7/2022
4.8.0-dev.8725 114 11/7/2022
4.8.0-dev.8648 113 11/3/2022
4.7.0 23,960 11/3/2022
4.7.0-dev.8625 121 11/2/2022
4.7.0-dev.8594 121 10/31/2022
4.7.0-dev.8579 122 10/31/2022
4.7.0-dev.8557 113 10/31/2022
4.7.0-dev.8540 105 10/31/2022
4.7.0-dev.8518 109 10/31/2022
4.7.0-dev.8517 118 10/31/2022
4.7.0-dev.8509 116 10/31/2022
4.7.0-dev.8377 120 10/26/2022
4.7.0-dev.8360 127 10/25/2022
4.7.0-dev.8350 126 10/24/2022
4.7.0-dev.8335 123 10/24/2022
4.7.0-dev.8334 124 10/24/2022
4.7.0-dev.8223 164 10/19/2022
4.7.0-dev.8178 118 10/17/2022
4.7.0-dev.8170 116 10/17/2022
4.7.0-dev.8148 125 10/17/2022
4.7.0-dev.8133 122 10/17/2022
4.7.0-dev.8097 110 10/17/2022
4.7.0-dev.8034 128 10/11/2022
4.7.0-dev.8025 116 10/11/2022
4.7.0-dev.8009 134 10/10/2022
4.7.0-dev.8001 134 10/10/2022
4.7.0-dev.7959 116 10/4/2022
4.7.0-dev.7905 121 9/30/2022
4.7.0-dev.7875 112 9/29/2022
4.6.0 2,692 9/29/2022
4.6.0-dev.7832 126 9/29/2022
4.6.0-dev.7817 125 9/29/2022
4.6.0-dev.7779 140 9/27/2022
4.6.0-dev.7778 136 9/27/2022
4.6.0-dev.7734 127 9/26/2022
4.6.0-dev.7733 127 9/26/2022
4.6.0-dev.7677 128 9/20/2022
4.6.0-dev.7650 134 9/16/2022
4.6.0-dev.7626 188 9/14/2022
4.6.0-dev.7618 179 9/14/2022
4.6.0-dev.7574 120 9/13/2022
4.6.0-dev.7572 119 9/13/2022
4.6.0-dev.7528 111 9/12/2022
4.6.0-dev.7502 126 9/9/2022
4.6.0-dev.7479 139 9/8/2022
4.6.0-dev.7471 130 9/8/2022
4.6.0-dev.7447 122 9/7/2022
4.6.0-dev.7425 115 9/7/2022
4.6.0-dev.7395 115 9/6/2022
4.6.0-dev.7344 120 8/31/2022
4.6.0-dev.7329 114 8/31/2022
4.6.0-dev.7292 106 8/30/2022
4.6.0-dev.7240 122 8/29/2022
4.5.0 2,427 8/29/2022
4.5.0-dev.7216 118 8/27/2022
4.5.0-dev.7147 122 8/22/2022
4.5.0-dev.7134 123 8/17/2022
4.5.0-dev.7096 128 8/15/2022
4.5.0-dev.7070 134 8/11/2022
4.5.0-dev.7040 154 8/10/2022
4.5.0-dev.7011 132 8/3/2022
4.5.0-dev.6987 135 8/1/2022
4.5.0-dev.6962 138 7/29/2022
4.4.0 14,723 7/29/2022
4.4.0-dev.6901 136 7/25/2022
4.4.0-dev.6843 130 7/19/2022
4.4.0-dev.6804 134 7/19/2022
4.4.0-dev.6789 132 7/19/2022
4.4.0-dev.6760 128 7/19/2022
4.4.0-dev.6705 142 7/14/2022
4.4.0-dev.6663 168 6/24/2022
4.4.0-dev.6655 126 6/24/2022
4.3.0 10,612 6/24/2022
4.3.0-dev.multiple.buckets3 156 6/21/2022
4.3.0-dev.multiple.buckets2 122 6/17/2022
4.3.0-dev.multiple.buckets1 129 6/17/2022
4.3.0-dev.6631 123 6/22/2022
4.3.0-dev.6623 131 6/22/2022
4.3.0-dev.6374 134 6/13/2022
4.3.0-dev.6286 136 5/20/2022
4.2.0 2,405 5/20/2022
4.2.0-dev.6257 138 5/13/2022
4.2.0-dev.6248 135 5/12/2022
4.2.0-dev.6233 140 5/12/2022
4.2.0-dev.6194 137 5/10/2022
4.2.0-dev.6193 131 5/10/2022
4.2.0-dev.6158 2,846 5/6/2022
4.2.0-dev.6135 142 5/6/2022
4.2.0-dev.6091 143 4/28/2022
4.2.0-dev.6048 143 4/28/2022
4.2.0-dev.6047 143 4/28/2022
4.2.0-dev.5966 145 4/25/2022
4.2.0-dev.5938 146 4/19/2022
4.1.0 3,395 4/19/2022
4.1.0-dev.5910 335 4/13/2022
4.1.0-dev.5888 145 4/13/2022
4.1.0-dev.5887 147 4/13/2022
4.1.0-dev.5794 147 4/6/2022
4.1.0-dev.5725 153 3/18/2022
4.0.0 7,549 3/18/2022
4.0.0-rc3 396 3/4/2022
4.0.0-rc2 546 2/25/2022
4.0.0-rc1 207 2/18/2022
4.0.0-dev.5709 145 3/18/2022
4.0.0-dev.5684 155 3/15/2022
4.0.0-dev.5630 155 3/4/2022
4.0.0-dev.5607 147 3/3/2022
4.0.0-dev.5579 150 2/25/2022
4.0.0-dev.5556 155 2/24/2022
4.0.0-dev.5555 143 2/24/2022
4.0.0-dev.5497 141 2/23/2022
4.0.0-dev.5489 152 2/23/2022
4.0.0-dev.5460 148 2/23/2022
4.0.0-dev.5444 142 2/22/2022
4.0.0-dev.5333 147 2/17/2022
4.0.0-dev.5303 142 2/16/2022
4.0.0-dev.5280 154 2/16/2022
4.0.0-dev.5279 155 2/16/2022
4.0.0-dev.5241 249 2/15/2022
4.0.0-dev.5225 143 2/15/2022
4.0.0-dev.5217 148 2/15/2022
4.0.0-dev.5209 141 2/15/2022
4.0.0-dev.5200 141 2/14/2022
4.0.0-dev.5188 145 2/10/2022
4.0.0-dev.5180 144 2/10/2022
4.0.0-dev.5172 147 2/10/2022
4.0.0-dev.5130 139 2/10/2022
4.0.0-dev.5122 147 2/9/2022
4.0.0-dev.5103 154 2/9/2022
4.0.0-dev.5097 153 2/9/2022
4.0.0-dev.5091 146 2/9/2022
4.0.0-dev.5084 148 2/8/2022
3.4.0-dev.5263 156 2/15/2022
3.4.0-dev.4986 148 2/7/2022
3.4.0-dev.4968 163 2/4/2022
3.3.0 8,667 2/4/2022
3.3.0-dev.4889 151 2/3/2022
3.3.0-dev.4865 159 2/1/2022
3.3.0-dev.4823 162 1/19/2022
3.3.0-dev.4691 160 1/7/2022
3.3.0-dev.4557 1,370 11/26/2021
3.2.0 5,896 11/26/2021
3.2.0-dev.4533 4,865 11/24/2021
3.2.0-dev.4484 227 11/11/2021
3.2.0-dev.4475 199 11/10/2021
3.2.0-dev.4387 175 10/26/2021
3.2.0-dev.4363 190 10/22/2021
3.2.0-dev.4356 188 10/22/2021
3.1.0 1,788 10/22/2021
3.1.0-dev.4303 190 10/18/2021
3.1.0-dev.4293 192 10/15/2021
3.1.0-dev.4286 171 10/15/2021
3.1.0-dev.4240 208 10/12/2021
3.1.0-dev.4202 167 10/11/2021
3.1.0-dev.4183 210 10/11/2021
3.1.0-dev.4131 177 10/8/2021
3.1.0-dev.3999 186 10/5/2021
3.1.0-dev.3841 264 9/29/2021
3.1.0-dev.3798 185 9/17/2021
3.0.0 1,202 9/17/2021
3.0.0-dev.3726 525 8/31/2021
3.0.0-dev.3719 172 8/31/2021
3.0.0-dev.3671 184 8/20/2021
2.2.0-dev.3652 180 8/20/2021
2.1.0 1,555 8/20/2021
2.1.0-dev.3605 185 8/17/2021
2.1.0-dev.3584 186 8/16/2021
2.1.0-dev.3558 174 8/16/2021
2.1.0-dev.3527 220 7/29/2021
2.1.0-dev.3519 224 7/29/2021
2.1.0-dev.3490 175 7/20/2021
2.1.0-dev.3445 198 7/12/2021
2.1.0-dev.3434 233 7/9/2021
2.0.0 9,017 7/9/2021
2.0.0-dev.3401 214 6/25/2021
2.0.0-dev.3368 199 6/23/2021
2.0.0-dev.3361 210 6/23/2021
2.0.0-dev.3330 206 6/17/2021
2.0.0-dev.3291 209 6/16/2021
1.20.0-dev.3218 226 6/4/2021
1.19.0 927 6/4/2021
1.19.0-dev.3204 194 6/3/2021
1.19.0-dev.3160 180 6/2/2021
1.19.0-dev.3159 176 6/2/2021
1.19.0-dev.3084 837 5/7/2021
1.19.0-dev.3051 201 5/5/2021
1.19.0-dev.3044 198 5/5/2021
1.19.0-dev.3008 192 4/30/2021
1.18.0 1,234 4/30/2021
1.18.0-dev.2973 211 4/27/2021
1.18.0-dev.2930 191 4/16/2021
1.18.0-dev.2919 188 4/13/2021
1.18.0-dev.2893 174 4/12/2021
1.18.0-dev.2880 193 4/12/2021
1.18.0-dev.2856 187 4/7/2021
1.18.0-dev.2830 283 4/1/2021
1.18.0-dev.2816 189 4/1/2021
1.17.0 763 4/1/2021
1.17.0-dev.linq.17 802 3/18/2021
1.17.0-dev.linq.16 181 3/16/2021
1.17.0-dev.linq.15 215 3/15/2021
1.17.0-dev.linq.14 218 3/12/2021
1.17.0-dev.linq.13 248 3/11/2021
1.17.0-dev.linq.12 199 3/10/2021
1.17.0-dev.linq.11 194 3/8/2021
1.17.0-dev.2776 218 3/26/2021
1.17.0-dev.2713 231 3/25/2021
1.16.0-dev.linq.10 1,236 2/4/2021
1.15.0-dev.linq.9 215 2/4/2021
1.15.0-dev.linq.8 188 1/28/2021
1.15.0-dev.linq.7 205 1/27/2021
1.15.0-dev.linq.6 222 1/20/2021
1.15.0-dev.linq.5 241 1/19/2021
1.15.0-dev.linq.4 204 1/15/2021
1.15.0-dev.linq.3 180 1/14/2021
1.15.0-dev.linq.2 196 1/13/2021
1.15.0-dev.linq.1 220 1/12/2021