diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a7570af3..ce81df14b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features 1. [#239](https://github.com/influxdata/influxdb-client-csharp/pull/239): Add support for Asynchronous queries [LINQ] +1. [#240](https://github.com/influxdata/influxdb-client-csharp/pull/240): Add IsMeasurement option to Column attribute for dynamic measurement names in POCO classes ## 3.0.0 [2021-09-17] @@ -11,7 +12,6 @@ Adds a `Type` overload for POCOs to `QueryAsync`. This will add `object ConvertT ### Features 1. [#232](https://github.com/influxdata/influxdb-client-csharp/pull/232): Add a `Type` overload for POCOs to `QueryAsync`. 1. [#233](https://github.com/influxdata/influxdb-client-csharp/pull/233): Add possibility to follow HTTP redirects -1. [#239](https://github.com/influxdata/influxdb-client-csharp/pull/239): Add support for Asynchronous queries [LINQ] ### Bug Fixes 1. [#236](https://github.com/influxdata/influxdb-client-csharp/pull/236): Mapping `long` type into Flux AST [LINQ] diff --git a/Client.Core.Test/AbstractTest.cs b/Client.Core.Test/AbstractTest.cs index eb84addb4..af6316c51 100644 --- a/Client.Core.Test/AbstractTest.cs +++ b/Client.Core.Test/AbstractTest.cs @@ -114,13 +114,13 @@ private async Task InfluxDbRequest(HttpRequestMessage request) try { var response = await httpClient.SendAsync(request); - Assert.IsTrue(response.IsSuccessStatusCode); + Assert.IsTrue(response.IsSuccessStatusCode, $"Failed to make HTTP request: {response.ReasonPhrase}"); Thread.Sleep(DefaultInfluxDBSleep); } - catch (Exception) + catch (Exception e) { - Assert.Fail("Unexpected exception"); + Assert.Fail("Unexpected exception: " + e); } } } diff --git a/Client.Core/Attributes.cs b/Client.Core/Attributes.cs index 9d2976779..6575d6f50 100644 --- a/Client.Core/Attributes.cs +++ b/Client.Core/Attributes.cs @@ -5,6 +5,7 @@ namespace InfluxDB.Client.Core /// /// The annotation is used for mapping POCO class into line protocol. /// + [AttributeUsage(AttributeTargets.Class)] public sealed class Measurement : Attribute { public string Name { get; } @@ -18,10 +19,13 @@ public Measurement(string name) /// /// The annotation is used to customize bidirectional mapping between POCO and Flux query result or Line Protocol. /// + [AttributeUsage(AttributeTargets.Property)] public sealed class Column : Attribute { public string Name { get; } + public bool IsMeasurement { get; set; } + public bool IsTag { get; set; } public bool IsTimestamp { get; set; } diff --git a/Client.Core/Flux/Internal/AttributesCache.cs b/Client.Core/Flux/Internal/AttributesCache.cs index 91cda2501..7b9681451 100644 --- a/Client.Core/Flux/Internal/AttributesCache.cs +++ b/Client.Core/Flux/Internal/AttributesCache.cs @@ -30,7 +30,7 @@ public PropertyInfo[] GetProperties(Type type) } /// - /// Get Mapping attribute for specified propery. + /// Get Mapping attribute for specified property. /// /// property of DomainObject /// Property Attribute diff --git a/Client.Core/Flux/Internal/FluxResultMapper.cs b/Client.Core/Flux/Internal/FluxResultMapper.cs index 180c27946..1fd817ad7 100644 --- a/Client.Core/Flux/Internal/FluxResultMapper.cs +++ b/Client.Core/Flux/Internal/FluxResultMapper.cs @@ -69,6 +69,10 @@ internal object ToPoco(FluxRecord record, Type type) { var attribute = _attributesCache.GetAttribute(property); + if (attribute != null && attribute.IsMeasurement) + { + SetFieldValue(poco, property, record.GetMeasurement()); + } if (attribute != null && attribute.IsTimestamp) { SetFieldValue(poco, property, record.GetTime()); diff --git a/Client.Linq.Test/DomainObjects.cs b/Client.Linq.Test/DomainObjects.cs index af1a51c0c..9228c5db2 100644 --- a/Client.Linq.Test/DomainObjects.cs +++ b/Client.Linq.Test/DomainObjects.cs @@ -37,6 +37,18 @@ class SensorDateTimeOffset [Column(IsTimestamp = true)] public DateTimeOffset Timestamp { get; set; } } + class SensorWithCustomMeasurement + { + [Column(IsMeasurement = true)] + public string Measurement { get; set; } + + [Column("sensor_id", IsTag = true)] + public string SensorId { get; set; } + + [Column("data")] + public int Value { get; set; } + } + class SensorCustom { public Guid Id { get; set; } diff --git a/Client.Linq.Test/InfluxDBQueryVisitorTest.cs b/Client.Linq.Test/InfluxDBQueryVisitorTest.cs index a1516675f..80ac890c7 100644 --- a/Client.Linq.Test/InfluxDBQueryVisitorTest.cs +++ b/Client.Linq.Test/InfluxDBQueryVisitorTest.cs @@ -428,6 +428,31 @@ where month11 > s.Timestamp } } + [Test] + public void ResultOperatorByMeasurement() + { + var query = from s in InfluxDBQueryable.Queryable("my-bucket", "my-org", _queryApi) + where s.Value > 10 + where s.Measurement == "my-measurement" + select s; + var visitor = BuildQueryVisitor(query); + + const string expected = "start_shifted = int(v: time(v: p2))\n\nfrom(bucket: p1) " + + "|> range(start: time(v: start_shifted)) " + + "|> filter(fn: (r) => (r[\"_measurement\"] == p4)) " + + "|> pivot(rowKey:[\"_time\"], columnKey: [\"_field\"], valueColumn: \"_value\") " + + "|> drop(columns: [\"_start\", \"_stop\", \"_measurement\"]) " + + "|> filter(fn: (r) => (r[\"data\"] > p3))"; + + Assert.AreEqual(expected, visitor.BuildFluxQuery()); + + var ast = visitor.BuildFluxAST(); + + var measurementAssignment = ((OptionStatement)ast.Body[3]).Assignment as VariableAssignment; + Assert.AreEqual("p4", measurementAssignment?.Id.Name); + Assert.AreEqual("my-measurement", (measurementAssignment?.Init as StringLiteral)?.Value); + } + [Test] public void TimestampAsDateTimeOffset() { diff --git a/Client.Linq/IMemberNameResolver.cs b/Client.Linq/IMemberNameResolver.cs index fbae526e1..6fb4e60c2 100644 --- a/Client.Linq/IMemberNameResolver.cs +++ b/Client.Linq/IMemberNameResolver.cs @@ -33,6 +33,7 @@ public interface IMemberNameResolver public enum MemberType { + Measurement, Tag, Field, Timestamp, @@ -52,6 +53,11 @@ public MemberType ResolveMemberType(MemberInfo memberInfo) if (attribute != null) { + if (attribute.IsMeasurement) + { + return MemberType.Measurement; + } + if (attribute.IsTag) { return MemberType.Tag; diff --git a/Client.Linq/Internal/Expressions/ColumnName.cs b/Client.Linq/Internal/Expressions/ColumnName.cs index 7c07c0769..5e1eea0f9 100644 --- a/Client.Linq/Internal/Expressions/ColumnName.cs +++ b/Client.Linq/Internal/Expressions/ColumnName.cs @@ -18,6 +18,9 @@ public void AppendFlux(StringBuilder builder) { switch (_memberResolver.ResolveMemberType(_member)) { + case MemberType.Measurement: + builder.Append("_measurement"); + break; case MemberType.Timestamp: builder.Append("_time"); break; diff --git a/Client.Linq/Internal/Expressions/MeasurementColumnName.cs b/Client.Linq/Internal/Expressions/MeasurementColumnName.cs new file mode 100644 index 000000000..d53742110 --- /dev/null +++ b/Client.Linq/Internal/Expressions/MeasurementColumnName.cs @@ -0,0 +1,22 @@ +using System.Reflection; +using System.Text; + +namespace InfluxDB.Client.Linq.Internal.Expressions +{ + internal class MeasurementColumnName : IExpressionPart + { + private readonly ColumnName _delegate; + + internal MeasurementColumnName(MemberInfo member, IMemberNameResolver memberNameResolver) + { + _delegate = new ColumnName(member, memberNameResolver); + } + + public void AppendFlux(StringBuilder builder) + { + builder.Append("r[\""); + _delegate.AppendFlux(builder); + builder.Append("\"]"); + } + } +} \ No newline at end of file diff --git a/Client.Linq/Internal/QueryExpressionTreeVisitor.cs b/Client.Linq/Internal/QueryExpressionTreeVisitor.cs index 94232d37b..72c2c90d8 100644 --- a/Client.Linq/Internal/QueryExpressionTreeVisitor.cs +++ b/Client.Linq/Internal/QueryExpressionTreeVisitor.cs @@ -102,6 +102,9 @@ protected override Expression VisitMember(MemberExpression expression) { switch (_context.MemberResolver.ResolveMemberType(expression.Member)) { + case MemberType.Measurement: + _expressionParts.Add(new MeasurementColumnName(expression.Member, _context.MemberResolver)); + break; case MemberType.Timestamp: _expressionParts.Add(new TimeColumnName(expression.Member, _context.MemberResolver)); break; diff --git a/Client.Linq/Internal/QueryVisitor.cs b/Client.Linq/Internal/QueryVisitor.cs index 6b6421210..b6b1fd2d5 100644 --- a/Client.Linq/Internal/QueryVisitor.cs +++ b/Client.Linq/Internal/QueryVisitor.cs @@ -75,7 +75,7 @@ public override void VisitWhereClause(WhereClause whereClause, QueryModel queryM var tagFilter = new List(); var fieldFilter = new List(); - // Map LINQ filter expresion to right place: range, tag filtering, field filtering + // Map LINQ filter expression to right place: range, tag filtering, field filtering foreach (var expression in expressions) { switch (expression) @@ -86,6 +86,7 @@ public override void VisitWhereClause(WhereClause whereClause, QueryModel queryM break; // Tag case TagColumnName _: + case MeasurementColumnName _: tagFilter.Add(expression); break; // Field @@ -187,7 +188,7 @@ private string ConcatExpression(IEnumerable expressions) }).ToString(); } - private void AddFilterByRange(List rangeFilter) + private void AddFilterByRange(List rangeFilter) { var rangeBinaryIndexes = Enumerable.Range(0, rangeFilter.Count) .Where(i => rangeFilter[i] is BinaryOperator) diff --git a/Client.Test/ItTasksApiTest.cs b/Client.Test/ItTasksApiTest.cs index 682775da2..7f5b9a0e0 100644 --- a/Client.Test/ItTasksApiTest.cs +++ b/Client.Test/ItTasksApiTest.cs @@ -527,9 +527,9 @@ public async Task Runs() Assert.IsNotEmpty(run.Id); Assert.AreEqual(task.Id, run.TaskID); Assert.AreEqual(Run.StatusEnum.Success, run.Status); - Assert.Greater(DateTime.Now, run.StartedAt); - Assert.Greater(DateTime.Now, run.FinishedAt); - Assert.Greater(DateTime.Now, run.ScheduledFor); + Assert.Greater(DateTime.UtcNow, run.StartedAt); + Assert.Greater(DateTime.UtcNow, run.FinishedAt); + Assert.Greater(DateTime.UtcNow, run.ScheduledFor); Assert.IsNull(run.RequestedAt); task = await _tasksApi.FindTaskByIdAsync(task.Id); diff --git a/Client.Test/MeasurementMapperTest.cs b/Client.Test/MeasurementMapperTest.cs index f62d4e185..93fd0d2b6 100644 --- a/Client.Test/MeasurementMapperTest.cs +++ b/Client.Test/MeasurementMapperTest.cs @@ -91,7 +91,36 @@ public void HeavyLoad() Assert.LessOrEqual(ts.Seconds, 10, $"Elapsed time: {elapsedTime}"); } - + + [Test] + public void MeasurementProperty() + { + var poco = new MeasurementPropertyPoco + { + Measurement = "poco", + Tag = "tag val", + Value = 15.444, + ValueWithoutDefaultName = 20, + ValueWithEmptyName = 25d, + Timestamp = TimeSpan.FromDays(10) + }; + + var lineProtocol = _mapper.ToPoint(poco, WritePrecision.S).ToLineProtocol(); + + Assert.AreEqual("poco,tag=tag\\ val value=15.444,ValueWithEmptyName=25,ValueWithoutDefaultName=20i 864000", lineProtocol); + } + + [Test] + public void MeasurementPropertyValidation() + { + var poco = new BadMeasurementAttributesPoco + { + Measurement = "poco" + }; + + Assert.Throws(() => _mapper.ToPoint(poco, WritePrecision.S)); + } + private class MyClass { public override string ToString() @@ -118,5 +147,33 @@ private class Poco [Column(IsTimestamp = true)] public Object Timestamp { get; set; } } + + private class MeasurementPropertyPoco + { + [Column(IsMeasurement = true)] + public string Measurement { get; set; } + + [Column("tag", IsTag = true)] + public string Tag { get; set; } + + [Column("value")] + public Object Value { get; set; } + + [Column] + public int? ValueWithoutDefaultName { get; set; } + + [Column("")] + public Double? ValueWithEmptyName { get; set; } + + [Column(IsTimestamp = true)] + public Object Timestamp { get; set; } + } + + [Measurement("poco")] + private class BadMeasurementAttributesPoco + { + [Column(IsMeasurement = true)] + public string Measurement { get; set; } + } } } \ No newline at end of file diff --git a/Client.Test/QueryApiTest.cs b/Client.Test/QueryApiTest.cs index dcdfef08c..f097c577e 100644 --- a/Client.Test/QueryApiTest.cs +++ b/Client.Test/QueryApiTest.cs @@ -79,6 +79,7 @@ public async Task GenericAndTypeofCalls() Assert.AreEqual(13.00, measurements[1].Value); Assert.IsAssignableFrom(measurementsTypeof[0]); var cast = measurementsTypeof.Cast().ToList(); + Assert.AreEqual(measurements[0].Measurement, cast[0].Measurement); Assert.AreEqual(measurements[0].Timestamp, cast[0].Timestamp); Assert.AreEqual(12.25, cast[0].Value); Assert.AreEqual(13.00, cast[1].Value); @@ -106,6 +107,8 @@ public async Task QueryAsyncEnumerable() private class SyncPoco { + [Column(IsMeasurement = true)] public string Measurement { get; set; } + [Column("id", IsTag = true)] public string Tag { get; set; } [Column("_value")] public double Value { get; set; } diff --git a/Client/Internal/MeasurementMapper.cs b/Client/Internal/MeasurementMapper.cs index 3cf70ddb1..91e594ae3 100644 --- a/Client/Internal/MeasurementMapper.cs +++ b/Client/Internal/MeasurementMapper.cs @@ -35,18 +35,30 @@ internal PointData ToPoint(TM measurement, WritePrecision precision) var measurementType = measurement.GetType(); CacheMeasurementClass(measurementType); - + var measurementAttribute = (Measurement) measurementType.GetCustomAttribute(typeof(Measurement)); - if (measurementAttribute == null) + var measurementColumn = CACHE[measurementType.Name].SingleOrDefault(p => p.Column.IsMeasurement); + + if (((measurementAttribute == null) ^ (measurementColumn == null)) == false) { throw new InvalidOperationException( - $"Measurement {measurement} does not have a {typeof(Measurement)} attribute."); + $"Unable to determine Measurement for {measurement}. Does it have a {typeof(Measurement)} or IsMessage {typeof(Column)} attribute?"); } - var point = PointData.Measurement(measurementAttribute.Name); + string measurementName = + measurementAttribute == null + ? (string)measurementColumn.Property.GetValue(measurement) + : measurementAttribute.Name; + + var point = PointData.Measurement(measurementName); foreach (var propertyInfo in CACHE[measurementType.Name]) { + if (propertyInfo.Column.IsMeasurement) + { + continue; + } + var value = propertyInfo.Property.GetValue(measurement); if (value == null) { diff --git a/Scripts/ci-test.sh b/Scripts/ci-test.sh index 763de7226..9fd7b9caa 100755 --- a/Scripts/ci-test.sh +++ b/Scripts/ci-test.sh @@ -45,7 +45,7 @@ fi # # Install testing tools # -dotnet tool install --tool-path="./trx2junit/" trx2junit --version ${TRX2JUNIT_VERSION} +dotnet tool update --tool-path="./trx2junit/" trx2junit --version ${TRX2JUNIT_VERSION} # # Build