zeroframework/Services/DeviceCenter/ZeroFramework.DeviceCenter.Infrastructure/Repositories/MeasurementRepository.cs
2023-12-05 17:22:48 +08:00

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;
}
}
}