InfluxDB.Client.Linq 4.8.0-dev.8648

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.8.0-dev.8648
                    
NuGet\Install-Package InfluxDB.Client.Linq -Version 4.8.0-dev.8648
                    
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.8.0-dev.8648" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="InfluxDB.Client.Linq" Version="4.8.0-dev.8648" />
                    
Directory.Packages.props
<PackageReference Include="InfluxDB.Client.Linq" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add InfluxDB.Client.Linq --version 4.8.0-dev.8648
                    
#r "nuget: InfluxDB.Client.Linq, 4.8.0-dev.8648"
                    
#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.
#addin nuget:?package=InfluxDB.Client.Linq&version=4.8.0-dev.8648&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=InfluxDB.Client.Linq&version=4.8.0-dev.8648&prerelease
                    
Install as a Cake Tool

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 = InfluxDBClientFactory.Create("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 = InfluxDBClientFactory.Create("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.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.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 (3)

Showing the top 3 NuGet packages that depend on InfluxDB.Client.Linq:

Package Downloads
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 232 12/5/2024
4.19.0-dev.15189 63 12/5/2024
4.19.0-dev.15188 62 12/5/2024
4.19.0-dev.15178 67 12/5/2024
4.19.0-dev.15177 71 12/5/2024
4.19.0-dev.14906 139 10/2/2024
4.19.0-dev.14897 69 10/2/2024
4.19.0-dev.14896 70 10/2/2024
4.19.0-dev.14895 76 10/2/2024
4.19.0-dev.14811 126 9/13/2024
4.18.0 118,549 9/13/2024
4.18.0-dev.14769 82 9/4/2024
4.18.0-dev.14743 74 9/3/2024
4.18.0-dev.14694 69 9/3/2024
4.18.0-dev.14693 66 9/3/2024
4.18.0-dev.14692 67 9/3/2024
4.18.0-dev.14618 66 9/2/2024
4.18.0-dev.14609 65 9/2/2024
4.18.0-dev.14592 68 9/2/2024
4.18.0-dev.14446 91 8/19/2024
4.18.0-dev.14414 82 8/12/2024
4.17.0 13,051 8/12/2024
4.17.0-dev.headers.read.1 105 7/22/2024
4.17.0-dev.14350 64 8/5/2024
4.17.0-dev.14333 59 8/5/2024
4.17.0-dev.14300 61 8/5/2024
4.17.0-dev.14291 62 8/5/2024
4.17.0-dev.14189 80 7/23/2024
4.17.0-dev.14179 73 7/22/2024
4.17.0-dev.14101 160 7/1/2024
4.17.0-dev.14100 78 7/1/2024
4.17.0-dev.14044 84 6/24/2024
4.16.0 12,842 6/24/2024
4.16.0-dev.13990 81 6/3/2024
4.16.0-dev.13973 75 6/3/2024
4.16.0-dev.13972 76 6/3/2024
4.16.0-dev.13963 79 6/3/2024
4.16.0-dev.13962 74 6/3/2024
4.16.0-dev.13881 75 6/3/2024
4.16.0-dev.13775 95 5/17/2024
4.16.0-dev.13702 82 5/17/2024
4.15.0 3,467 5/17/2024
4.15.0-dev.13674 90 5/14/2024
4.15.0-dev.13567 97 4/2/2024
4.15.0-dev.13558 98 4/2/2024
4.15.0-dev.13525 86 4/2/2024
4.15.0-dev.13524 79 4/2/2024
4.15.0-dev.13433 91 3/7/2024
4.15.0-dev.13432 86 3/7/2024
4.15.0-dev.13407 88 3/7/2024
4.15.0-dev.13390 81 3/7/2024
4.15.0-dev.13388 78 3/7/2024
4.15.0-dev.13282 85 3/6/2024
4.15.0-dev.13257 88 3/6/2024
4.15.0-dev.13113 247 2/1/2024
4.15.0-dev.13104 80 2/1/2024
4.15.0-dev.13081 82 2/1/2024
4.15.0-dev.13040 78 2/1/2024
4.15.0-dev.13039 83 2/1/2024
4.15.0-dev.12863 133 1/8/2024
4.15.0-dev.12846 99 1/8/2024
4.15.0-dev.12837 89 1/8/2024
4.15.0-dev.12726 172 12/1/2023
4.15.0-dev.12725 94 12/1/2023
4.15.0-dev.12724 91 12/1/2023
4.15.0-dev.12691 98 12/1/2023
4.15.0-dev.12658 88 12/1/2023
4.15.0-dev.12649 96 12/1/2023
4.15.0-dev.12624 88 12/1/2023
4.15.0-dev.12471 114 11/7/2023
4.15.0-dev.12462 89 11/7/2023
4.14.0 67,780 11/7/2023
4.14.0-dev.12437 96 11/7/2023
4.14.0-dev.12343 104 11/2/2023
4.14.0-dev.12310 94 11/2/2023
4.14.0-dev.12284 95 11/1/2023
4.14.0-dev.12235 97 11/1/2023
4.14.0-dev.12226 87 11/1/2023
4.14.0-dev.11972 248 8/8/2023
4.14.0-dev.11915 136 7/31/2023
4.14.0-dev.11879 149 7/28/2023
4.13.0 23,197 7/28/2023
4.13.0-dev.11854 119 7/28/2023
4.13.0-dev.11814 130 7/21/2023
4.13.0-dev.11771 123 7/19/2023
4.13.0-dev.11770 132 7/19/2023
4.13.0-dev.11728 121 7/18/2023
4.13.0-dev.11686 119 7/17/2023
4.13.0-dev.11685 119 7/17/2023
4.13.0-dev.11676 133 7/17/2023
4.13.0-dev.11479 122 6/27/2023
4.13.0-dev.11478 121 6/27/2023
4.13.0-dev.11477 126 6/27/2023
4.13.0-dev.11396 133 6/19/2023
4.13.0-dev.11395 114 6/19/2023
4.13.0-dev.11342 127 6/15/2023
4.13.0-dev.11330 138 6/12/2023
4.13.0-dev.11305 129 6/12/2023
4.13.0-dev.11296 134 6/12/2023
4.13.0-dev.11217 136 6/6/2023
4.13.0-dev.11089 126 5/30/2023
4.13.0-dev.11064 135 5/30/2023
4.13.0-dev.10998 124 5/29/2023
4.13.0-dev.10989 130 5/29/2023
4.13.0-dev.10871 134 5/8/2023
4.13.0-dev.10870 116 5/8/2023
4.13.0-dev.10819 142 4/28/2023
4.12.0 14,068 4/28/2023
4.12.0-dev.10777 134 4/27/2023
4.12.0-dev.10768 138 4/27/2023
4.12.0-dev.10759 134 4/27/2023
4.12.0-dev.10742 133 4/27/2023
4.12.0-dev.10685 123 4/27/2023
4.12.0-dev.10684 127 4/27/2023
4.12.0-dev.10643 127 4/27/2023
4.12.0-dev.10642 131 4/27/2023
4.12.0-dev.10569 131 4/27/2023
4.12.0-dev.10193 168 2/23/2023
4.11.0 25,158 2/23/2023
4.11.0-dev.10176 139 2/23/2023
4.11.0-dev.10059 248 1/26/2023
4.10.0 8,300 1/26/2023
4.10.0-dev.10033 163 1/25/2023
4.10.0-dev.10032 162 1/25/2023
4.10.0-dev.10031 161 1/25/2023
4.10.0-dev.9936 2,240 12/26/2022
4.10.0-dev.9935 158 12/26/2022
4.10.0-dev.9881 148 12/21/2022
4.10.0-dev.9880 149 12/21/2022
4.10.0-dev.9818 155 12/16/2022
4.10.0-dev.9773 148 12/12/2022
4.10.0-dev.9756 151 12/12/2022
4.10.0-dev.9693 146 12/6/2022
4.9.0 10,221 12/6/2022
4.9.0-dev.9684 155 12/6/2022
4.9.0-dev.9666 155 12/6/2022
4.9.0-dev.9617 153 12/6/2022
4.9.0-dev.9478 147 12/5/2022
4.9.0-dev.9469 157 12/5/2022
4.9.0-dev.9444 141 12/5/2022
4.9.0-dev.9411 141 12/5/2022
4.9.0-dev.9350 146 12/1/2022
4.8.0 1,639 12/1/2022
4.8.0-dev.9324 152 11/30/2022
4.8.0-dev.9232 151 11/28/2022
4.8.0-dev.9223 149 11/28/2022
4.8.0-dev.9222 157 11/28/2022
4.8.0-dev.9117 160 11/21/2022
4.8.0-dev.9108 146 11/21/2022
4.8.0-dev.9099 160 11/21/2022
4.8.0-dev.9029 151 11/16/2022
4.8.0-dev.8971 153 11/15/2022
4.8.0-dev.8961 164 11/14/2022
4.8.0-dev.8928 159 11/14/2022
4.8.0-dev.8899 159 11/14/2022
4.8.0-dev.8898 157 11/14/2022
4.8.0-dev.8839 169 11/14/2022
4.8.0-dev.8740 146 11/7/2022
4.8.0-dev.8725 153 11/7/2022
4.8.0-dev.8648 146 11/3/2022
4.7.0 25,509 11/3/2022
4.7.0-dev.8625 159 11/2/2022
4.7.0-dev.8594 166 10/31/2022
4.7.0-dev.8579 158 10/31/2022
4.7.0-dev.8557 149 10/31/2022
4.7.0-dev.8540 139 10/31/2022
4.7.0-dev.8518 147 10/31/2022
4.7.0-dev.8517 158 10/31/2022
4.7.0-dev.8509 152 10/31/2022
4.7.0-dev.8377 161 10/26/2022
4.7.0-dev.8360 163 10/25/2022
4.7.0-dev.8350 164 10/24/2022
4.7.0-dev.8335 161 10/24/2022
4.7.0-dev.8334 161 10/24/2022
4.7.0-dev.8223 204 10/19/2022
4.7.0-dev.8178 155 10/17/2022
4.7.0-dev.8170 152 10/17/2022
4.7.0-dev.8148 161 10/17/2022
4.7.0-dev.8133 157 10/17/2022
4.7.0-dev.8097 144 10/17/2022
4.7.0-dev.8034 168 10/11/2022
4.7.0-dev.8025 153 10/11/2022
4.7.0-dev.8009 172 10/10/2022
4.7.0-dev.8001 173 10/10/2022
4.7.0-dev.7959 151 10/4/2022
4.7.0-dev.7905 159 9/30/2022
4.7.0-dev.7875 156 9/29/2022
4.6.0 2,742 9/29/2022
4.6.0-dev.7832 167 9/29/2022
4.6.0-dev.7817 163 9/29/2022
4.6.0-dev.7779 176 9/27/2022
4.6.0-dev.7778 177 9/27/2022
4.6.0-dev.7734 164 9/26/2022
4.6.0-dev.7733 163 9/26/2022
4.6.0-dev.7677 169 9/20/2022
4.6.0-dev.7650 170 9/16/2022
4.6.0-dev.7626 225 9/14/2022
4.6.0-dev.7618 224 9/14/2022
4.6.0-dev.7574 156 9/13/2022
4.6.0-dev.7572 162 9/13/2022
4.6.0-dev.7528 150 9/12/2022
4.6.0-dev.7502 164 9/9/2022
4.6.0-dev.7479 185 9/8/2022
4.6.0-dev.7471 169 9/8/2022
4.6.0-dev.7447 163 9/7/2022
4.6.0-dev.7425 157 9/7/2022
4.6.0-dev.7395 153 9/6/2022
4.6.0-dev.7344 156 8/31/2022
4.6.0-dev.7329 157 8/31/2022
4.6.0-dev.7292 152 8/30/2022
4.6.0-dev.7240 163 8/29/2022
4.5.0 2,878 8/29/2022
4.5.0-dev.7216 154 8/27/2022
4.5.0-dev.7147 161 8/22/2022
4.5.0-dev.7134 166 8/17/2022
4.5.0-dev.7096 167 8/15/2022
4.5.0-dev.7070 174 8/11/2022
4.5.0-dev.7040 200 8/10/2022
4.5.0-dev.7011 174 8/3/2022
4.5.0-dev.6987 181 8/1/2022
4.5.0-dev.6962 182 7/29/2022
4.4.0 14,787 7/29/2022
4.4.0-dev.6901 177 7/25/2022
4.4.0-dev.6843 170 7/19/2022
4.4.0-dev.6804 180 7/19/2022
4.4.0-dev.6789 173 7/19/2022
4.4.0-dev.6760 170 7/19/2022
4.4.0-dev.6705 185 7/14/2022
4.4.0-dev.6663 210 6/24/2022
4.4.0-dev.6655 170 6/24/2022
4.3.0 17,707 6/24/2022
4.3.0-dev.multiple.buckets3 200 6/21/2022
4.3.0-dev.multiple.buckets2 169 6/17/2022
4.3.0-dev.multiple.buckets1 171 6/17/2022
4.3.0-dev.6631 166 6/22/2022
4.3.0-dev.6623 175 6/22/2022
4.3.0-dev.6374 178 6/13/2022
4.3.0-dev.6286 185 5/20/2022
4.2.0 2,509 5/20/2022
4.2.0-dev.6257 179 5/13/2022
4.2.0-dev.6248 179 5/12/2022
4.2.0-dev.6233 184 5/12/2022
4.2.0-dev.6194 185 5/10/2022
4.2.0-dev.6193 180 5/10/2022
4.2.0-dev.6158 2,892 5/6/2022
4.2.0-dev.6135 188 5/6/2022
4.2.0-dev.6091 189 4/28/2022
4.2.0-dev.6048 189 4/28/2022
4.2.0-dev.6047 191 4/28/2022
4.2.0-dev.5966 201 4/25/2022
4.2.0-dev.5938 193 4/19/2022
4.1.0 3,457 4/19/2022
4.1.0-dev.5910 386 4/13/2022
4.1.0-dev.5888 194 4/13/2022
4.1.0-dev.5887 198 4/13/2022
4.1.0-dev.5794 197 4/6/2022
4.1.0-dev.5725 208 3/18/2022
4.0.0 9,443 3/18/2022
4.0.0-rc3 446 3/4/2022
4.0.0-rc2 599 2/25/2022
4.0.0-rc1 257 2/18/2022
4.0.0-dev.5709 192 3/18/2022
4.0.0-dev.5684 208 3/15/2022
4.0.0-dev.5630 203 3/4/2022
4.0.0-dev.5607 197 3/3/2022
4.0.0-dev.5579 201 2/25/2022
4.0.0-dev.5556 207 2/24/2022
4.0.0-dev.5555 195 2/24/2022
4.0.0-dev.5497 189 2/23/2022
4.0.0-dev.5489 202 2/23/2022
4.0.0-dev.5460 199 2/23/2022
4.0.0-dev.5444 197 2/22/2022
4.0.0-dev.5333 197 2/17/2022
4.0.0-dev.5303 192 2/16/2022
4.0.0-dev.5280 205 2/16/2022
4.0.0-dev.5279 207 2/16/2022
4.0.0-dev.5241 304 2/15/2022
4.0.0-dev.5225 195 2/15/2022
4.0.0-dev.5217 202 2/15/2022
4.0.0-dev.5209 192 2/15/2022
4.0.0-dev.5200 199 2/14/2022
4.0.0-dev.5188 200 2/10/2022
4.0.0-dev.5180 195 2/10/2022
4.0.0-dev.5172 198 2/10/2022
4.0.0-dev.5130 192 2/10/2022
4.0.0-dev.5122 198 2/9/2022
4.0.0-dev.5103 207 2/9/2022
4.0.0-dev.5097 204 2/9/2022
4.0.0-dev.5091 198 2/9/2022
4.0.0-dev.5084 202 2/8/2022
3.4.0-dev.5263 215 2/15/2022
3.4.0-dev.4986 206 2/7/2022
3.4.0-dev.4968 216 2/4/2022
3.3.0 8,743 2/4/2022
3.3.0-dev.4889 202 2/3/2022
3.3.0-dev.4865 208 2/1/2022
3.3.0-dev.4823 220 1/19/2022
3.3.0-dev.4691 213 1/7/2022
3.3.0-dev.4557 1,419 11/26/2021
3.2.0 5,968 11/26/2021
3.2.0-dev.4533 4,918 11/24/2021
3.2.0-dev.4484 279 11/11/2021
3.2.0-dev.4475 252 11/10/2021
3.2.0-dev.4387 227 10/26/2021
3.2.0-dev.4363 246 10/22/2021
3.2.0-dev.4356 246 10/22/2021
3.1.0 1,850 10/22/2021
3.1.0-dev.4303 242 10/18/2021
3.1.0-dev.4293 246 10/15/2021
3.1.0-dev.4286 227 10/15/2021
3.1.0-dev.4240 259 10/12/2021
3.1.0-dev.4202 225 10/11/2021
3.1.0-dev.4183 265 10/11/2021
3.1.0-dev.4131 235 10/8/2021
3.1.0-dev.3999 238 10/5/2021
3.1.0-dev.3841 319 9/29/2021
3.1.0-dev.3798 238 9/17/2021
3.0.0 1,268 9/17/2021
3.0.0-dev.3726 581 8/31/2021
3.0.0-dev.3719 223 8/31/2021
3.0.0-dev.3671 239 8/20/2021
2.2.0-dev.3652 234 8/20/2021
2.1.0 1,618 8/20/2021
2.1.0-dev.3605 242 8/17/2021
2.1.0-dev.3584 239 8/16/2021
2.1.0-dev.3558 226 8/16/2021
2.1.0-dev.3527 276 7/29/2021
2.1.0-dev.3519 277 7/29/2021
2.1.0-dev.3490 225 7/20/2021
2.1.0-dev.3445 250 7/12/2021
2.1.0-dev.3434 281 7/9/2021
2.0.0 9,078 7/9/2021
2.0.0-dev.3401 266 6/25/2021
2.0.0-dev.3368 252 6/23/2021
2.0.0-dev.3361 262 6/23/2021
2.0.0-dev.3330 257 6/17/2021
2.0.0-dev.3291 259 6/16/2021
1.20.0-dev.3218 275 6/4/2021
1.19.0 997 6/4/2021
1.19.0-dev.3204 247 6/3/2021
1.19.0-dev.3160 233 6/2/2021
1.19.0-dev.3159 228 6/2/2021
1.19.0-dev.3084 894 5/7/2021
1.19.0-dev.3051 256 5/5/2021
1.19.0-dev.3044 250 5/5/2021
1.19.0-dev.3008 248 4/30/2021
1.18.0 1,300 4/30/2021
1.18.0-dev.2973 268 4/27/2021
1.18.0-dev.2930 250 4/16/2021
1.18.0-dev.2919 244 4/13/2021
1.18.0-dev.2893 234 4/12/2021
1.18.0-dev.2880 252 4/12/2021
1.18.0-dev.2856 242 4/7/2021
1.18.0-dev.2830 339 4/1/2021
1.18.0-dev.2816 247 4/1/2021
1.17.0 842 4/1/2021
1.17.0-dev.linq.17 860 3/18/2021
1.17.0-dev.linq.16 238 3/16/2021
1.17.0-dev.linq.15 270 3/15/2021
1.17.0-dev.linq.14 278 3/12/2021
1.17.0-dev.linq.13 311 3/11/2021
1.17.0-dev.linq.12 257 3/10/2021
1.17.0-dev.linq.11 256 3/8/2021
1.17.0-dev.2776 276 3/26/2021
1.17.0-dev.2713 293 3/25/2021
1.16.0-dev.linq.10 1,297 2/4/2021
1.15.0-dev.linq.9 278 2/4/2021
1.15.0-dev.linq.8 247 1/28/2021
1.15.0-dev.linq.7 263 1/27/2021
1.15.0-dev.linq.6 285 1/20/2021
1.15.0-dev.linq.5 306 1/19/2021
1.15.0-dev.linq.4 266 1/15/2021
1.15.0-dev.linq.3 240 1/14/2021
1.15.0-dev.linq.2 256 1/13/2021
1.15.0-dev.linq.1 279 1/12/2021