diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a7570af3..79cda63dc 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]
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/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..93d6fd935 100644
--- a/Client.Linq/Internal/QueryVisitor.cs
+++ b/Client.Linq/Internal/QueryVisitor.cs
@@ -86,6 +86,7 @@ public override void VisitWhereClause(WhereClause whereClause, QueryModel queryM
break;
// Tag
case TagColumnName _:
+ case MeasurementColumnName _:
tagFilter.Add(expression);
break;
// Field
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)
{