210 lines
10 KiB
C#
210 lines
10 KiB
C#
|
using Microsoft.Extensions.Configuration;
|
|||
|
using MongoDB.Bson;
|
|||
|
using MongoDB.Bson.Serialization;
|
|||
|
using MongoDB.Bson.Serialization.Serializers;
|
|||
|
using MongoDB.Driver;
|
|||
|
using ZeroFramework.DeviceCenter.Domain.Aggregates.MeasurementAggregate;
|
|||
|
using ZeroFramework.DeviceCenter.Domain.Aggregates.ProductAggregate;
|
|||
|
using ZeroFramework.DeviceCenter.Domain.Constants;
|
|||
|
|
|||
|
namespace ZeroFramework.DeviceCenter.Infrastructure.Repositories
|
|||
|
{
|
|||
|
public class MeasurementRepository(IConfiguration configuration) : IMeasurementRepository
|
|||
|
{
|
|||
|
private readonly IMongoClient _mongoClient = new MongoClient(configuration.GetConnectionString("MongoConnectionString"));
|
|||
|
|
|||
|
static MeasurementRepository()
|
|||
|
{
|
|||
|
BsonSerializer.RegisterSerializer(new DateTimeSerializer(DateTimeKind.Local, BsonType.DateTime));
|
|||
|
|
|||
|
BsonClassMap.RegisterClassMap<MeasurementBucket>(classMapInitializer =>
|
|||
|
{
|
|||
|
classMapInitializer.AutoMap();
|
|||
|
classMapInitializer.IdMemberMap.SetSerializer(new StringSerializer(BsonType.ObjectId));
|
|||
|
classMapInitializer.MapMember(e => e.FeatureType).SetSerializer(new EnumSerializer<FeatureType>(BsonType.String));
|
|||
|
classMapInitializer.MapMember(e => e.Measurements);
|
|||
|
classMapInitializer.MapExtraElementsMember(e => e.Metadata);
|
|||
|
});
|
|||
|
|
|||
|
BsonClassMap.RegisterClassMap<Measurement>(classMapInitializer =>
|
|||
|
{
|
|||
|
classMapInitializer.AutoMap();
|
|||
|
classMapInitializer.MapCreator(e => new Measurement(e.Timestamp));
|
|||
|
classMapInitializer.MapExtraElementsMember(e => e.Fields);
|
|||
|
});
|
|||
|
|
|||
|
BsonClassMap.RegisterClassMap<DeviceTelemetry>(classMapInitializer =>
|
|||
|
{
|
|||
|
classMapInitializer.MapIdMember(e => e.DeviceId);
|
|||
|
classMapInitializer.AutoMap();
|
|||
|
classMapInitializer.SetIgnoreExtraElements(true);
|
|||
|
});
|
|||
|
|
|||
|
BsonClassMap.RegisterClassMap<TelemetryAggregate>(classMapInitializer =>
|
|||
|
{
|
|||
|
classMapInitializer.AutoMap();
|
|||
|
classMapInitializer.MapIdMember(e => e.Time);
|
|||
|
classMapInitializer.SetIgnoreExtraElements(true);
|
|||
|
});
|
|||
|
}
|
|||
|
|
|||
|
protected virtual async Task<IMongoDatabase> GetProductDatabase(int productId) => await Task.FromResult(_mongoClient.GetDatabase($"product-{productId}"));
|
|||
|
|
|||
|
protected virtual async Task<IMongoCollection<MeasurementBucket>> GetDeviceCollection(int productId, long deviceId)
|
|||
|
{
|
|||
|
IMongoDatabase database = await GetProductDatabase(productId);
|
|||
|
|
|||
|
IMongoCollection<MeasurementBucket> collection = database.GetCollection<MeasurementBucket>($"device-{deviceId}");
|
|||
|
|
|||
|
var indexKeys = Builders<MeasurementBucket>.IndexKeys.Ascending(e => e.FeatureType).Ascending(e => e.Identifier).Ascending(e => e.StartTime).Ascending(e => e.EndTime);
|
|||
|
|
|||
|
var indexModel = new CreateIndexModel<MeasurementBucket>(indexKeys);
|
|||
|
|
|||
|
await collection.Indexes.CreateOneAsync(indexModel);
|
|||
|
|
|||
|
return collection;
|
|||
|
}
|
|||
|
|
|||
|
public virtual async Task AddMeasurementsAsync(int productId, long deviceId, FeatureType featureType, string identifier, params Measurement[] measurements)
|
|||
|
{
|
|||
|
IMongoCollection<MeasurementBucket> collection = await GetDeviceCollection(productId, deviceId);
|
|||
|
|
|||
|
foreach (var bucketingGroup in measurements.GroupBy(e => e.Timestamp.Date.AddHours(e.Timestamp.Hour)))
|
|||
|
{
|
|||
|
DateTime bucketStartTime = bucketingGroup.Key;
|
|||
|
DateTime bucketEndTime = bucketStartTime.AddHours(1);
|
|||
|
|
|||
|
var filter = Builders<MeasurementBucket>.Filter.Where(e => e.FeatureType == featureType && e.Identifier == identifier && e.StartTime == bucketStartTime);
|
|||
|
|
|||
|
var update = Builders<MeasurementBucket>.Update.SetOnInsert(e => e.FeatureType, featureType).SetOnInsert(e => e.Identifier, identifier)
|
|||
|
.SetOnInsert(e => e.StartTime, bucketStartTime).SetOnInsert(e => e.EndTime, bucketEndTime)
|
|||
|
.PushEach(e => e.Measurements, measurements).Set(e => e.LastUpdated, DateTime.Now);
|
|||
|
|
|||
|
List<double> values = [];
|
|||
|
|
|||
|
if (featureType == FeatureType.Property)
|
|||
|
{
|
|||
|
bucketingGroup.ToList().ForEach(item =>
|
|||
|
{
|
|||
|
if (item.Fields.TryGetValue("Value", out object? value) && value is not null && (int)Type.GetTypeCode(value.GetType()) is > 4 and < 16)
|
|||
|
{
|
|||
|
values.Add(Convert.ToDouble(value));
|
|||
|
}
|
|||
|
});
|
|||
|
|
|||
|
if (values.Any())
|
|||
|
{
|
|||
|
update = update.Inc(e => e.Sum, values.Sum()).Min(e => e.Min, values.Min()).Max(e => e.Max, values.Max());
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
update = update.Inc(e => e.Count, bucketingGroup.Count());
|
|||
|
|
|||
|
await collection.UpdateOneAsync(filter, update, new UpdateOptions { IsUpsert = true });
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
public virtual async Task<PageableListResult<Measurement>?> GetMeasurementsAsync(int productId, long deviceId, FeatureType? featureType, string? identifier, DateTime startTime, DateTime endTime, bool hoursFirst = false, bool descending = false, int offset = 0, int count = PagingConstants.DefaultPageSize)
|
|||
|
{
|
|||
|
IMongoCollection<MeasurementBucket> collection = await GetDeviceCollection(productId, deviceId);
|
|||
|
|
|||
|
DateTime bucketStartTime = startTime.Date.AddHours(startTime.Hour);
|
|||
|
DateTime bucketEndTime = endTime.Date.AddHours(endTime.Hour + 1);
|
|||
|
|
|||
|
var bucketQuery = collection.AsQueryable().Where(e => e.StartTime >= bucketStartTime && e.EndTime < bucketEndTime);
|
|||
|
|
|||
|
if (featureType is not null)
|
|||
|
{
|
|||
|
bucketQuery = bucketQuery.Where(e => e.FeatureType == featureType);
|
|||
|
}
|
|||
|
|
|||
|
if (identifier is not null)
|
|||
|
{
|
|||
|
bucketQuery = bucketQuery.Where(e => e.Identifier == identifier);
|
|||
|
}
|
|||
|
|
|||
|
IQueryable<Measurement> measurementQuery = hoursFirst ? bucketQuery.Select(e => e.Measurements.First()) : bucketQuery.SelectMany(e => e.Measurements);
|
|||
|
|
|||
|
measurementQuery = measurementQuery.Where(e => e.Timestamp >= startTime && e.Timestamp <= endTime);
|
|||
|
|
|||
|
var orderedQueryable = descending ? measurementQuery.OrderByDescending(e => e.Timestamp) : measurementQuery.OrderBy(e => e.Timestamp);
|
|||
|
|
|||
|
int tryTakeCount = count + 1;
|
|||
|
|
|||
|
var list = orderedQueryable.Skip(offset).Take(tryTakeCount).ToList();
|
|||
|
|
|||
|
var measurements = list.Take(count).ToList();
|
|||
|
|
|||
|
return measurements.Any() ? new PageableListResult<Measurement>(measurements, list.Count > count ? offset + count : null) : null;
|
|||
|
}
|
|||
|
|
|||
|
public virtual async Task<PageableListResult<TelemetryAggregate>?> GetTelemetryAggregatesAsync(int productId, long deviceId, string identifier, DateTime startTime, DateTime endTime, string timeInterval, int offset, int count)
|
|||
|
{
|
|||
|
IMongoCollection<MeasurementBucket> collection = await GetDeviceCollection(productId, deviceId);
|
|||
|
|
|||
|
DateTime bucketStartTime = startTime.Date.AddHours(startTime.Hour);
|
|||
|
DateTime bucketEndTime = endTime.Date.AddHours(endTime.Hour + 1);
|
|||
|
|
|||
|
FilterDefinitionBuilder<MeasurementBucket> filterBuilder = Builders<MeasurementBucket>.Filter;
|
|||
|
|
|||
|
var filter = filterBuilder.Eq(e => e.FeatureType, FeatureType.Property) & filterBuilder.Eq(e => e.Identifier, identifier) & filterBuilder.Gte(e => e.StartTime, bucketStartTime) & filterBuilder.Lte(e => e.EndTime, bucketEndTime);
|
|||
|
|
|||
|
Dictionary<string, string> reportTypeToFormat = new(StringComparer.OrdinalIgnoreCase)
|
|||
|
{
|
|||
|
{ "Year", "%Y" },
|
|||
|
{ "Month", "%Y-%m" },
|
|||
|
{ "Day", "%Y-%m-%d" },
|
|||
|
{ "Hour", "%Y-%m-%d %H:00" }
|
|||
|
};
|
|||
|
|
|||
|
string format = reportTypeToFormat[timeInterval];
|
|||
|
|
|||
|
int tryTakeCount = count + 1;
|
|||
|
|
|||
|
var list = await collection.Aggregate().Match(filter).Group(e => e.StartTime.ToString(format), g => new TelemetryAggregate
|
|||
|
{
|
|||
|
Time = g.Key,
|
|||
|
Min = g.Min(e => e.Min),
|
|||
|
Max = g.Max(e => e.Max),
|
|||
|
Average = g.Average(e => e.Sum / e.Count),
|
|||
|
Count = g.Sum(e => e.Count)
|
|||
|
}).SortBy(e => e.Time).Skip(offset).Limit(tryTakeCount).ToListAsync();
|
|||
|
|
|||
|
return list.Any() ? new PageableListResult<TelemetryAggregate>(list.Take(count).ToList(), list.Count > count ? offset + count : null) : null;
|
|||
|
}
|
|||
|
|
|||
|
public virtual async Task SetTelemetryValueAsync(int productId, long deviceId, params TelemetryValue[] telemetryValues)
|
|||
|
{
|
|||
|
IMongoDatabase database = await GetProductDatabase(productId);
|
|||
|
|
|||
|
IMongoCollection<DeviceTelemetry> collection = database.GetCollection<DeviceTelemetry>("telemetry");
|
|||
|
|
|||
|
var filter = Builders<DeviceTelemetry>.Filter.Where(e => e.DeviceId == deviceId);
|
|||
|
|
|||
|
var identifiers = telemetryValues.Select(e => new StringOrRegularExpression(e.Identifier)).ToArray();
|
|||
|
|
|||
|
var subFilter = Builders<TelemetryValue>.Filter.StringIn(e => e.Identifier, identifiers);
|
|||
|
|
|||
|
List<WriteModel<DeviceTelemetry>> bulkUpdates =
|
|||
|
[
|
|||
|
new UpdateOneModel<DeviceTelemetry>(filter, Builders<DeviceTelemetry>.Update.PullFilter(e => e.Values, subFilter)),
|
|||
|
new UpdateOneModel<DeviceTelemetry>(filter, Builders<DeviceTelemetry>.Update.SetOnInsert(e => e.DeviceId, deviceId).AddToSetEach(e => e.Values, telemetryValues)) { IsUpsert = true },
|
|||
|
];
|
|||
|
|
|||
|
await collection.BulkWriteAsync(bulkUpdates);
|
|||
|
}
|
|||
|
|
|||
|
public virtual async Task<IEnumerable<TelemetryValue>?> GetTelemetryValuesAsync(int productId, long deviceId)
|
|||
|
{
|
|||
|
IMongoDatabase database = await GetProductDatabase(productId);
|
|||
|
|
|||
|
IMongoCollection<DeviceTelemetry> collection = database.GetCollection<DeviceTelemetry>("telemetry");
|
|||
|
|
|||
|
var filter = Builders<DeviceTelemetry>.Filter.Where(e => e.DeviceId == deviceId);
|
|||
|
|
|||
|
var deviceTelemetry = await collection.Find(filter).SingleOrDefaultAsync();
|
|||
|
|
|||
|
return deviceTelemetry?.Values;
|
|||
|
}
|
|||
|
}
|
|||
|
}
|