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