Bitvantage.HyperMetrics
0.9.0-rc1
Prefix Reserved
dotnet add package Bitvantage.HyperMetrics --version 0.9.0-rc1
NuGet\Install-Package Bitvantage.HyperMetrics -Version 0.9.0-rc1
<PackageReference Include="Bitvantage.HyperMetrics" Version="0.9.0-rc1" />
<PackageVersion Include="Bitvantage.HyperMetrics" Version="0.9.0-rc1" />
<PackageReference Include="Bitvantage.HyperMetrics" />
paket add Bitvantage.HyperMetrics --version 0.9.0-rc1
#r "nuget: Bitvantage.HyperMetrics, 0.9.0-rc1"
#:package Bitvantage.HyperMetrics@0.9.0-rc1
#addin nuget:?package=Bitvantage.HyperMetrics&version=0.9.0-rc1&prerelease
#tool nuget:?package=Bitvantage.HyperMetrics&version=0.9.0-rc1&prerelease
Bitvantage.HyperMetrics
A high-performance .NET 8.0 TimescaleDB client library that transforms your C# classes into optimized time-series database schemas with minimal configuration and maximum performance.
What It Does
Bitvantage.HyperMetrics solves the complexity of efficiently storing and managing time-series data in PostgreSQL/TimescaleDB by providing:
๐๏ธ Code-First Schema Generation Define your data models using simple C# classes with attributes, and the library automatically generates optimized database schemas, tables, indexes, and hypertables.
โก Ultra-High Performance Ingestion Achieve 100,000+ records/second throughput using binary COPY protocol with temporary tables, and compiled expression trees.
๐ง Smart Data Organization Choose between denormalized (fast writes) or normalized (storage efficient) models based on your data patterns.
Key Features
- ๐ High Performance: Binary COPY protocol for optimal throughput
- ๐๏ธ Automatic Schema Generation: Creates database tables, indices, and constraints from .NET attributes
- ๐ Dual Data Models: Support for both denormalized (single table) and normalized (lookup tables) approaches
- ๐ก๏ธ Type Safety: Compile-time verification with runtime validation
- ๐ Grafana Compatible: Grafana friendly table structure
- ๐ง Smart Duplicate Handling: Configurable overwrite or ignore behavior for duplicate measurements
- ๐งต Thread-Safe: Concurrent insertions supported in some cases
Quick Start****
Installation
dotnet add package Bitvantage.HyperMetrics
Basic Usage
using Bitvantage.HyperMetrics;
using Bitvantage.HyperMetrics.Attributes;
// Define your measurement model
[Measurement(Table = "sensor_data")]
public class SensorReading
{
[Timestamp]
public DateTimeOffset Time { get; set; }
[Label]
public string DeviceName { get; set; }
[Metric]
public double Temperature { get; set; }
[Metric]
public double? Humidity { get; set; }
}
// Open a time series connection
await using var timeSeries = await TimeSeries<SensorReading>.OpenAsync(connectionString);
// Insert measurements
var readings = new[]
{
new SensorReading
{
Time = DateTimeOffset.Now,
DeviceName = "sensor-1",
Temperature = 23.5,
Humidity = 65.2
},
new SensorReading
{
Time = DateTimeOffset.Now,
DeviceName = "sensor-2",
Temperature = 24.1,
Humidity = null // Metrics can be nullable
}
};
await timeSeries.InsertAsync(readings);
Core Concepts
Attributes System
Bitvantage.HyperMetrics uses attributes to define how .NET types map to time-series database tables:
[Measurement] - Class Configuration
Applied to classes to configure database mapping:
[Measurement(
Table = "server_metrics", // Table name (defaults to snake_case class name)
Schema = "monitoring", // Schema name (defaults to user's default schema)
OptIn = false // Include all public members unless marked [Ignore]
)]
[Timestamp] - Time Column
Marks the timestamp field (exactly one per model):
[Timestamp] // Defaults to 'time' column for Grafana compatibility
public DateTimeOffset Time { get; set; }
[Timestamp(Column = "measurement_time")] // or use a custom column name
public DateTimeOffset MeasurementTime { get; set; }
[Label] - Dimensional Data
Defines categorical data for filtering and grouping:
[Label] // Stored in main table (denormalized)
public string ServerName { get; set; }
[Label(LookupTable = "servers")] // Stored in separate lookup table (normalized)
public string ServerHostname { get; set; }
[Metric] - Measurement Values
Defines measurable numeric values:
[Metric]
public double CpuPercent { get; set; }
[Metric(Column = "memory_bytes")]
public long MemoryUsageBytes { get; set; }
[Ignore] - Exclusion
Excludes fields from database persistence:
[Ignore]
public string InternalNote { get; set; }
Data Models
Denormalized Model (Single Table)
Best for high-performance scenarios with simple schemas:
[Measurement(Table = "simple_metrics")]
public class DenormalizedMetrics
{
[Timestamp]
public DateTimeOffset Time { get; set; }
[Label] // Stored directly in main table
public string DeviceName { get; set; }
[Label] // Stored directly in main table
public string Location { get; set; }
[Metric]
public double Temperature { get; set; }
}
Normalized Model (Lookup Tables)
Best for storage efficiency with low-cardinality labels:
[Measurement(Table = "normalized_metrics")]
public class NormalizedMetrics
{
[Timestamp]
public DateTimeOffset Time { get; set; }
[Label(LookupTable = "devices")] // Separate lookup table
public string DeviceName { get; set; }
[Label(LookupTable = "devices")] // Same lookup table as DeviceName
public string DeviceType { get; set; }
[Label(LookupTable = "locations")] // Different lookup table
public string Location { get; set; }
[Metric]
public double Temperature { get; set; }
}
Configuration Options
using Bitvantage.HyperMetrics.Types;
var config = new TimeSeriesConfig(
Mode: TimeSeriesMode.OpenOrCreate, // Schema handling mode
DuplicateHandling: DuplicateHandlingBehavior.Overwrite, // When the measurement contains an idenitical timestamp and set of labels, overwrite the values
CreateView: true // Create convenience view for normalized data
);
await using var timeSeries = await TimeSeries<SensorReading>.OpenAsync(connectionString, config);
TimeSeriesMode Options
OpenOrCreate(Default): Create schema if missing, validate if existsOpen: Schema must exist and be correctUse: Assume schema exists, skip validation (highest performance)
Duplicate Handling
Overwrite(Default): Replace existing measurements with same timestamp + labelsIgnore: Skip duplicate measurements silently
Advanced Features
Schema Generation
Generate database schema as SQL for review or deployment:
await using var timeSeries = await TimeSeries<SensorReading>.OpenAsync(connectionString);
string schemaSql = timeSeries.GenerateSchema();
Console.WriteLine(schemaSql);
Output example:
CREATE TABLE sensor_data (
time TIMESTAMPTZ NOT NULL,
device_id TEXT NOT NULL,
temperature DOUBLE PRECISION NOT NULL,
humidity DOUBLE PRECISION,
PRIMARY KEY (time, device_id)
);
SELECT create_hypertable('sensor_data', 'time');
Runtime Model Transformation
Modify the model at runtime for dynamic configurations:
var config = TimeSeriesConfig.WithModelTransform(model =>
{
model.Schema = $"tenant_{tenantId}";
model.Table = $"{model.Table}_{DateTime.UtcNow:yyyyMM}";
model["DeviceId"].Column = "device_identifier";
});
Thread Safety and Concurrency
Thread Safety Guarantees
TimeSeries<T> instances are thread-safe when created using:
- Connection string constructor:
TimeSeries<T>.OpenAsync(connectionString, ...) NpgsqlDataSourceconstructor:TimeSeries<T>.OpenAsync(dataSource, ...)
Multiple threads can safely call InsertAsync() concurrently on the same TimeSeries<T> instance.
Note:
TimeSeries<T>instances created with an existingNpgsqlConnectionare not thread-safe and should only be used from a single thread.
Database Concurrency Considerations
While the client library is thread-safe, concurrent insertions can encounter database-level contention depending on your configuration.
Denormalized Data with Duplicate Handling Set to Ignore
When using denormalized data models with DuplicateHandling.Ignore, the library leverages PostgreSQL's ON CONFLICT DO NOTHING clause which does not acquire row locks. This means concurrent threads can insert duplicate records without creating duplicate records blocking each other, achieving near-linear scaling with multiple threads. This configuration is ideal for high-throughput scenarios where duplicates are either acceptable or rare, as there is no database contention even under heavy concurrent load.
Denormalized Data with Duplicate Handling Set to Overwrite
When using DuplicateHandling.Overwrite, concurrent threads inserting duplicate records with the same timestamp and labels will cause temporary database contention as PostgreSQL must serialize conflicting row updates. To mitigate this, you can route duplicate records to the same thread using timestamp-based hashing or partitioning, ensure your data pipeline eliminates duplicates before insertion, or decrease batch sizes to reduce the duration that locks are held.
Normalized Data with Lookup Tables
Normalized data models introduce additional complexity due to concurrent lookup table insertions. When multiple threads insert new lookup values simultaneously, PostgreSQL may encounter deadlocks while serializing unique constraint checks and resolving foreign key references. To address this, you can pre-insert lookup values by populating lookup tables with known label values before beginning high-concurrency insertion, implement retry logic with deadlock detection and exponential backoff, or serialize new lookup insertions by using a dedicated thread or queue for records containing previously unseen label values.
Supported Data Types
Timestamp Types
DateTimeโtimestamp without time zoneDateTimeOffsetโtimestamp with time zone(recommended)
Label Types (Always Non-Nullable)
stringโtextGuidโuuidIPAddressโinetint,long,shortโinteger,bigint,smallintfloat,double,decimalโreal,double precision,numericboolโboolean
Metric Types (Nullable Supported)
int,int?โintegerlong,long?โbigintshort,short?โsmallintfloat,float?โrealdouble,double?โdouble precisiondecimal,decimal?โnumericbool,bool?โbooleanstring?โtext(nullable reference type for metrics only)
Examples
IoT Sensor Monitoring
[Measurement(Table = "iot_sensors", Schema = "monitoring")]
public class IoTSensorReading
{
[Timestamp]
public DateTimeOffset Timestamp { get; set; }
[Label]
public string DeviceId { get; set; }
[Label]
public string SensorType { get; set; }
[Label]
public string Location { get; set; }
[Metric]
public double Value { get; set; }
[Metric]
public string? Unit { get; set; }
[Metric]
public double? Quality { get; set; }
}
Application Performance Monitoring
[Measurement(Table = "request_metrics")]
public class RequestMetric
{
[Timestamp]
public DateTimeOffset Time { get; set; }
[Label(LookupTable = "services")]
public string ServiceName { get; set; }
[Label(LookupTable = "services")]
public string Environment { get; set; }
[Label]
public string Endpoint { get; set; }
[Label]
public int StatusCode { get; set; }
[Metric]
public long ResponseTimeMs { get; set; }
[Metric]
public int RequestSizeBytes { get; set; }
[Metric]
public int? ResponseSizeBytes { get; set; }
}
Background Service Integration
public class MetricsIngestionService : BackgroundService
{
private readonly TimeSeries<RequestMetric> _timeSeries;
public MetricsIngestionService(string connectionString)
{
var config = new TimeSeriesConfig(
DuplicateHandling: DuplicateHandlingBehavior.Ignore
);
_timeSeries = await TimeSeries<RequestMetric>.OpenAsync(connectionString, config);
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
await foreach (var batch in GetMetricBatches(stoppingToken))
{
await _timeSeries.InsertAsync(batch, stoppingToken);
}
}
public override async ValueTask DisposeAsync()
{
await _timeSeries.DisposeAsync();
await base.DisposeAsync();
}
}
Best Practices
Model Design
- Use
DateTimeOffsetfor timestamps to handle time zones properly - Keep labels non-nullable for data integrity
- Use nullable metrics (
double?) only when genuinely optional - Choose denormalized models for high-cardinality data
- Choose normalized models for low-cardinality data with high repetition
Schema Management
- Use
TimeSeriesMode.OpenOrCreatefor development - Use
TimeSeriesMode.Openfor production environments - Generate and review schemas before deployment
- Plan for data retention and partitioning strategies
Database Integration
PostgreSQL/TimescaleDB Setup
The library automatically creates:
- Main tables with composite primary keys
- Lookup tables for normalized data (when specified)
- TimescaleDB hypertables with time-based partitioning
- Appropriate indexes and constraints
Grafana Integration
Default 'time' column naming ensures automatic Grafana compatibility:
- Time-series data source detection
- Time-based visualizations
- Optimized time-range filtering
Requirements
- .NET 8.0 or later
- PostgreSQL 12+ with TimescaleDB extension (recommended)
- Npgsql 8.0+
License
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
Support
For issues, questions, or feature requests, please visit our GitHub issues page.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. 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. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Npgsql (>= 8.0.5)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.9.0-rc1 | 155 | 10/11/2025 |