InfluxDB.Client.Linq 4.15.0-dev.12863

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

// Install InfluxDB.Client.Linq as a Cake Tool
#tool nuget:?package=InfluxDB.Client.Linq&version=4.15.0-dev.12863&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 69 10/2/2024
4.19.0-dev.14897 46 10/2/2024
4.19.0-dev.14896 42 10/2/2024
4.19.0-dev.14895 44 10/2/2024
4.19.0-dev.14811 60 9/13/2024
4.18.0 3,892 9/13/2024
4.18.0-dev.14769 61 9/4/2024
4.18.0-dev.14743 54 9/3/2024
4.18.0-dev.14694 51 9/3/2024
4.18.0-dev.14693 48 9/3/2024
4.18.0-dev.14692 46 9/3/2024
4.18.0-dev.14618 45 9/2/2024
4.18.0-dev.14609 45 9/2/2024
4.18.0-dev.14592 46 9/2/2024
4.18.0-dev.14446 71 8/19/2024
4.18.0-dev.14414 64 8/12/2024
4.17.0 4,185 8/12/2024
4.17.0-dev.headers.read.1 73 7/22/2024
4.17.0-dev.14350 44 8/5/2024
4.17.0-dev.14333 39 8/5/2024
4.17.0-dev.14300 37 8/5/2024
4.17.0-dev.14291 37 8/5/2024
4.17.0-dev.14189 50 7/23/2024
4.17.0-dev.14179 51 7/22/2024
4.17.0-dev.14101 128 7/1/2024
4.17.0-dev.14100 58 7/1/2024
4.17.0-dev.14044 61 6/24/2024
4.16.0 5,711 6/24/2024
4.16.0-dev.13990 63 6/3/2024
4.16.0-dev.13973 54 6/3/2024
4.16.0-dev.13972 53 6/3/2024
4.16.0-dev.13963 61 6/3/2024
4.16.0-dev.13962 55 6/3/2024
4.16.0-dev.13881 59 6/3/2024
4.16.0-dev.13775 71 5/17/2024
4.16.0-dev.13702 63 5/17/2024
4.15.0 2,404 5/17/2024
4.15.0-dev.13674 71 5/14/2024
4.15.0-dev.13567 75 4/2/2024
4.15.0-dev.13558 57 4/2/2024
4.15.0-dev.13525 66 4/2/2024
4.15.0-dev.13524 58 4/2/2024
4.15.0-dev.13433 69 3/7/2024
4.15.0-dev.13432 68 3/7/2024
4.15.0-dev.13407 66 3/7/2024
4.15.0-dev.13390 62 3/7/2024
4.15.0-dev.13388 58 3/7/2024
4.15.0-dev.13282 69 3/6/2024
4.15.0-dev.13257 69 3/6/2024
4.15.0-dev.13113 229 2/1/2024
4.15.0-dev.13104 64 2/1/2024
4.15.0-dev.13081 65 2/1/2024
4.15.0-dev.13040 62 2/1/2024
4.15.0-dev.13039 67 2/1/2024
4.15.0-dev.12863 112 1/8/2024
4.15.0-dev.12846 81 1/8/2024
4.15.0-dev.12837 73 1/8/2024
4.15.0-dev.12726 152 12/1/2023
4.15.0-dev.12725 75 12/1/2023
4.15.0-dev.12724 73 12/1/2023
4.15.0-dev.12691 77 12/1/2023
4.15.0-dev.12658 70 12/1/2023
4.15.0-dev.12649 75 12/1/2023
4.15.0-dev.12624 72 12/1/2023
4.15.0-dev.12471 98 11/7/2023
4.15.0-dev.12462 73 11/7/2023
4.14.0 47,835 11/7/2023
4.14.0-dev.12437 75 11/7/2023
4.14.0-dev.12343 87 11/2/2023
4.14.0-dev.12310 74 11/2/2023
4.14.0-dev.12284 76 11/1/2023
4.14.0-dev.12235 75 11/1/2023
4.14.0-dev.12226 72 11/1/2023
4.14.0-dev.11972 207 8/8/2023
4.14.0-dev.11915 110 7/31/2023
4.14.0-dev.11879 119 7/28/2023
4.13.0 21,570 7/28/2023
4.13.0-dev.11854 91 7/28/2023
4.13.0-dev.11814 103 7/21/2023
4.13.0-dev.11771 94 7/19/2023
4.13.0-dev.11770 102 7/19/2023
4.13.0-dev.11728 90 7/18/2023
4.13.0-dev.11686 89 7/17/2023
4.13.0-dev.11685 87 7/17/2023
4.13.0-dev.11676 103 7/17/2023
4.13.0-dev.11479 90 6/27/2023
4.13.0-dev.11478 90 6/27/2023
4.13.0-dev.11477 96 6/27/2023
4.13.0-dev.11396 97 6/19/2023
4.13.0-dev.11395 82 6/19/2023
4.13.0-dev.11342 91 6/15/2023
4.13.0-dev.11330 102 6/12/2023
4.13.0-dev.11305 93 6/12/2023
4.13.0-dev.11296 95 6/12/2023
4.13.0-dev.11217 96 6/6/2023
4.13.0-dev.11089 88 5/30/2023
4.13.0-dev.11064 95 5/30/2023
4.13.0-dev.10998 92 5/29/2023
4.13.0-dev.10989 95 5/29/2023
4.13.0-dev.10871 96 5/8/2023
4.13.0-dev.10870 80 5/8/2023
4.13.0-dev.10819 108 4/28/2023
4.12.0 12,905 4/28/2023
4.12.0-dev.10777 97 4/27/2023
4.12.0-dev.10768 102 4/27/2023
4.12.0-dev.10759 100 4/27/2023
4.12.0-dev.10742 95 4/27/2023
4.12.0-dev.10685 86 4/27/2023
4.12.0-dev.10684 90 4/27/2023
4.12.0-dev.10643 92 4/27/2023
4.12.0-dev.10642 92 4/27/2023
4.12.0-dev.10569 92 4/27/2023
4.12.0-dev.10193 132 2/23/2023
4.11.0 19,730 2/23/2023
4.11.0-dev.10176 103 2/23/2023
4.11.0-dev.10059 208 1/26/2023
4.10.0 6,040 1/26/2023
4.10.0-dev.10033 123 1/25/2023
4.10.0-dev.10032 123 1/25/2023
4.10.0-dev.10031 120 1/25/2023
4.10.0-dev.9936 2,193 12/26/2022
4.10.0-dev.9935 117 12/26/2022
4.10.0-dev.9881 110 12/21/2022
4.10.0-dev.9880 108 12/21/2022
4.10.0-dev.9818 117 12/16/2022
4.10.0-dev.9773 107 12/12/2022
4.10.0-dev.9756 113 12/12/2022
4.10.0-dev.9693 109 12/6/2022
4.9.0 9,407 12/6/2022
4.9.0-dev.9684 111 12/6/2022
4.9.0-dev.9666 116 12/6/2022
4.9.0-dev.9617 111 12/6/2022
4.9.0-dev.9478 104 12/5/2022
4.9.0-dev.9469 121 12/5/2022
4.9.0-dev.9444 103 12/5/2022
4.9.0-dev.9411 98 12/5/2022
4.9.0-dev.9350 108 12/1/2022
4.8.0 1,588 12/1/2022
4.8.0-dev.9324 110 11/30/2022
4.8.0-dev.9232 114 11/28/2022
4.8.0-dev.9223 110 11/28/2022
4.8.0-dev.9222 118 11/28/2022
4.8.0-dev.9117 123 11/21/2022
4.8.0-dev.9108 108 11/21/2022
4.8.0-dev.9099 114 11/21/2022
4.8.0-dev.9029 110 11/16/2022
4.8.0-dev.8971 114 11/15/2022
4.8.0-dev.8961 120 11/14/2022
4.8.0-dev.8928 118 11/14/2022
4.8.0-dev.8899 122 11/14/2022
4.8.0-dev.8898 116 11/14/2022
4.8.0-dev.8839 128 11/14/2022
4.8.0-dev.8740 106 11/7/2022
4.8.0-dev.8725 111 11/7/2022
4.8.0-dev.8648 110 11/3/2022
4.7.0 23,955 11/3/2022
4.7.0-dev.8625 118 11/2/2022
4.7.0-dev.8594 118 10/31/2022
4.7.0-dev.8579 119 10/31/2022
4.7.0-dev.8557 110 10/31/2022
4.7.0-dev.8540 102 10/31/2022
4.7.0-dev.8518 106 10/31/2022
4.7.0-dev.8517 115 10/31/2022
4.7.0-dev.8509 113 10/31/2022
4.7.0-dev.8377 117 10/26/2022
4.7.0-dev.8360 124 10/25/2022
4.7.0-dev.8350 123 10/24/2022
4.7.0-dev.8335 120 10/24/2022
4.7.0-dev.8334 121 10/24/2022
4.7.0-dev.8223 161 10/19/2022
4.7.0-dev.8178 115 10/17/2022
4.7.0-dev.8170 113 10/17/2022
4.7.0-dev.8148 122 10/17/2022
4.7.0-dev.8133 119 10/17/2022
4.7.0-dev.8097 107 10/17/2022
4.7.0-dev.8034 125 10/11/2022
4.7.0-dev.8025 113 10/11/2022
4.7.0-dev.8009 131 10/10/2022
4.7.0-dev.8001 131 10/10/2022
4.7.0-dev.7959 113 10/4/2022
4.7.0-dev.7905 118 9/30/2022
4.7.0-dev.7875 109 9/29/2022
4.6.0 2,689 9/29/2022
4.6.0-dev.7832 123 9/29/2022
4.6.0-dev.7817 122 9/29/2022
4.6.0-dev.7779 137 9/27/2022
4.6.0-dev.7778 133 9/27/2022
4.6.0-dev.7734 124 9/26/2022
4.6.0-dev.7733 124 9/26/2022
4.6.0-dev.7677 125 9/20/2022
4.6.0-dev.7650 131 9/16/2022
4.6.0-dev.7626 185 9/14/2022
4.6.0-dev.7618 176 9/14/2022
4.6.0-dev.7574 117 9/13/2022
4.6.0-dev.7572 116 9/13/2022
4.6.0-dev.7528 108 9/12/2022
4.6.0-dev.7502 123 9/9/2022
4.6.0-dev.7479 136 9/8/2022
4.6.0-dev.7471 127 9/8/2022
4.6.0-dev.7447 119 9/7/2022
4.6.0-dev.7425 112 9/7/2022
4.6.0-dev.7395 112 9/6/2022
4.6.0-dev.7344 117 8/31/2022
4.6.0-dev.7329 111 8/31/2022
4.6.0-dev.7292 103 8/30/2022
4.6.0-dev.7240 119 8/29/2022
4.5.0 2,390 8/29/2022
4.5.0-dev.7216 115 8/27/2022
4.5.0-dev.7147 119 8/22/2022
4.5.0-dev.7134 120 8/17/2022
4.5.0-dev.7096 125 8/15/2022
4.5.0-dev.7070 131 8/11/2022
4.5.0-dev.7040 151 8/10/2022
4.5.0-dev.7011 129 8/3/2022
4.5.0-dev.6987 132 8/1/2022
4.5.0-dev.6962 135 7/29/2022
4.4.0 14,719 7/29/2022
4.4.0-dev.6901 133 7/25/2022
4.4.0-dev.6843 127 7/19/2022
4.4.0-dev.6804 131 7/19/2022
4.4.0-dev.6789 129 7/19/2022
4.4.0-dev.6760 125 7/19/2022
4.4.0-dev.6705 139 7/14/2022
4.4.0-dev.6663 165 6/24/2022
4.4.0-dev.6655 123 6/24/2022
4.3.0 10,551 6/24/2022
4.3.0-dev.multiple.buckets3 153 6/21/2022
4.3.0-dev.multiple.buckets2 119 6/17/2022
4.3.0-dev.multiple.buckets1 126 6/17/2022
4.3.0-dev.6631 120 6/22/2022
4.3.0-dev.6623 128 6/22/2022
4.3.0-dev.6374 131 6/13/2022
4.3.0-dev.6286 133 5/20/2022
4.2.0 2,401 5/20/2022
4.2.0-dev.6257 135 5/13/2022
4.2.0-dev.6248 132 5/12/2022
4.2.0-dev.6233 137 5/12/2022
4.2.0-dev.6194 134 5/10/2022
4.2.0-dev.6193 128 5/10/2022
4.2.0-dev.6158 2,843 5/6/2022
4.2.0-dev.6135 139 5/6/2022
4.2.0-dev.6091 140 4/28/2022
4.2.0-dev.6048 140 4/28/2022
4.2.0-dev.6047 140 4/28/2022
4.2.0-dev.5966 142 4/25/2022
4.2.0-dev.5938 143 4/19/2022
4.1.0 3,392 4/19/2022
4.1.0-dev.5910 332 4/13/2022
4.1.0-dev.5888 142 4/13/2022
4.1.0-dev.5887 144 4/13/2022
4.1.0-dev.5794 144 4/6/2022
4.1.0-dev.5725 150 3/18/2022
4.0.0 7,516 3/18/2022
4.0.0-rc3 393 3/4/2022
4.0.0-rc2 543 2/25/2022
4.0.0-rc1 204 2/18/2022
4.0.0-dev.5709 142 3/18/2022
4.0.0-dev.5684 152 3/15/2022
4.0.0-dev.5630 152 3/4/2022
4.0.0-dev.5607 144 3/3/2022
4.0.0-dev.5579 147 2/25/2022
4.0.0-dev.5556 152 2/24/2022
4.0.0-dev.5555 140 2/24/2022
4.0.0-dev.5497 138 2/23/2022
4.0.0-dev.5489 149 2/23/2022
4.0.0-dev.5460 145 2/23/2022
4.0.0-dev.5444 139 2/22/2022
4.0.0-dev.5333 144 2/17/2022
4.0.0-dev.5303 139 2/16/2022
4.0.0-dev.5280 151 2/16/2022
4.0.0-dev.5279 152 2/16/2022
4.0.0-dev.5241 246 2/15/2022
4.0.0-dev.5225 140 2/15/2022
4.0.0-dev.5217 145 2/15/2022
4.0.0-dev.5209 138 2/15/2022
4.0.0-dev.5200 138 2/14/2022
4.0.0-dev.5188 142 2/10/2022
4.0.0-dev.5180 141 2/10/2022
4.0.0-dev.5172 144 2/10/2022
4.0.0-dev.5130 136 2/10/2022
4.0.0-dev.5122 144 2/9/2022
4.0.0-dev.5103 151 2/9/2022
4.0.0-dev.5097 150 2/9/2022
4.0.0-dev.5091 143 2/9/2022
4.0.0-dev.5084 145 2/8/2022
3.4.0-dev.5263 153 2/15/2022
3.4.0-dev.4986 145 2/7/2022
3.4.0-dev.4968 160 2/4/2022
3.3.0 8,664 2/4/2022
3.3.0-dev.4889 148 2/3/2022
3.3.0-dev.4865 156 2/1/2022
3.3.0-dev.4823 159 1/19/2022
3.3.0-dev.4691 157 1/7/2022
3.3.0-dev.4557 1,367 11/26/2021
3.2.0 5,893 11/26/2021
3.2.0-dev.4533 4,862 11/24/2021
3.2.0-dev.4484 224 11/11/2021
3.2.0-dev.4475 196 11/10/2021
3.2.0-dev.4387 172 10/26/2021
3.2.0-dev.4363 187 10/22/2021
3.2.0-dev.4356 185 10/22/2021
3.1.0 1,785 10/22/2021
3.1.0-dev.4303 187 10/18/2021
3.1.0-dev.4293 189 10/15/2021
3.1.0-dev.4286 168 10/15/2021
3.1.0-dev.4240 205 10/12/2021
3.1.0-dev.4202 164 10/11/2021
3.1.0-dev.4183 207 10/11/2021
3.1.0-dev.4131 174 10/8/2021
3.1.0-dev.3999 183 10/5/2021
3.1.0-dev.3841 261 9/29/2021
3.1.0-dev.3798 182 9/17/2021
3.0.0 1,199 9/17/2021
3.0.0-dev.3726 522 8/31/2021
3.0.0-dev.3719 169 8/31/2021
3.0.0-dev.3671 181 8/20/2021
2.2.0-dev.3652 177 8/20/2021
2.1.0 1,552 8/20/2021
2.1.0-dev.3605 182 8/17/2021
2.1.0-dev.3584 183 8/16/2021
2.1.0-dev.3558 171 8/16/2021
2.1.0-dev.3527 217 7/29/2021
2.1.0-dev.3519 221 7/29/2021
2.1.0-dev.3490 172 7/20/2021
2.1.0-dev.3445 195 7/12/2021
2.1.0-dev.3434 230 7/9/2021
2.0.0 9,014 7/9/2021
2.0.0-dev.3401 211 6/25/2021
2.0.0-dev.3368 196 6/23/2021
2.0.0-dev.3361 207 6/23/2021
2.0.0-dev.3330 203 6/17/2021
2.0.0-dev.3291 206 6/16/2021
1.20.0-dev.3218 223 6/4/2021
1.19.0 924 6/4/2021
1.19.0-dev.3204 191 6/3/2021
1.19.0-dev.3160 177 6/2/2021
1.19.0-dev.3159 173 6/2/2021
1.19.0-dev.3084 834 5/7/2021
1.19.0-dev.3051 198 5/5/2021
1.19.0-dev.3044 195 5/5/2021
1.19.0-dev.3008 189 4/30/2021
1.18.0 1,231 4/30/2021
1.18.0-dev.2973 208 4/27/2021
1.18.0-dev.2930 188 4/16/2021
1.18.0-dev.2919 185 4/13/2021
1.18.0-dev.2893 171 4/12/2021
1.18.0-dev.2880 190 4/12/2021
1.18.0-dev.2856 184 4/7/2021
1.18.0-dev.2830 280 4/1/2021
1.18.0-dev.2816 186 4/1/2021
1.17.0 760 4/1/2021
1.17.0-dev.linq.17 799 3/18/2021
1.17.0-dev.linq.16 178 3/16/2021
1.17.0-dev.linq.15 212 3/15/2021
1.17.0-dev.linq.14 215 3/12/2021
1.17.0-dev.linq.13 245 3/11/2021
1.17.0-dev.linq.12 196 3/10/2021
1.17.0-dev.linq.11 191 3/8/2021
1.17.0-dev.2776 215 3/26/2021
1.17.0-dev.2713 228 3/25/2021
1.16.0-dev.linq.10 1,233 2/4/2021
1.15.0-dev.linq.9 212 2/4/2021
1.15.0-dev.linq.8 185 1/28/2021
1.15.0-dev.linq.7 202 1/27/2021
1.15.0-dev.linq.6 219 1/20/2021
1.15.0-dev.linq.5 238 1/19/2021
1.15.0-dev.linq.4 201 1/15/2021
1.15.0-dev.linq.3 177 1/14/2021
1.15.0-dev.linq.2 193 1/13/2021
1.15.0-dev.linq.1 217 1/12/2021