diff --git a/InfluxData.Net.Common/Attributes/FieldAttribute.cs b/InfluxData.Net.Common/Attributes/FieldAttribute.cs
new file mode 100644
index 0000000..a74821a
--- /dev/null
+++ b/InfluxData.Net.Common/Attributes/FieldAttribute.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace InfluxData.Net.Common.Attributes
+{
+ public class FieldAttribute : InfluxBaseAttribute
+ {
+ public FieldAttribute([CallerMemberName]string name = null)
+ : base(name) { }
+ }
+}
diff --git a/InfluxData.Net.Common/Attributes/InfluxBaseAttribute.cs b/InfluxData.Net.Common/Attributes/InfluxBaseAttribute.cs
new file mode 100644
index 0000000..9741038
--- /dev/null
+++ b/InfluxData.Net.Common/Attributes/InfluxBaseAttribute.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace InfluxData.Net.Common.Attributes
+{
+ public class InfluxBaseAttribute : Attribute
+ {
+
+ public InfluxBaseAttribute() { }
+
+ public InfluxBaseAttribute(string name)
+ {
+ Name = name;
+ }
+
+ public string Name { get; set; }
+ }
+}
diff --git a/InfluxData.Net.Common/Attributes/MeasurementAttribute.cs b/InfluxData.Net.Common/Attributes/MeasurementAttribute.cs
new file mode 100644
index 0000000..86c7b6f
--- /dev/null
+++ b/InfluxData.Net.Common/Attributes/MeasurementAttribute.cs
@@ -0,0 +1,8 @@
+using System;
+
+namespace InfluxData.Net.Common.Attributes
+{
+ public class MeasurementAttribute : InfluxBaseAttribute
+ {
+ }
+}
diff --git a/InfluxData.Net.Common/Attributes/TagAttribute.cs b/InfluxData.Net.Common/Attributes/TagAttribute.cs
new file mode 100644
index 0000000..19e4ec6
--- /dev/null
+++ b/InfluxData.Net.Common/Attributes/TagAttribute.cs
@@ -0,0 +1,13 @@
+using System;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Text;
+
+namespace InfluxData.Net.Common.Attributes
+{
+ public class TagAttribute : InfluxBaseAttribute
+ {
+ public TagAttribute([CallerMemberName]string name = null)
+ : base(name) { }
+ }
+}
diff --git a/InfluxData.Net.Common/Attributes/TimestampAttribute.cs b/InfluxData.Net.Common/Attributes/TimestampAttribute.cs
new file mode 100644
index 0000000..eae1d85
--- /dev/null
+++ b/InfluxData.Net.Common/Attributes/TimestampAttribute.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace InfluxData.Net.Common.Attributes
+{
+ public class TimestampAttribute : InfluxBaseAttribute
+ {
+ public TimestampAttribute()
+ : base("time") { }
+ }
+}
diff --git a/InfluxData.Net.Common/Infrastructure/MissingExpectedAttributeException.cs b/InfluxData.Net.Common/Infrastructure/MissingExpectedAttributeException.cs
new file mode 100644
index 0000000..6af0abb
--- /dev/null
+++ b/InfluxData.Net.Common/Infrastructure/MissingExpectedAttributeException.cs
@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace InfluxData.Net.Common.Infrastructure
+{
+ public class MissingExpectedAttributeException : Exception
+ {
+ public MissingExpectedAttributeException(Type attributeType)
+ : base($"The expected attribute: {attributeType.Name} is missing")
+ {
+ }
+ }
+}
diff --git a/InfluxData.Net.InfluxDb/Helpers/PointExtensions.cs b/InfluxData.Net.InfluxDb/Helpers/PointExtensions.cs
new file mode 100644
index 0000000..83f5a0a
--- /dev/null
+++ b/InfluxData.Net.InfluxDb/Helpers/PointExtensions.cs
@@ -0,0 +1,187 @@
+using InfluxData.Net.Common.Attributes;
+using InfluxData.Net.InfluxDb.Models;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Linq;
+using InfluxData.Net.Common.Infrastructure;
+using System.Reflection;
+
+namespace InfluxData.Net.InfluxDb.Helpers
+{
+ public static class PointExtensions
+ {
+ ///
+ /// Allows for converting attribute decorated types into a
+ /// Attribute rules:
+ /// 1) Must have exactly ONE [Measurement] attribute
+ /// 2) Must not have more than ONE [Timestamp] attribute
+ /// 3) Must have at least ONE [Field] attribute
+ /// 4) [Tag] attribute is optional
+ ///
+ ///
+ ///
+ ///
+ ///
+ /// Example of valid type:
+ ///
+ ///
+ /// public class MyType
+ /// {
+ /// [Measurement]
+ /// public string MyMeasurement { get; set; }
+ ///
+ /// [Timestamp]
+ /// public DateTime Time { get; set; }
+ ///
+ /// [Tag]
+ /// public string SignalName { get; set; }
+ ///
+ /// [Field]
+ /// public double Value { get; set; }
+ /// }
+ ///
+ ///
+ ///
+ public static Point ToPoint(this TModel model)
+ {
+ var type = model.GetType();
+
+ Point point = new Point();
+
+ var properties = type.GetProperties();
+
+ point.TrySetTimestamp(model, properties);
+ point.TrySetMeasurement(model, properties);
+ point.TrySetTags(model, properties);
+ point.TrySetFields(model, properties);
+
+ return point;
+ }
+
+ private static Point TrySetTimestamp(this Point point, TModel model, PropertyInfo[] properties)
+ {
+ var timestampProperties = properties.Where(x => x.IsDefined(typeof(TimestampAttribute), false));
+
+ // Make sure only one TimestampAttribute is defined
+ if (timestampProperties.Any())
+ {
+ if (timestampProperties.Count() != 1)
+ throw new InvalidOperationException($"Cannot have multiple {typeof(TimestampAttribute).Name} attributes defined");
+
+ var timestampProperty = timestampProperties.FirstOrDefault();
+ var timestampPropertyValue = timestampProperty.GetValue(model);
+
+ if (!timestampProperty.PropertyType.Equals(typeof(DateTime)))
+ throw new InvalidOperationException($"{nameof(timestampProperty.Name)} is not of type {typeof(DateTime).Name}");
+
+ if (timestampPropertyValue == null)
+ throw new InvalidOperationException($"{nameof(timestampProperty.Name)} cannot be null");
+
+ point.Timestamp = (DateTime)timestampPropertyValue;
+ }
+
+ return point;
+ }
+
+ private static Point TrySetMeasurement(this Point point, TModel model, PropertyInfo[] properties)
+ {
+ var measurementProperties = properties.Where(x => x.IsDefined(typeof(MeasurementAttribute), false));
+
+ if(!measurementProperties.Any())
+ {
+ throw new MissingExpectedAttributeException(typeof(MeasurementAttribute));
+ }
+
+ // Make sure only one MeasurementAttribute is defined
+ if (measurementProperties.Count() != 1)
+ {
+ throw new InvalidOperationException($"Must have exactly one {typeof(MeasurementAttribute).Name} attribute defined");
+ }
+
+ var measurementProperty = measurementProperties.FirstOrDefault();
+ var measurementPropertyValue = measurementProperty.GetValue(model);
+
+ if (!measurementProperty.PropertyType.Equals(typeof(string)))
+ {
+ throw new InvalidOperationException($"{nameof(measurementProperty.Name)} is not of type {typeof(string).Name}");
+ }
+
+ if ((string.IsNullOrWhiteSpace((string)measurementPropertyValue)))
+ {
+ throw new InvalidOperationException($"{nameof(measurementProperty.Name)} cannot be null or whitespace");
+ }
+
+ point.Name = (string)measurementPropertyValue;
+
+ return point;
+ }
+
+ private static Point TrySetTags(this Point point, TModel model, PropertyInfo[] properties)
+ {
+ var tagProperties = properties.Where(x => x.IsDefined(typeof(TagAttribute), false));
+
+ if (tagProperties.Any(x => !x.PropertyType.Equals(typeof(string))))
+ {
+ throw new InvalidOperationException($"Tags can only be string values");
+ }
+
+ foreach (var tagProperty in tagProperties)
+ {
+ var tagType = tagProperty.PropertyType;
+ var tagValue = tagProperty.GetValue(model);
+
+ if (tagValue == null)
+ continue;
+
+ var converted = Convert.ChangeType(tagValue, tagType);
+
+ var propertyName = tagProperty.GetCustomAttribute().Name;
+
+ point.Tags.Add(propertyName, converted);
+ }
+
+ return point;
+ }
+
+ private static Point TrySetFields(this Point point, TModel model, PropertyInfo[] properties)
+ {
+ var fieldProperties = properties.Where(x => x.IsDefined(typeof(FieldAttribute), false));
+
+ // Make sure at least one FieldAttribute is defined
+ if (!fieldProperties.Any())
+ {
+ throw new MissingExpectedAttributeException(typeof(FieldAttribute));
+ }
+
+ if (fieldProperties.Any(x => !x.PropertyType.IsSimple()))
+ {
+ throw new InvalidOperationException($"Fields can only be primitive or string values");
+ }
+
+ foreach (var fieldProperty in fieldProperties)
+ {
+ var fieldType = fieldProperty.PropertyType;
+ var fieldValue = fieldProperty.GetValue(model);
+
+ if (fieldValue == null)
+ continue;
+
+ var converted = Convert.ChangeType(fieldValue, fieldType);
+
+ var propertyName = fieldProperty.GetCustomAttribute().Name;
+
+ point.Fields.Add(propertyName, converted);
+ }
+
+ return point;
+ }
+
+ private static bool IsSimple(this Type type)
+ {
+ return
+ type.IsPrimitive ||
+ type.Equals(typeof(String));
+ }
+ }
+}
diff --git a/InfluxData.Net.Tests/InfluxDb/Helpers/PointExtensionsTests.cs b/InfluxData.Net.Tests/InfluxDb/Helpers/PointExtensionsTests.cs
new file mode 100644
index 0000000..fc57d60
--- /dev/null
+++ b/InfluxData.Net.Tests/InfluxDb/Helpers/PointExtensionsTests.cs
@@ -0,0 +1,284 @@
+using InfluxData.Net.Common.Attributes;
+using InfluxData.Net.Common.Infrastructure;
+using InfluxData.Net.InfluxDb.Helpers;
+using InfluxData.Net.InfluxDb.Models;
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using Xunit;
+
+namespace InfluxData.Net.Tests.InfluxDb.Helpers
+{
+ [Trait("InfluxDb Helpers", "Point extensions")]
+ public class PointExtensionsTests
+ {
+ [Fact]
+ public void ToPoint_OnMissingMeasurementAttribute_ThrowsException()
+ {
+ TestModelWithoutMeasurement model = new TestModelWithoutMeasurement
+ {
+ Time = DateTime.Now,
+ TestTag = "TestTag",
+ TestField = "TestField"
+ };
+
+ Assert.Throws(() => model.ToPoint());
+ }
+
+ [Fact]
+ public void ToPoint_OnMissingFieldAttribute_ThrowsException()
+ {
+ TestModelWithoutFields model = new TestModelWithoutFields
+ {
+ Time = DateTime.Now,
+ MyMeasurement = "FakeMeasurement",
+ TestTag = "TestTag"
+ };
+
+ Assert.Throws(() => model.ToPoint());
+ }
+
+ [Fact]
+ public void ToPoint_OnMultipleMeasurementAttributes_ThrowsException()
+ {
+ TestModelWithMultipleMeasurements model = new TestModelWithMultipleMeasurements
+ {
+ Time = DateTime.Now,
+ Measurement1 = "FakeMeasurement1",
+ Measurement2 = "FakeMeasurement2",
+ TestTag = "TestTag",
+ TestField = "TestField"
+ };
+
+ Assert.Throws(() => model.ToPoint());
+ }
+
+ [Fact]
+ public void ToPoint_OnMultipleTimestampAttributes_ThrowsException()
+ {
+ TestModelWithMultipleTimestamps model = new TestModelWithMultipleTimestamps
+ {
+ Time1 = DateTime.Now,
+ Time2 = DateTime.Now,
+ Measurement = "FakeMeasurement",
+ TestTag = "TestTag",
+ TestField = "TestField"
+ };
+
+ Assert.Throws(() => model.ToPoint());
+ }
+
+ [Fact]
+ public void ToPoint_OnNonPrimitiveTagProperties_ThrowsException()
+ {
+ TestModelWithNonStringTags model = new TestModelWithNonStringTags
+ {
+ Measurement = "FakeMeasurement",
+ Time = DateTime.Now,
+ PrimitiveTag = "PrimitiveTag",
+ NonPrimitiveTag = new NonPrimitiveType { Whatever = "Whatever", Whatever2 = "Whatever" },
+ PrimitiveField = "PrimitiveField"
+ };
+
+ Assert.Throws(() => model.ToPoint());
+ }
+
+ [Fact]
+ public void ToPoint_OnNonPrimitiveFieldProperties_ThrowsException()
+ {
+ TestModelWithNonPrimitiveFields model = new TestModelWithNonPrimitiveFields
+ {
+ Measurement = "FakeMeasurement",
+ Time = DateTime.Now,
+ PrimitiveTag = "PrimitiveTag",
+ PrimitiveField = "PrimitiveField",
+ NonPrimitiveField = new NonPrimitiveType { Whatever = "Whatever", Whatever2 = "Whatever" }
+ };
+
+ Assert.Throws(() => model.ToPoint());
+ }
+
+ [Fact]
+ public void ToPoint_OnValidType_IsConverted()
+ {
+ var measurement = "FakeMeasurement";
+ var time = DateTime.Now;
+
+ var firstTagValue = "FirstTag";
+ var secondTagValue = "SecondTag";
+
+ double firstFieldValue = 23.2;
+ float secondFieldValue = 2323;
+
+ ValidTestModel model = new ValidTestModel
+ {
+ Measurement = measurement,
+ Time = time,
+ Tag1 = firstTagValue,
+ Tag2 = secondTagValue,
+ PrimitiveField1 = firstFieldValue,
+ PrimitiveField2 = secondFieldValue
+ };
+
+ var expectedPoint = new Point
+ {
+ Name = measurement,
+ Timestamp = time,
+ Tags = new Dictionary
+ {
+ { "Tag1", firstTagValue }, // Tag name is set explicitly in attribute name argument
+ { "tag2", secondTagValue }
+ },
+ Fields = new Dictionary
+ {
+ { "PrimitiveField1", firstFieldValue }, // Field name is set explicitly in attribute name argument
+ { "field2", secondFieldValue }
+ }
+ };
+
+ var actualPoint = model.ToPoint();
+
+ // Compare fields in point
+ Assert.Equal(expectedPoint.Fields.Count, actualPoint.Fields.Count);
+ Assert.Equal(expectedPoint.Fields.First().Key, actualPoint.Fields.First().Key);
+ Assert.Equal(expectedPoint.Fields.Last().Key, actualPoint.Fields.Last().Key);
+ Assert.Equal(expectedPoint.Fields.First().Value, actualPoint.Fields.First().Value);
+ Assert.Equal(expectedPoint.Fields.Last().Value, actualPoint.Fields.Last().Value);
+
+ // Compare tags in point
+ Assert.Equal(expectedPoint.Tags.Count, actualPoint.Tags.Count);
+ Assert.Equal(expectedPoint.Tags.First().Key, actualPoint.Tags.First().Key);
+ Assert.Equal(expectedPoint.Tags.Last().Key, actualPoint.Tags.Last().Key);
+ Assert.Equal(expectedPoint.Tags.First().Value, actualPoint.Tags.First().Value);
+ Assert.Equal(expectedPoint.Tags.Last().Value, actualPoint.Tags.Last().Value);
+
+ // Compare name and timestamp in point
+ Assert.Equal(expectedPoint.Name, actualPoint.Name);
+ Assert.Equal(expectedPoint.Timestamp, actualPoint.Timestamp);
+ }
+
+ private class TestModelWithoutMeasurement
+ {
+ [Timestamp]
+ public DateTime Time { get; set; }
+
+ [Tag("testtag")]
+ public string TestTag { get; set; }
+
+ [Field("testfield")]
+ public string TestField { get; set; }
+ }
+
+ private class TestModelWithoutFields
+ {
+ [Timestamp]
+ public DateTime Time { get; set; }
+
+ [Measurement]
+ public string MyMeasurement { get; set; }
+
+ [Tag]
+ public string TestTag { get; set; }
+ }
+
+ private class TestModelWithMultipleMeasurements
+ {
+ [Measurement]
+ public string Measurement1 { get; set; }
+
+ [Measurement]
+ public string Measurement2 { get; set; }
+
+ [Timestamp]
+ public DateTime Time { get; set; }
+
+ [Tag("testtag")]
+ public string TestTag { get; set; }
+
+ [Field("testfield")]
+ public string TestField { get; set; }
+ }
+
+ private class TestModelWithMultipleTimestamps
+ {
+ [Measurement]
+ public string Measurement { get; set; }
+
+ [Timestamp]
+ public DateTime Time1 { get; set; }
+
+ [Timestamp]
+ public DateTime Time2 { get; set; }
+
+ [Tag("testtag")]
+ public string TestTag { get; set; }
+
+ [Field("testfield")]
+ public string TestField { get; set; }
+ }
+
+ private class TestModelWithNonStringTags
+ {
+ [Measurement]
+ public string Measurement { get; set; }
+
+ [Timestamp]
+ public DateTime Time { get; set; }
+
+ [Tag("primitivetag")]
+ public string PrimitiveTag { get; set; }
+
+ [Tag("nonprimitivetag")]
+ public NonPrimitiveType NonPrimitiveTag { get; set; }
+
+ [Field("primitivetag")]
+ public string PrimitiveField { get; set; }
+ }
+
+ private class TestModelWithNonPrimitiveFields
+ {
+ [Measurement]
+ public string Measurement { get; set; }
+
+ [Timestamp]
+ public DateTime Time { get; set; }
+
+ [Tag("primitivetag")]
+ public string PrimitiveTag { get; set; }
+
+ [Field("primitivefield")]
+ public string PrimitiveField { get; set; }
+
+ [Field("nonprimitivefield")]
+ public NonPrimitiveType NonPrimitiveField { get; set; }
+ }
+
+ private class ValidTestModel
+ {
+ [Measurement]
+ public string Measurement { get; set; }
+
+ [Timestamp]
+ public DateTime Time { get; set; }
+
+ [Tag]
+ public string Tag1 { get; set; }
+
+ [Tag("tag2")]
+ public string Tag2 { get; set; }
+
+ [Field]
+ public double PrimitiveField1 { get; set; }
+
+ [Field("field2")]
+ public float PrimitiveField2 { get; set; }
+ }
+
+ private class NonPrimitiveType
+ {
+ public string Whatever { get; set; }
+ public string Whatever2 { get; set; }
+ }
+ }
+}