diff --git a/CHANGELOG.md b/CHANGELOG.md index c29dc49..d838fde 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## v0.21.0 [08/25/2020] + +### Release Notes +This version introduces methods to query and post strongly typed points using attributes. Thanks to @tituszban for adding the attribute support. Refer to [Readme.md](https://github.com/AdysTech/InfluxDB.Client.Net/blob/master/README.md) for examples. + + ## v0.20.0 [08/11/2020] ### Release Notes @@ -268,4 +274,4 @@ Added the functionality to query for existing data from InfluxDB. Also unknown l ### Bugfixes -- [#3](https://github.com/AdysTech/InfluxDB.Client.Net/pull/3): Double to str conversion fix, Thanks to @spamik \ No newline at end of file +- [#3](https://github.com/AdysTech/InfluxDB.Client.Net/pull/3): Double to str conversion fix, Thanks to @spamik diff --git a/README.md b/README.md index 16a96d3..7acffa7 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,62 @@ var r = await client.PostPointAsync(dbName, valMixed); A collection of points can be posted using `await client.PostPointsAsync (dbName, points)`, where `points` can be collection of different types of `InfluxDatapoint` +#### Writing strongly typed data points to DB + +```Csharp +class YourPoint +{ + [InfluxDBMeasurementName] + public string Measurement { get; set; } + + [InfluxDBTime] + public DateTime Time { get; set; } + + [InfluxDBPrecision] + public TimePrecision Precision { get; set; } + + [InfluxDBRetentionPolicy] + public InfluxRetentionPolicy Retention { get; set; } + + [InfluxDBField("StringFieldName")] + public string StringFieldProperty { get; set; } + + [InfluxDBField("IntFieldName")] + public int IntFieldProperty { get; set; } + + [InfluxDBField("BoolFieldName")] + public bool BoolFieldProperty { get; set; } + + [InfluxDBField("DoubleFieldName")] + public double DoubleFieldProperty { get; set; } + + [InfluxDBTag("TagName")] + public string TagProperty { get; set; } + +} + +var point = new YourPoint +{ + Time = DateTime.UtcNow, + Measurement = measurementName, + Precision = TimePrecision.Seconds, + StringFieldProperty = "FieldValue", + IntFieldProperty = 42, + BoolFieldProperty = true, + DoubleFieldProperty = 3.1415, + TagProperty = "TagValue", + Retention = new InfluxRetentionPolicy() { Name = "Test2" }; +}; + +var r = await client.PostPointAsync(dbName, point); +``` + +This supports all types `InfluxValueField` supports. Additionally it supports tags other than strings, as long as they can be converted to string. + +The parameter that has `InfluxDBRetentionPolicy` applied to it can be an `IInfluxRetentionPolicy` alternatively it will be treated as a string and will be used as the name of the retention policy. + +A collection of points can be posted using `await client.PostPointsAsync(dbName, points)`, where `points` can be collection of arbitrary type `T` with appropriate attributes + #### Query for data points ```Csharp @@ -107,6 +163,18 @@ Second example above will provide multiple series objects, and allows to get dat The last example above makes InfluxDB to split the selected points (100 limited by `limit` clause) to multiple series, each having 10 points as given by `chunk` size. + +#### Query for strongly typed data points + +```Csharp +var r = await client.QueryMultiSeriesAsync(dbName, $"select * from {measurementName}"); +``` + +`QueryMultiSeriesAsync` method returns `List>`, `InfluxSeries` behaves similar to `InfluxSeries` except Entries are not dynamic but strongly typed as T. It uses the same attributes as used for writing the points to match the fields and tags to the matching properties. If a matching property can't be found, the field is discarded. If a property doesn't have a matching field, it's left empty. + +It supports multiple chuncks similarly to `QueryMultiSeriesAsync`. + + #### Retention Policies This library uses a cutsom .Net object to represent the Influx Retention Policy. The `Duration` concept is nicely wraped in `TimeSpan`, so it can be easily manipulated using .Net code. It also supports `ShardDuration` concept introduced in recent versions of InfluxDB. diff --git a/src/AdysTech.InfluxDB.Client.Net.Core.csproj b/src/AdysTech.InfluxDB.Client.Net.Core.csproj index d25a5da..2cc975f 100644 --- a/src/AdysTech.InfluxDB.Client.Net.Core.csproj +++ b/src/AdysTech.InfluxDB.Client.Net.Core.csproj @@ -6,7 +6,7 @@ AdysTech.InfluxDB.Client.Net AdysTech AdysTech;mvadu - 0.20.0.0 + 0.21.0.0 AdysTech.InfluxDB.Client.Net.Core © AdysTech 2016-2020 https://github.com/AdysTech/InfluxDB.Client.Net @@ -31,8 +31,8 @@ 11. Drop databases, measurements or points 12. Get series count or points count for a measurement - 0.20.0.0 - 0.20.0.0 + 0.21.0.0 + 0.21.0.0 InfluxDB Influx TSDB TimeSeries InfluxData Chunking retention RetentionPolicy MIT diff --git a/src/Attributes/InfluxDBField.cs b/src/Attributes/InfluxDBField.cs new file mode 100644 index 0000000..199c9ce --- /dev/null +++ b/src/Attributes/InfluxDBField.cs @@ -0,0 +1,14 @@ +using System; + +namespace AdysTech.InfluxDB.Client.Net +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true)] + public class InfluxDBField : Attribute + { + public readonly string Name; + public InfluxDBField(string name) + { + Name = name; + } + } +} diff --git a/src/Attributes/InfluxDBMeasurementName.cs b/src/Attributes/InfluxDBMeasurementName.cs new file mode 100644 index 0000000..f09d97f --- /dev/null +++ b/src/Attributes/InfluxDBMeasurementName.cs @@ -0,0 +1,7 @@ +using System; + +namespace AdysTech.InfluxDB.Client.Net +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true)] + public class InfluxDBMeasurementName : Attribute { } +} diff --git a/src/Attributes/InfluxDBPrecision.cs b/src/Attributes/InfluxDBPrecision.cs new file mode 100644 index 0000000..500600b --- /dev/null +++ b/src/Attributes/InfluxDBPrecision.cs @@ -0,0 +1,7 @@ +using System; + +namespace AdysTech.InfluxDB.Client.Net +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true)] + public class InfluxDBPrecision : Attribute { } +} diff --git a/src/Attributes/InfluxDBRetentionPolicy.cs b/src/Attributes/InfluxDBRetentionPolicy.cs new file mode 100644 index 0000000..764ec94 --- /dev/null +++ b/src/Attributes/InfluxDBRetentionPolicy.cs @@ -0,0 +1,7 @@ +using System; + +namespace AdysTech.InfluxDB.Client.Net +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true)] + public class InfluxDBRetentionPolicy : Attribute { } +} \ No newline at end of file diff --git a/src/Attributes/InfluxDBTag.cs b/src/Attributes/InfluxDBTag.cs new file mode 100644 index 0000000..6c432f9 --- /dev/null +++ b/src/Attributes/InfluxDBTag.cs @@ -0,0 +1,14 @@ +using System; + +namespace AdysTech.InfluxDB.Client.Net +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true)] + public class InfluxDBTag : Attribute + { + public readonly string Name; + public InfluxDBTag(string name) + { + Name = name; + } + } +} diff --git a/src/Attributes/InfluxDBTime.cs b/src/Attributes/InfluxDBTime.cs new file mode 100644 index 0000000..f590fcf --- /dev/null +++ b/src/Attributes/InfluxDBTime.cs @@ -0,0 +1,7 @@ +using System; + +namespace AdysTech.InfluxDB.Client.Net +{ + [AttributeUsage(AttributeTargets.Property, Inherited = true)] + public class InfluxDBTime : Attribute { } +} diff --git a/src/DataStructures/InfluxDBClient.cs b/src/DataStructures/InfluxDBClient.cs index b2824bf..341c059 100644 --- a/src/DataStructures/InfluxDBClient.cs +++ b/src/DataStructures/InfluxDBClient.cs @@ -2,6 +2,7 @@ using Newtonsoft.Json; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Dynamic; using System.IO; using System.IO.Compression; @@ -9,6 +10,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Reflection; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; @@ -268,6 +270,112 @@ private async Task PostPointsAsync(string dbName, TimePrecision precision, return false; } + private static IInfluxDatapoint ToInfluxDataPoint(T point) + { + if (typeof(IInfluxDatapoint).IsAssignableFrom(typeof(T))) + { + return point as IInfluxDatapoint; + } + + var measurementProp = typeof(T) + .GetProperties() + .Where(prop => prop.IsDefined(typeof(InfluxDBMeasurementName), true)) + .ToList(); + if (!measurementProp.Any()) + throw new CustomAttributeFormatException("InfluxDBMeasurement attribute is required on object published to InfluxDB"); + var measurementName = measurementProp.First().GetValue(point, null) as string; + + var timeProp = typeof(T) + .GetProperties() + .Where(prop => prop.IsDefined(typeof(InfluxDBTime), true)) + .ToList(); + var time = timeProp.Any() ? (DateTime)timeProp.First().GetValue(point, null) : DateTime.UtcNow; + + var precisionProp = typeof(T) + .GetProperties() + .Where(prop => prop.IsDefined(typeof(InfluxDBPrecision), true)) + .ToList(); + var precision = precisionProp.Any() + ? (TimePrecision)precisionProp.First().GetValue(point, null) + : TimePrecision.Seconds; + + var retentionProp = typeof(T) + .GetProperties() + .Where(prop => prop.IsDefined(typeof(InfluxDBRetentionPolicy), true)) + .ToList(); + var retentionPolicy = retentionProp.Any() + ? retentionProp.First().GetValue(point, null) is IInfluxRetentionPolicy policy + ? policy + : new InfluxRetentionPolicy + { + Name = retentionProp.First().GetValue(point, null) as string + } + : null; + + var tags = typeof(T) + .GetProperties() + .Where(prop => prop.IsDefined(typeof(InfluxDBTag), true)) + .ToDictionary( + prop => (prop.GetCustomAttributes(typeof(InfluxDBTag), true).First() as InfluxDBTag)?.Name, + prop => prop.GetValue(point, null).ToString() + ); + + var fields = typeof(T) + .GetProperties() + .Where(prop => prop.IsDefined(typeof(InfluxDBField), true)) + .ToDictionary( + prop => (prop.GetCustomAttributes(typeof(InfluxDBField), true).First() as InfluxDBField)?.Name, + prop => new InfluxValueField(prop.GetValue(point, null) as IComparable) + ); + + return new InfluxDatapoint + { + Precision = precision, + UtcTimestamp = time, + MeasurementName = measurementName, + Retention = retentionPolicy, + Tags = tags, + Fields = fields + }; + } + + private static T FromInfluxDataPoint(dynamic entry) + { + var instance = (T)Activator.CreateInstance(typeof(T)); + var timeProp = typeof(T) + .GetProperties() + .Where(prop => prop.IsDefined(typeof(InfluxDBTime), true)) + .ToList(); + if (timeProp.Any()) + timeProp.First().SetValue(instance, entry.Time); + + var dict = (IDictionary)entry; + + foreach (var prop in typeof(T) + .GetProperties() + .Where(prop => prop.IsDefined(typeof(InfluxDBTag), true)) + .Where(prop => + dict.ContainsKey((prop.GetCustomAttributes(typeof(InfluxDBTag), true).First() as InfluxDBTag)?.Name ?? ""))) + { + var converter = TypeDescriptor.GetConverter(prop.PropertyType); + var tagName = (Enumerable.First(prop.GetCustomAttributes(typeof(InfluxDBTag), true)) as InfluxDBTag)?.Name; + prop.SetValue(instance, converter.ConvertFrom(dict[tagName ?? ""].ToString())); + } + + foreach (var prop in typeof(T) + .GetProperties() + .Where(prop => prop.IsDefined(typeof(InfluxDBField), true)) + .Where(prop => + dict.ContainsKey((prop.GetCustomAttributes(typeof(InfluxDBField), true).First() as InfluxDBField)?.Name ?? ""))) + { + var converter = TypeDescriptor.GetConverter(prop.PropertyType); + var fieldName = (Enumerable.First(prop.GetCustomAttributes(typeof(InfluxDBField), true)) as InfluxDBField)?.Name; + prop.SetValue(instance, converter.ConvertFrom(dict[fieldName ?? ""].ToString())); + } + + return instance; + } + #endregion private methods /// @@ -458,6 +566,18 @@ public async Task PostPointAsync(string dbName, IInfluxDatapoint point) } } + /// + /// Posts an arbitrary object decorated with InfluxDB attributes to a given measurement + /// + /// InfluxDB database name + /// Object to be converted to influx data point and written + /// True:Success, False:Failure + ///When Influx needs authentication, and no user name password is supplied or auth fails + ///all other HTTP exceptions + ///When the provided object is missing required attributes + public Task PostPointAsync(string dbName, T point) + => PostPointAsync(dbName, ToInfluxDataPoint(point)); + /// /// Posts series of InfluxDataPoints to given measurement, in batches of 255 /// @@ -499,6 +619,19 @@ public async Task PostPointsAsync(string dbName, IEnumerable + /// Posts series of arbitrary objects decorated with InfluxDB attributes to a given measurement, in batches of 255 + /// + /// InfluxDB database name + /// Collection of object to be converted to data points and be written + /// Maximal size of Influx batch to be written + /// True:Success, False:Failure + ///When Influx needs authentication, and no user name password is supplied or auth fails + ///all other HTTP exceptions + ///When the provided object is missing required attributes + public Task PostPointsAsync(string dbName, IEnumerable points, int maxBatchSize = 255) + => PostPointsAsync(dbName, points.Select(ToInfluxDataPoint), maxBatchSize); + /// /// InfluxDB engine version /// @@ -687,6 +820,56 @@ public async Task> QueryMultiSeriesAsync(string dbName, stri return null; } + /// + /// Queries Influx DB and gets a time series data back. Ideal for fetching measurement values. + /// The return list is of T objects, and each element in there will have values deserialized assuming correct attribute usage + /// + /// Name of the database + /// Query text, Supports multi series results + /// retention policy containing the measurement + /// epoch precision of the data set + /// List of InfluxSeries + public async Task>> QueryMultiSeriesAsync(string dbName, string measurementQuery, string retentionPolicy = null, TimePrecision precision = TimePrecision.Nanoseconds) + => (await QueryMultiSeriesAsync(dbName, measurementQuery, retentionPolicy, precision)) + .Select(series => + new InfluxSeries + { + SeriesName = series.SeriesName, + Tags = series.Tags, + HasEntries = series.HasEntries, + Partial = series.Partial, + Entries = series.Entries.Select(FromInfluxDataPoint).ToList().AsReadOnly(), + } as IInfluxSeries) + .ToList(); + + /// + /// Queries Influx DB and gets a time series data back. Ideal for fetching measurement values. + /// The return list is of T objects, and each element in there will have values deserialized assuming correct attribute usage + /// THis uses Chunking support from InfluxDB. It returns results in streamed batches rather than as a single response + /// Responses will be chunked by series or by every ChunkSize points, whichever occurs first. + /// + /// Name of the database + /// Query text, Only results with single series are supported for now + /// Maximum Number of points in a chunk + /// retention policy containing the measurement + /// epoch precision of the data set + /// List of InfluxSeries + /// + + public async Task>> QueryMultiSeriesAsync(string dbName, string measurementQuery, int ChunkSize, string retentionPolicy = null, TimePrecision precision = TimePrecision.Nanoseconds) + => (await QueryMultiSeriesAsync(dbName, measurementQuery, ChunkSize, retentionPolicy, precision)) + .Select(series => + new InfluxSeries + { + SeriesName = series.SeriesName, + Tags = series.Tags, + HasEntries = series.HasEntries, + Partial = series.Partial, + Entries = series.Entries.Select(FromInfluxDataPoint).ToList().AsReadOnly(), + } as IInfluxSeries) + .ToList(); + + /// /// Convert the Influx Series JSON objects to InfluxSeries /// diff --git a/src/DataStructures/InfluxDatapoint.cs b/src/DataStructures/InfluxDatapoint.cs index 1009e5f..2e5cf99 100644 --- a/src/DataStructures/InfluxDatapoint.cs +++ b/src/DataStructures/InfluxDatapoint.cs @@ -22,13 +22,13 @@ public class InfluxDatapoint : IInfluxDatapoint, IInfluxDatapoint where T /// /// Dictionary storing all Tag/Value combinations /// - public Dictionary Tags { get; private set; } + public Dictionary Tags { get; internal set; } /// /// The key-value pair in InfluxDB’s data structure that records metadata and the actual data value. /// Fields are required in InfluxDB’s data structure and they are not indexed. /// - public Dictionary Fields { get; private set; } + public Dictionary Fields { get; internal set; } /// /// Timestamp for the point, will be converted to Epoch, expcted to be in UTC diff --git a/src/DataStructures/InfluxSeries.cs b/src/DataStructures/InfluxSeries.cs index a00cbd7..1f42ca2 100644 --- a/src/DataStructures/InfluxSeries.cs +++ b/src/DataStructures/InfluxSeries.cs @@ -41,4 +41,40 @@ public class InfluxSeries : IInfluxSeries /// public bool Partial { get; set; } } + + /// + /// Represents the results returned by Query end point of the InfluxDB engine + /// + + public class InfluxSeries : IInfluxSeries + { + + /// + /// Name of the series. Usually its MeasurementName in case of select queries + /// + public string SeriesName { get; internal set; } + + /// + /// Dictionary of tags, and their respective values. + /// + public IReadOnlyDictionary Tags { get; internal set; } + + /// + /// Indicates whether this Series has any entries or not + /// + public bool HasEntries { get; internal set; } + + /// + /// Read only List of ExpandoObjects (in the form of dynamic) representing the entries in the query result + /// The objects will have columns as Peoperties with their current values + /// + public IReadOnlyList Entries { get; internal set; } + + /// + /// True if the influx query was answered with a partial response due to e.g. exceeding a configured + /// max-row-limit in the InfluxDB. As we don't know which series was truncated by InfluxDB, all series + /// of the response will be flagged with Partial=true. + /// + public bool Partial { get; set; } + } } diff --git a/src/DataStructures/InfluxValueField.cs b/src/DataStructures/InfluxValueField.cs index f88461f..1121df4 100644 --- a/src/DataStructures/InfluxValueField.cs +++ b/src/DataStructures/InfluxValueField.cs @@ -47,7 +47,7 @@ public override string ToString() else if (Value is DateTime dtValue) { // Unix nanosecond timestamp. Specify alternative precisions with the HTTP API. The minimum valid timestamp is -9223372036854775806 or 1677-09-21T00:12:43.145224194Z. The maximum valid timestamp is 9223372036854775806 or 2262-04-11T23:47:16.854775806Z. - // InfluxDb does not support a datetime type for fields or tags + // InfluxDB does not support a datetime type for fields or tags // Convert datetime to UNIX long return dtValue.ToEpoch(TimePrecision.Milliseconds).ToString(); } diff --git a/src/Interfaces/IInfluxDBClient.cs b/src/Interfaces/IInfluxDBClient.cs index 2995b8e..03618f7 100644 --- a/src/Interfaces/IInfluxDBClient.cs +++ b/src/Interfaces/IInfluxDBClient.cs @@ -57,6 +57,17 @@ public interface IInfluxDBClient ///all other HTTP exceptions Task PostPointAsync(string dbName, IInfluxDatapoint point); + /// + /// Posts an arbitrary object decorated with InfluxDB attributes to a given measurement + /// + /// InfluxDB database name + /// Object to be converted to influx data point and written + /// True:Success, False:Failure + ///When Influx needs authentication, and no user name password is supplied or auth fails + ///all other HTTP exceptions + ///When the provided object is missing required attributes + Task PostPointAsync(string dbName, T point); + /// /// Posts series of InfluxDataPoints to given measurement, in batches of 255 /// @@ -68,6 +79,17 @@ public interface IInfluxDBClient ///all other HTTP exceptions Task PostPointsAsync(string dbName, IEnumerable points, int maxBatchSize = 255); + /// + /// Posts series of arbitrary objects decorated with InfluxDB attributes to a given measurement, in batches of 255 + /// + /// InfluxDB database name + /// Collection of object to be converted to data points and be written + /// Maximal size of Influx batch to be written + /// True:Success, False:Failure + ///When Influx needs authentication, and no user name password is supplied or auth fails + ///all other HTTP exceptions + ///When the provided object is missing required attributes + Task PostPointsAsync(string dbName, IEnumerable points, int maxBatchSize = 255); /// /// Gets the list of retention policies present in a DB @@ -94,6 +116,17 @@ public interface IInfluxDBClient /// List of InfluxSeries Task> QueryMultiSeriesAsync(string dbName, string measurementQuery, string retentionPolicy = null, TimePrecision precision = TimePrecision.Nanoseconds); + /// + /// Queries Influx DB and gets a time series data back. Ideal for fetching measurement values. + /// The return list is of T objects, and each element in there will have values deserialized assuming correct attribute usage + /// + /// Name of the database + /// Query text, Supports multi series results + /// retention policy containing the measurement + /// epoch precision of the data set + /// List of InfluxSeries + Task>> QueryMultiSeriesAsync(string dbName, string measurementQuery, string retentionPolicy = null, TimePrecision precision = TimePrecision.Nanoseconds); + /// /// Queries Influx DB and gets a time series data back. Ideal for fetching measurement values. @@ -111,6 +144,22 @@ public interface IInfluxDBClient Task> QueryMultiSeriesAsync(string dbName, string measurementQuery, int ChunkSize, string retentionPolicy = null, TimePrecision precision = TimePrecision.Nanoseconds); + /// + /// Queries Influx DB and gets a time series data back. Ideal for fetching measurement values. + /// The return list is of T objects, and each element in there will have values deserialized assuming correct attribute usage + /// THis uses Chunking support from InfluxDB. It returns results in streamed batches rather than as a single response + /// Responses will be chunked by series or by every ChunkSize points, whichever occurs first. + /// + /// Name of the database + /// Query text, Only results with single series are supported for now + /// Maximum Number of points in a chunk + /// retention policy containing the measurement + /// epoch precision of the data set + /// List of InfluxSeries + /// + + Task>> QueryMultiSeriesAsync(string dbName, string measurementQuery, int ChunkSize, string retentionPolicy = null, TimePrecision precision = TimePrecision.Nanoseconds); + /// /// Gets the list of Continuous Queries present in Influx Instance diff --git a/src/Interfaces/IInfluxSeries.cs b/src/Interfaces/IInfluxSeries.cs index 94f3cd8..eff5305 100644 --- a/src/Interfaces/IInfluxSeries.cs +++ b/src/Interfaces/IInfluxSeries.cs @@ -9,7 +9,7 @@ public interface IInfluxSeries { /// /// Read only List of ExpandoObjects (in the form of dynamic) representing the entries in the query result - /// The objects will have columns as Peoperties with their current values + /// The objects will have columns as Properties with their current values /// IReadOnlyList Entries { get; } @@ -35,4 +35,38 @@ public interface IInfluxSeries /// bool Partial { get; set; } } + + /// + /// Represents the results returned by Query end point of the InfluxDB engine, supporting strong typing + /// + public interface IInfluxSeries + { + /// + /// Read only List of T representing the entries in the query result + /// The objects will have been deserialized from the results + /// + IReadOnlyList Entries { get; } + + /// + /// Indicates whether this Series has any entries or not + /// + bool HasEntries { get; } + + /// + /// Name of the series. Usually its MeasurementName in case of select queries + /// + string SeriesName { get; } + + /// + /// Dictionary of tags, and their respective values. + /// + IReadOnlyDictionary Tags { get; } + + /// + /// True if the influx query was answered with a partial response due to e.g. exceeding a configured + /// max-row-limit in the InfluxDB. As we don't know which series was truncated by InfluxDB, all series + /// of the response will be flagged with Partial=true. + /// + bool Partial { get; set; } + } } \ No newline at end of file diff --git a/tests/InfluxDBClientTest.cs b/tests/InfluxDBClientTest.cs index b571ba0..751a0bb 100644 --- a/tests/InfluxDBClientTest.cs +++ b/tests/InfluxDBClientTest.cs @@ -42,7 +42,7 @@ public async Task TestGetInfluxDBNamesAsync() s.Stop(); Debug.WriteLine(s.ElapsedMilliseconds); - Assert.IsTrue(r != null, "GetInfluxDBNamesAsync retunred null or empty collection"); + Assert.IsTrue(r != null, "GetInfluxDBNamesAsync returned null or empty collection"); } catch (Exception e) @@ -66,7 +66,7 @@ public async Task TestCreateDatabaseAsync() { var client = new InfluxDBClient(influxUrl, dbUName, dbpwd); var r = await client.CreateDatabaseAsync(dbName); - Assert.IsTrue(r, "CreateDatabaseAsync retunred false"); + Assert.IsTrue(r, "CreateDatabaseAsync returned false"); } catch (InvalidOperationException e) { @@ -86,7 +86,7 @@ public async Task TestGetInfluxDBStructureAsync_InvalidDB() { var client = new InfluxDBClient(influxUrl, dbUName, dbpwd); var r = await client.GetInfluxDBStructureAsync("InvalidDB"); - Assert.IsTrue(r != null && r.Measurements.Count() == 0, "GetInfluxDBStructureAsync retunred non null or non empty collection"); + Assert.IsTrue(r != null && r.Measurements.Count() == 0, "GetInfluxDBStructureAsync returned non null or non empty collection"); } catch (Exception e) { @@ -106,7 +106,7 @@ public async Task TestGetInfluxDBStructureAsync() var r = await client.GetInfluxDBStructureAsync(dbName); s.Stop(); Debug.WriteLine(s.ElapsedMilliseconds); - Assert.IsTrue(r != null && r.Measurements.Count >= 0, "GetInfluxDBStructureAsync retunred null or non empty collection"); + Assert.IsTrue(r != null && r.Measurements.Count >= 0, "GetInfluxDBStructureAsync returned null or non empty collection"); } catch (Exception e) { @@ -127,10 +127,56 @@ public async Task TestQueryMultiSeriesAsync() s.Stop(); Debug.WriteLine($"Elapsed{s.ElapsedMilliseconds}"); - Assert.IsTrue(r != null, "QueryMultiSeriesAsync retunred null or invalid data"); + Assert.IsTrue(r != null, "QueryMultiSeriesAsync returned null or invalid data"); } + [TestMethod, TestCategory("Query")] + public async Task TestQueryMultiSeriesAsync_ToObject() + { + var client = new InfluxDBClient(influxUrl, dbUName, dbpwd); + + var tagId = DataGen.RandomString(); + + var points = Enumerable.Range(0, 3).Select(i => + new PointObjectWithStringRetention + { + Time = DateTime.UtcNow, + Measurement = measurementName, + Precision = TimePrecision.Nanoseconds, + StringTag = tagId, + IntTag = i, + StringField = DataGen.RandomString(), + DoubleField = DataGen.RandomDouble(), + IntField = DataGen.RandomInt(), + BoolField = i % 2 == 0, + } + ).ToList(); + + var postResult = await client.PostPointsAsync(dbName, points); + + Assert.IsTrue(postResult, "PostPointsAsync returned false"); + Stopwatch s = new Stopwatch(); + s.Start(); + var queryResult = await client.QueryMultiSeriesAsync(dbName, $"SELECT * from {measurementName} WHERE StringTag='{tagId}'"); + + s.Stop(); + Debug.WriteLine($"Elapsed {s.ElapsedMilliseconds}ms"); + + Assert.IsTrue(queryResult != null, "QueryMultiSeriesAsync returned null or invalid data"); + Assert.IsTrue(queryResult.Count == 1, "QueryMultiSeriesAsync returned invalid number of series"); + foreach (var point in points) + { + var matching = queryResult[0].Entries.FirstOrDefault(e => e.IntTag == point.IntTag); + + Assert.IsTrue(matching != null, $"Missing record corresponding to record {point.IntTag}"); + + Assert.IsTrue(matching.StringField == point.StringField, $"Mismatching string field on record {point.IntTag}"); + Assert.IsTrue(Math.Abs(matching.DoubleField - point.DoubleField) < .0000001, $"Mismatching double field on record {point.IntTag}"); + Assert.IsTrue(matching.IntField == point.IntField, $"Mismatching int field on record {point.IntTag}"); + Assert.IsTrue(matching.BoolField == point.BoolField, $"Mismatching bool field on record {point.IntTag}"); + } + } [TestMethod, TestCategory("Post")] public async Task TestPostPointsAsync() @@ -204,7 +250,7 @@ public async Task TestPostPointsAsync() var r = await client.PostPointsAsync(dbName, points); - Assert.IsTrue(r, "PostPointsAsync retunred false"); + Assert.IsTrue(r, "PostPointsAsync returned false"); } catch (Exception e) { @@ -214,6 +260,39 @@ public async Task TestPostPointsAsync() } } + [TestMethod, TestCategory("Post")] + public async Task TestPostPointsAsync_FromObjects() + { + try + { + var client = new InfluxDBClient(influxUrl, dbUName, dbpwd); + var time = DateTime.Now; + + var points = Enumerable.Range(0, 3).Select(i => + new PointObjectWithStringRetention + { + Time = DateTime.UtcNow, + Measurement = measurementName, + Precision = TimePrecision.Milliseconds, + StringTag = "FromObject", + IntTag = DataGen.RandomInt(), + StringField = DataGen.RandomString(), + DoubleField = DataGen.RandomDouble(), + IntField = DataGen.RandomInt(), + BoolField = i % 2 == 0, + } + ); + + var r = await client.PostPointsAsync(dbName, points); + Assert.IsTrue(r, "PostPointsAsync returned false"); + } + catch (Exception e) + { + + Assert.Fail($"Unexpected exception of type {e.GetType()} caught: {e.Message}"); + } + } + [TestMethod, TestCategory("Post")] public async Task TestPostPointAsync_InvalidReq() @@ -354,7 +433,38 @@ public async Task TestPostMixedPointAsync() valMixed.MeasurementName = measurementName; valMixed.Precision = TimePrecision.Seconds; var r = await client.PostPointAsync(dbName, valMixed); - Assert.IsTrue(r, "PostPointAsync retunred false"); + Assert.IsTrue(r, "PostPointAsync returned false"); + } + catch (Exception e) + { + + Assert.Fail($"Unexpected exception of type {e.GetType()} caught: {e.Message}"); + return; + } + } + + [TestMethod, TestCategory("Post")] + public async Task TestPostObjectPointAsync() + { + try + { + var client = new InfluxDBClient(influxUrl, dbUName, dbpwd); + + var point = new PointObjectWithStringRetention + { + Time = DateTime.UtcNow, + Measurement = measurementName, + Precision = TimePrecision.Seconds, + StringTag = "FromObject", + IntTag = DataGen.RandomInt(), + StringField = DataGen.RandomString(), + DoubleField = DataGen.RandomDouble(), + IntField = DataGen.RandomInt(), + BoolField = true, + }; + + var r = await client.PostPointAsync(dbName, point); + Assert.IsTrue(r, "PostPointAsync returned false"); } catch (Exception e) { @@ -386,7 +496,7 @@ public async Task TestPostPointAsyncNonDefaultRetention() valMixed.Retention = new InfluxRetentionPolicy() { Duration = TimeSpan.FromHours(6) }; var r = await client.PostPointAsync(dbName, valMixed); - Assert.IsTrue(r && valMixed.Saved, "PostPointAsync retunred false"); + Assert.IsTrue(r && valMixed.Saved, "PostPointAsync returned false"); } catch (Exception e) { @@ -535,7 +645,7 @@ public async Task TestQueryMultiSeriesAsync_Timeseries() s.Stop(); Debug.WriteLine($"Elapsed{s.ElapsedMilliseconds}"); - Assert.IsTrue(r != null && r.Count > 0, "QueryMultiSeriesAsync retunred null or invalid data"); + Assert.IsTrue(r != null && r.Count > 0, "QueryMultiSeriesAsync returned null or invalid data"); } @@ -563,7 +673,7 @@ public async Task TestPostPointsAsync_DifferentPrecisions() } var r = await client.PostPointsAsync(dbName, points); - Assert.IsTrue(r, "PostPointsAsync retunred false"); + Assert.IsTrue(r, "PostPointsAsync returned false"); var values = await client.QueryMultiSeriesAsync(dbName, "select * from /Precision[A-Za-z]s/"); foreach (var val in values) @@ -623,7 +733,7 @@ public async Task TestQueryMultiSeriesAsync_Chunking_BySeries() } var r = await client.PostPointsAsync(dbName, points); - Assert.IsTrue(r, "PostPointsAsync retunred false"); + Assert.IsTrue(r, "PostPointsAsync returned false"); var values = await client.QueryMultiSeriesAsync(dbName, $"select sum(Val) from ChunkTest where TestTime ='{ TestTime}' group by ChunkSeries", chunkSize * 10); foreach (var val in values) @@ -667,7 +777,7 @@ public async Task TestQueryMultiSeriesAsync_Chunking_BySize() } var r = await client.PostPointsAsync(dbName, points); - Assert.IsTrue(r, "PostPointsAsync retunred false"); + Assert.IsTrue(r, "PostPointsAsync returned false"); var values = await client.QueryMultiSeriesAsync(dbName, $"select Val from ChunkTest where TestTime ='{ TestTime}' limit {chunkSize * 5}", chunkSize); foreach (var val in values) @@ -693,7 +803,7 @@ public async Task TestPostPointsAsync_AutogenRetention() var points = await CreateTestPoints("RetentionTest", 10, TimePrecision.Nanoseconds, retention); var r = await client.PostPointsAsync(dbName, points); - Assert.IsTrue(r, "PostPointsAsync retunred false"); + Assert.IsTrue(r, "PostPointsAsync returned false"); Assert.IsTrue(points.Count(p => p.Saved) == 10, "PostPointsAsync failed with autogen default retention policy"); } @@ -732,7 +842,7 @@ private static async Task> CreateTestPoints(string Measur } [TestMethod, TestCategory("Post")] - public async Task TestPostPointsAsync_OlderthanRetention() + public async Task TestPostPointsAsync_OlderThanRetention() { try { @@ -741,7 +851,7 @@ public async Task TestPostPointsAsync_OlderthanRetention() var points = await CreateTestPoints("RetentionTest", 10, TimePrecision.Nanoseconds, retention); var r = await client.PostPointsAsync(dbName, points); - Assert.IsTrue(r, "PostPointsAsync retunred false"); + Assert.IsTrue(r, "PostPointsAsync returned false"); Assert.IsTrue(points.Count(p => p.Saved) == 1, "PostPointsAsync saved points older than retention policy"); @@ -761,7 +871,39 @@ public async Task TestPostPointsAsync_DefaultTimePrecision() var client = new InfluxDBClient(influxUrl, dbUName, dbpwd); var points = await CreateTestPoints("DefaultPrecisionTest", 10); var r = await client.PostPointsAsync(dbName, points); - Assert.IsTrue(r, "PostPointsAsync retunred false"); + Assert.IsTrue(r, "PostPointsAsync returned false"); + } + catch (Exception e) + { + Assert.Fail($"Unexpected exception of type {e.GetType()} caught: {e.Message}"); + return; + } + } + + [TestMethod, TestCategory("Post")] + public async Task TestPostPointsAsync_FromObject_AutogenRetention() + { + try + { + var client = new InfluxDBClient(influxUrl, dbUName, dbpwd); + var retention = new InfluxRetentionPolicy() { Name = "autogen", DBName = dbName, Duration = TimeSpan.FromMinutes(0), IsDefault = true }; + var points = Enumerable.Range(0, 10).Select(i => + new PointObjectWithObjectRetention + { + Measurement = "RetentionTest", + Time = DateTime.UtcNow.AddDays(-i), + Retention = retention, + Precision = TimePrecision.Milliseconds, + StringTag = "FromObject", + IntTag = DataGen.RandomInt(), + StringField = DataGen.RandomString(), + DoubleField = DataGen.RandomDouble(), + IntField = DataGen.RandomInt(), + BoolField = i % 2 == 0, + }); + + var r = await client.PostPointsAsync(dbName, points); + Assert.IsTrue(r, "PostPointsAsync returned false"); } catch (Exception e) { @@ -866,7 +1008,7 @@ public async Task TestPartialResponseBecauseOfMaxRowLimit() } - Assert.IsTrue(await client.PostPointsAsync(dbName, points, 25000), "PostPointsAsync retunred false"); + Assert.IsTrue(await client.PostPointsAsync(dbName, points, 25000), "PostPointsAsync returned false"); var r = await client.QueryMultiSeriesAsync(dbName, "SELECT * FROM Partialtest"); Assert.IsTrue(r.All(s => s.Partial), "Not all influx series returned by the query contained the flag 'partial=true'"); @@ -888,10 +1030,10 @@ public async Task TestDropDatabaseAsync() var db = "hara-kiri"; var client = new InfluxDBClient(influxUrl, dbUName, dbpwd); var r = await client.CreateDatabaseAsync(db); - Assert.IsTrue(r, "CreateDatabaseAsync retunred false"); + Assert.IsTrue(r, "CreateDatabaseAsync returned false"); var d = new InfluxDatabase(db); r = await client.DropDatabaseAsync(d); - Assert.IsTrue(r && d.Deleted, "DropDatabaseAsync retunred false"); + Assert.IsTrue(r && d.Deleted, "DropDatabaseAsync returned false"); } [TestMethod, TestCategory("Drop")] @@ -903,7 +1045,7 @@ public async Task TestDropMeasurementAsync() var r = await client.PostPointsAsync(dbName, points); var d = new InfluxMeasurement(measurement); r = await client.DropMeasurementAsync(new InfluxDatabase(dbName), d); - Assert.IsTrue(r && d.Deleted, "DropMeasurementAsync retunred false"); + Assert.IsTrue(r && d.Deleted, "DropMeasurementAsync returned false"); } [TestMethod, TestCategory("Drop")] @@ -933,7 +1075,7 @@ public async Task TestDeletePointsAsync() "purge = yes", $"time() > {DateTime.UtcNow.AddDays(-4).ToEpoch(TimePrecision.Hours)}" }); - Assert.IsTrue(r, "DropMeasurementAsync retunred false"); + Assert.IsTrue(r, "DropMeasurementAsync returned false"); } [TestMethod, TestCategory("Delete")] @@ -948,4 +1090,46 @@ public async Task TestDeletePointsAsyncError() } } + + internal abstract class PointObject + { + [InfluxDBMeasurementName] + public string Measurement { get; set; } + + [InfluxDBTime] + public DateTime Time { get; set; } + + [InfluxDBPrecision] + public TimePrecision Precision { get; set; } + + [InfluxDBField("IntField")] + public int IntField { get; set; } + + [InfluxDBField("DoubleField")] + public double DoubleField { get; set; } + + [InfluxDBField("BoolField")] + public bool BoolField { get; set; } + + [InfluxDBField("StringField")] + public string StringField { get; set; } + + [InfluxDBTag("StringTag")] + public string StringTag { get; set; } + + [InfluxDBTag("IntTag")] + public int IntTag { get; set; } + } + + internal class PointObjectWithStringRetention : PointObject + { + [InfluxDBRetentionPolicy] + public string Retention { get; set; } + } + + internal class PointObjectWithObjectRetention : PointObject + { + [InfluxDBRetentionPolicy] + public InfluxRetentionPolicy Retention { get; set; } + } } \ No newline at end of file