InfluxDB.Client.Linq 4.17.0-dev.14333

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

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