InfluxDB.Client.Linq 4.19.0-dev.15189

This is a prerelease version of InfluxDB.Client.Linq.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package InfluxDB.Client.Linq --version 4.19.0-dev.15189                
NuGet\Install-Package InfluxDB.Client.Linq -Version 4.19.0-dev.15189                
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.15189" />                
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.15189                
#r "nuget: InfluxDB.Client.Linq, 4.19.0-dev.15189"                
#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.15189&prerelease

// Install InfluxDB.Client.Linq as a Cake Tool
#tool nuget:?package=InfluxDB.Client.Linq&version=4.19.0-dev.15189&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.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.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.15190 109 12/5/2024
4.19.0-dev.15189 46 12/5/2024
4.19.0-dev.15188 43 12/5/2024
4.19.0-dev.15178 49 12/5/2024
4.19.0-dev.15177 47 12/5/2024
4.19.0-dev.14906 114 10/2/2024
4.19.0-dev.14897 55 10/2/2024
4.19.0-dev.14896 48 10/2/2024
4.19.0-dev.14895 52 10/2/2024
4.19.0-dev.14811 69 9/13/2024
4.18.0 19,737 9/13/2024
4.18.0-dev.14769 69 9/4/2024
4.18.0-dev.14743 63 9/3/2024
4.18.0-dev.14694 60 9/3/2024
4.18.0-dev.14693 55 9/3/2024
4.18.0-dev.14692 53 9/3/2024
4.18.0-dev.14618 54 9/2/2024
4.18.0-dev.14609 52 9/2/2024
4.18.0-dev.14592 56 9/2/2024
4.18.0-dev.14446 80 8/19/2024
4.18.0-dev.14414 73 8/12/2024
4.17.0 7,034 8/12/2024
4.17.0-dev.headers.read.1 85 7/22/2024
4.17.0-dev.14350 52 8/5/2024
4.17.0-dev.14333 49 8/5/2024
4.17.0-dev.14300 45 8/5/2024
4.17.0-dev.14291 45 8/5/2024
4.17.0-dev.14189 61 7/23/2024
4.17.0-dev.14179 58 7/22/2024
4.17.0-dev.14101 136 7/1/2024
4.17.0-dev.14100 66 7/1/2024
4.17.0-dev.14044 67 6/24/2024
4.16.0 7,287 6/24/2024
4.16.0-dev.13990 69 6/3/2024
4.16.0-dev.13973 62 6/3/2024
4.16.0-dev.13972 61 6/3/2024
4.16.0-dev.13963 69 6/3/2024
4.16.0-dev.13962 63 6/3/2024
4.16.0-dev.13881 65 6/3/2024
4.16.0-dev.13775 78 5/17/2024
4.16.0-dev.13702 69 5/17/2024
4.15.0 2,679 5/17/2024
4.15.0-dev.13674 79 5/14/2024
4.15.0-dev.13567 84 4/2/2024
4.15.0-dev.13558 64 4/2/2024
4.15.0-dev.13525 75 4/2/2024
4.15.0-dev.13524 65 4/2/2024
4.15.0-dev.13433 76 3/7/2024
4.15.0-dev.13432 75 3/7/2024
4.15.0-dev.13407 72 3/7/2024
4.15.0-dev.13390 69 3/7/2024
4.15.0-dev.13388 67 3/7/2024
4.15.0-dev.13282 75 3/6/2024
4.15.0-dev.13257 75 3/6/2024
4.15.0-dev.13113 236 2/1/2024
4.15.0-dev.13104 71 2/1/2024
4.15.0-dev.13081 72 2/1/2024
4.15.0-dev.13040 70 2/1/2024
4.15.0-dev.13039 73 2/1/2024
4.15.0-dev.12863 122 1/8/2024
4.15.0-dev.12846 88 1/8/2024
4.15.0-dev.12837 80 1/8/2024
4.15.0-dev.12726 161 12/1/2023
4.15.0-dev.12725 82 12/1/2023
4.15.0-dev.12724 79 12/1/2023
4.15.0-dev.12691 83 12/1/2023
4.15.0-dev.12658 77 12/1/2023
4.15.0-dev.12649 81 12/1/2023
4.15.0-dev.12624 78 12/1/2023
4.15.0-dev.12471 106 11/7/2023
4.15.0-dev.12462 80 11/7/2023
4.14.0 52,934 11/7/2023
4.14.0-dev.12437 82 11/7/2023
4.14.0-dev.12343 94 11/2/2023
4.14.0-dev.12310 81 11/2/2023
4.14.0-dev.12284 84 11/1/2023
4.14.0-dev.12235 83 11/1/2023
4.14.0-dev.12226 81 11/1/2023
4.14.0-dev.11972 219 8/8/2023
4.14.0-dev.11915 118 7/31/2023
4.14.0-dev.11879 128 7/28/2023
4.13.0 22,142 7/28/2023
4.13.0-dev.11854 100 7/28/2023
4.13.0-dev.11814 111 7/21/2023
4.13.0-dev.11771 101 7/19/2023
4.13.0-dev.11770 110 7/19/2023
4.13.0-dev.11728 98 7/18/2023
4.13.0-dev.11686 99 7/17/2023
4.13.0-dev.11685 95 7/17/2023
4.13.0-dev.11676 113 7/17/2023
4.13.0-dev.11479 99 6/27/2023
4.13.0-dev.11478 100 6/27/2023
4.13.0-dev.11477 106 6/27/2023
4.13.0-dev.11396 105 6/19/2023
4.13.0-dev.11395 92 6/19/2023
4.13.0-dev.11342 101 6/15/2023
4.13.0-dev.11330 112 6/12/2023
4.13.0-dev.11305 103 6/12/2023
4.13.0-dev.11296 104 6/12/2023
4.13.0-dev.11217 106 6/6/2023
4.13.0-dev.11089 96 5/30/2023
4.13.0-dev.11064 105 5/30/2023
4.13.0-dev.10998 101 5/29/2023
4.13.0-dev.10989 103 5/29/2023
4.13.0-dev.10871 106 5/8/2023
4.13.0-dev.10870 87 5/8/2023
4.13.0-dev.10819 118 4/28/2023
4.12.0 13,456 4/28/2023
4.12.0-dev.10777 107 4/27/2023
4.12.0-dev.10768 112 4/27/2023
4.12.0-dev.10759 109 4/27/2023
4.12.0-dev.10742 106 4/27/2023
4.12.0-dev.10685 97 4/27/2023
4.12.0-dev.10684 100 4/27/2023
4.12.0-dev.10643 100 4/27/2023
4.12.0-dev.10642 104 4/27/2023
4.12.0-dev.10569 100 4/27/2023
4.12.0-dev.10193 142 2/23/2023
4.11.0 20,717 2/23/2023
4.11.0-dev.10176 111 2/23/2023
4.11.0-dev.10059 218 1/26/2023
4.10.0 6,500 1/26/2023
4.10.0-dev.10033 131 1/25/2023
4.10.0-dev.10032 131 1/25/2023
4.10.0-dev.10031 130 1/25/2023
4.10.0-dev.9936 2,207 12/26/2022
4.10.0-dev.9935 125 12/26/2022
4.10.0-dev.9881 120 12/21/2022
4.10.0-dev.9880 118 12/21/2022
4.10.0-dev.9818 127 12/16/2022
4.10.0-dev.9773 117 12/12/2022
4.10.0-dev.9756 122 12/12/2022
4.10.0-dev.9693 119 12/6/2022
4.9.0 9,610 12/6/2022
4.9.0-dev.9684 121 12/6/2022
4.9.0-dev.9666 126 12/6/2022
4.9.0-dev.9617 121 12/6/2022
4.9.0-dev.9478 114 12/5/2022
4.9.0-dev.9469 130 12/5/2022
4.9.0-dev.9444 113 12/5/2022
4.9.0-dev.9411 106 12/5/2022
4.9.0-dev.9350 116 12/1/2022
4.8.0 1,603 12/1/2022
4.8.0-dev.9324 120 11/30/2022
4.8.0-dev.9232 122 11/28/2022
4.8.0-dev.9223 118 11/28/2022
4.8.0-dev.9222 127 11/28/2022
4.8.0-dev.9117 133 11/21/2022
4.8.0-dev.9108 118 11/21/2022
4.8.0-dev.9099 128 11/21/2022
4.8.0-dev.9029 120 11/16/2022
4.8.0-dev.8971 121 11/15/2022
4.8.0-dev.8961 127 11/14/2022
4.8.0-dev.8928 128 11/14/2022
4.8.0-dev.8899 131 11/14/2022
4.8.0-dev.8898 124 11/14/2022
4.8.0-dev.8839 138 11/14/2022
4.8.0-dev.8740 116 11/7/2022
4.8.0-dev.8725 121 11/7/2022
4.8.0-dev.8648 117 11/3/2022
4.7.0 24,317 11/3/2022
4.7.0-dev.8625 125 11/2/2022
4.7.0-dev.8594 128 10/31/2022
4.7.0-dev.8579 127 10/31/2022
4.7.0-dev.8557 118 10/31/2022
4.7.0-dev.8540 112 10/31/2022
4.7.0-dev.8518 116 10/31/2022
4.7.0-dev.8517 125 10/31/2022
4.7.0-dev.8509 124 10/31/2022
4.7.0-dev.8377 127 10/26/2022
4.7.0-dev.8360 134 10/25/2022
4.7.0-dev.8350 132 10/24/2022
4.7.0-dev.8335 129 10/24/2022
4.7.0-dev.8334 129 10/24/2022
4.7.0-dev.8223 171 10/19/2022
4.7.0-dev.8178 125 10/17/2022
4.7.0-dev.8170 121 10/17/2022
4.7.0-dev.8148 132 10/17/2022
4.7.0-dev.8133 127 10/17/2022
4.7.0-dev.8097 116 10/17/2022
4.7.0-dev.8034 135 10/11/2022
4.7.0-dev.8025 123 10/11/2022
4.7.0-dev.8009 140 10/10/2022
4.7.0-dev.8001 144 10/10/2022
4.7.0-dev.7959 122 10/4/2022
4.7.0-dev.7905 128 9/30/2022
4.7.0-dev.7875 117 9/29/2022
4.6.0 2,707 9/29/2022
4.6.0-dev.7832 133 9/29/2022
4.6.0-dev.7817 131 9/29/2022
4.6.0-dev.7779 147 9/27/2022
4.6.0-dev.7778 141 9/27/2022
4.6.0-dev.7734 133 9/26/2022
4.6.0-dev.7733 131 9/26/2022
4.6.0-dev.7677 134 9/20/2022
4.6.0-dev.7650 138 9/16/2022
4.6.0-dev.7626 193 9/14/2022
4.6.0-dev.7618 183 9/14/2022
4.6.0-dev.7574 127 9/13/2022
4.6.0-dev.7572 124 9/13/2022
4.6.0-dev.7528 119 9/12/2022
4.6.0-dev.7502 133 9/9/2022
4.6.0-dev.7479 148 9/8/2022
4.6.0-dev.7471 135 9/8/2022
4.6.0-dev.7447 129 9/7/2022
4.6.0-dev.7425 121 9/7/2022
4.6.0-dev.7395 119 9/6/2022
4.6.0-dev.7344 126 8/31/2022
4.6.0-dev.7329 120 8/31/2022
4.6.0-dev.7292 110 8/30/2022
4.6.0-dev.7240 129 8/29/2022
4.5.0 2,527 8/29/2022
4.5.0-dev.7216 124 8/27/2022
4.5.0-dev.7147 129 8/22/2022
4.5.0-dev.7134 130 8/17/2022
4.5.0-dev.7096 134 8/15/2022
4.5.0-dev.7070 140 8/11/2022
4.5.0-dev.7040 161 8/10/2022
4.5.0-dev.7011 139 8/3/2022
4.5.0-dev.6987 139 8/1/2022
4.5.0-dev.6962 145 7/29/2022
4.4.0 14,738 7/29/2022
4.4.0-dev.6901 142 7/25/2022
4.4.0-dev.6843 136 7/19/2022
4.4.0-dev.6804 143 7/19/2022
4.4.0-dev.6789 139 7/19/2022
4.4.0-dev.6760 134 7/19/2022
4.4.0-dev.6705 149 7/14/2022
4.4.0-dev.6663 175 6/24/2022
4.4.0-dev.6655 133 6/24/2022
4.3.0 11,955 6/24/2022
4.3.0-dev.multiple.buckets3 163 6/21/2022
4.3.0-dev.multiple.buckets2 127 6/17/2022
4.3.0-dev.multiple.buckets1 135 6/17/2022
4.3.0-dev.6631 130 6/22/2022
4.3.0-dev.6623 136 6/22/2022
4.3.0-dev.6374 141 6/13/2022
4.3.0-dev.6286 141 5/20/2022
4.2.0 2,426 5/20/2022
4.2.0-dev.6257 143 5/13/2022
4.2.0-dev.6248 140 5/12/2022
4.2.0-dev.6233 146 5/12/2022
4.2.0-dev.6194 142 5/10/2022
4.2.0-dev.6193 138 5/10/2022
4.2.0-dev.6158 2,853 5/6/2022
4.2.0-dev.6135 149 5/6/2022
4.2.0-dev.6091 150 4/28/2022
4.2.0-dev.6048 149 4/28/2022
4.2.0-dev.6047 150 4/28/2022
4.2.0-dev.5966 152 4/25/2022
4.2.0-dev.5938 153 4/19/2022
4.1.0 3,404 4/19/2022
4.1.0-dev.5910 344 4/13/2022
4.1.0-dev.5888 150 4/13/2022
4.1.0-dev.5887 156 4/13/2022
4.1.0-dev.5794 156 4/6/2022
4.1.0-dev.5725 158 3/18/2022
4.0.0 8,063 3/18/2022
4.0.0-rc3 401 3/4/2022
4.0.0-rc2 551 2/25/2022
4.0.0-rc1 212 2/18/2022
4.0.0-dev.5709 149 3/18/2022
4.0.0-dev.5684 160 3/15/2022
4.0.0-dev.5630 160 3/4/2022
4.0.0-dev.5607 154 3/3/2022
4.0.0-dev.5579 157 2/25/2022
4.0.0-dev.5556 162 2/24/2022
4.0.0-dev.5555 147 2/24/2022
4.0.0-dev.5497 146 2/23/2022
4.0.0-dev.5489 159 2/23/2022
4.0.0-dev.5460 153 2/23/2022
4.0.0-dev.5444 147 2/22/2022
4.0.0-dev.5333 154 2/17/2022
4.0.0-dev.5303 149 2/16/2022
4.0.0-dev.5280 161 2/16/2022
4.0.0-dev.5279 161 2/16/2022
4.0.0-dev.5241 256 2/15/2022
4.0.0-dev.5225 150 2/15/2022
4.0.0-dev.5217 155 2/15/2022
4.0.0-dev.5209 145 2/15/2022
4.0.0-dev.5200 150 2/14/2022
4.0.0-dev.5188 151 2/10/2022
4.0.0-dev.5180 151 2/10/2022
4.0.0-dev.5172 154 2/10/2022
4.0.0-dev.5130 146 2/10/2022
4.0.0-dev.5122 154 2/9/2022
4.0.0-dev.5103 161 2/9/2022
4.0.0-dev.5097 158 2/9/2022
4.0.0-dev.5091 153 2/9/2022
4.0.0-dev.5084 153 2/8/2022
3.4.0-dev.5263 163 2/15/2022
3.4.0-dev.4986 154 2/7/2022
3.4.0-dev.4968 169 2/4/2022
3.3.0 8,687 2/4/2022
3.3.0-dev.4889 158 2/3/2022
3.3.0-dev.4865 164 2/1/2022
3.3.0-dev.4823 169 1/19/2022
3.3.0-dev.4691 165 1/7/2022
3.3.0-dev.4557 1,374 11/26/2021
3.2.0 5,908 11/26/2021
3.2.0-dev.4533 4,872 11/24/2021
3.2.0-dev.4484 233 11/11/2021
3.2.0-dev.4475 206 11/10/2021
3.2.0-dev.4387 182 10/26/2021
3.2.0-dev.4363 197 10/22/2021
3.2.0-dev.4356 193 10/22/2021
3.1.0 1,800 10/22/2021
3.1.0-dev.4303 197 10/18/2021
3.1.0-dev.4293 199 10/15/2021
3.1.0-dev.4286 177 10/15/2021
3.1.0-dev.4240 215 10/12/2021
3.1.0-dev.4202 173 10/11/2021
3.1.0-dev.4183 217 10/11/2021
3.1.0-dev.4131 183 10/8/2021
3.1.0-dev.3999 193 10/5/2021
3.1.0-dev.3841 271 9/29/2021
3.1.0-dev.3798 192 9/17/2021
3.0.0 1,214 9/17/2021
3.0.0-dev.3726 532 8/31/2021
3.0.0-dev.3719 177 8/31/2021
3.0.0-dev.3671 191 8/20/2021
2.2.0-dev.3652 186 8/20/2021
2.1.0 1,566 8/20/2021
2.1.0-dev.3605 190 8/17/2021
2.1.0-dev.3584 191 8/16/2021
2.1.0-dev.3558 181 8/16/2021
2.1.0-dev.3527 230 7/29/2021
2.1.0-dev.3519 229 7/29/2021
2.1.0-dev.3490 180 7/20/2021
2.1.0-dev.3445 202 7/12/2021
2.1.0-dev.3434 238 7/9/2021
2.0.0 9,026 7/9/2021
2.0.0-dev.3401 219 6/25/2021
2.0.0-dev.3368 204 6/23/2021
2.0.0-dev.3361 214 6/23/2021
2.0.0-dev.3330 211 6/17/2021
2.0.0-dev.3291 214 6/16/2021
1.20.0-dev.3218 231 6/4/2021
1.19.0 938 6/4/2021
1.19.0-dev.3204 201 6/3/2021
1.19.0-dev.3160 185 6/2/2021
1.19.0-dev.3159 182 6/2/2021
1.19.0-dev.3084 842 5/7/2021
1.19.0-dev.3051 206 5/5/2021
1.19.0-dev.3044 202 5/5/2021
1.19.0-dev.3008 197 4/30/2021
1.18.0 1,245 4/30/2021
1.18.0-dev.2973 216 4/27/2021
1.18.0-dev.2930 195 4/16/2021
1.18.0-dev.2919 193 4/13/2021
1.18.0-dev.2893 179 4/12/2021
1.18.0-dev.2880 198 4/12/2021
1.18.0-dev.2856 192 4/7/2021
1.18.0-dev.2830 288 4/1/2021
1.18.0-dev.2816 194 4/1/2021
1.17.0 774 4/1/2021
1.17.0-dev.linq.17 807 3/18/2021
1.17.0-dev.linq.16 187 3/16/2021
1.17.0-dev.linq.15 220 3/15/2021
1.17.0-dev.linq.14 224 3/12/2021
1.17.0-dev.linq.13 254 3/11/2021
1.17.0-dev.linq.12 205 3/10/2021
1.17.0-dev.linq.11 199 3/8/2021
1.17.0-dev.2776 223 3/26/2021
1.17.0-dev.2713 236 3/25/2021
1.16.0-dev.linq.10 1,241 2/4/2021
1.15.0-dev.linq.9 219 2/4/2021
1.15.0-dev.linq.8 193 1/28/2021
1.15.0-dev.linq.7 210 1/27/2021
1.15.0-dev.linq.6 227 1/20/2021
1.15.0-dev.linq.5 246 1/19/2021
1.15.0-dev.linq.4 209 1/15/2021
1.15.0-dev.linq.3 185 1/14/2021
1.15.0-dev.linq.2 201 1/13/2021
1.15.0-dev.linq.1 224 1/12/2021