| | | 1 | | using pva.Helpers.Extensions; |
| | | 2 | | using pva.SuperV.Engine.Exceptions; |
| | | 3 | | using pva.SuperV.Engine.HistoryRetrieval; |
| | | 4 | | using pva.SuperV.Engine.HistoryStorage; |
| | | 5 | | using pva.SuperV.Engine.Processing; |
| | | 6 | | using System.Reflection; |
| | | 7 | | using System.Runtime.CompilerServices; |
| | | 8 | | using System.Text; |
| | | 9 | | using System.Text.Json.Serialization; |
| | | 10 | | |
| | | 11 | | namespace pva.SuperV.Engine |
| | | 12 | | { |
| | | 13 | | /// <summary> |
| | | 14 | | /// A runnable project. It allows to create new instances. However its definitions are fixed and can't be changed. |
| | | 15 | | /// </summary> |
| | | 16 | | /// <seealso cref="pva.SuperV.Engine.Project" /> |
| | | 17 | | public class RunnableProject : Project |
| | | 18 | | { |
| | | 19 | | /// <summary> |
| | | 20 | | /// The project assembly loader. |
| | | 21 | | /// </summary> |
| | | 22 | | [JsonIgnore] |
| | | 23 | | private ProjectAssemblyLoader? projectAssemblyLoader; |
| | | 24 | | |
| | | 25 | | [JsonIgnore] |
| | | 26 | | public WeakReference? ProjectAssemblyLoaderWeakRef |
| | | 27 | | { |
| | 13 | 28 | | get => projectAssemblyLoader == null |
| | 13 | 29 | | ? null |
| | 13 | 30 | | : new WeakReference(projectAssemblyLoader, trackResurrection: true); |
| | | 31 | | } |
| | | 32 | | |
| | | 33 | | /// <summary> |
| | | 34 | | /// Gets the instances. |
| | | 35 | | /// </summary> |
| | | 36 | | /// <value> |
| | | 37 | | /// The instances. |
| | | 38 | | /// </value> |
| | | 39 | | [JsonIgnore] |
| | 893 | 40 | | public Dictionary<string, Instance> Instances { get; } = new(StringComparer.OrdinalIgnoreCase); |
| | | 41 | | |
| | | 42 | | /// <summary> |
| | | 43 | | /// Initializes a new instance of the <see cref="RunnableProject"/> class. |
| | | 44 | | /// </summary> |
| | 13 | 45 | | public RunnableProject() |
| | 13 | 46 | | { |
| | 13 | 47 | | } |
| | | 48 | | |
| | | 49 | | /// <summary> |
| | | 50 | | /// Initializes a new instance of the <see cref="RunnableProject"/> class from a <see cref="WipProject"/>. |
| | | 51 | | /// </summary> |
| | | 52 | | /// <param name="wipProject">The wip project.</param> |
| | 126 | 53 | | public RunnableProject(WipProject wipProject) |
| | 126 | 54 | | { |
| | 126 | 55 | | Name = wipProject.Name; |
| | 126 | 56 | | Version = wipProject.Version; |
| | 126 | 57 | | Classes = new(wipProject.Classes); |
| | 126 | 58 | | FieldFormatters = new(wipProject.FieldFormatters); |
| | 126 | 59 | | HistoryStorageEngineConnectionString = wipProject.HistoryStorageEngineConnectionString; |
| | 126 | 60 | | HistoryStorageEngine = wipProject.HistoryStorageEngine; |
| | 126 | 61 | | HistoryRepositories = new(wipProject.HistoryRepositories); |
| | 126 | 62 | | CreateHistoryRepositories(HistoryStorageEngine); |
| | 125 | 63 | | CreateHistoryClassTimeSeries(); |
| | 124 | 64 | | SetupProjectAssemblyLoader(); |
| | 124 | 65 | | RecreateInstances(wipProject); |
| | 124 | 66 | | } |
| | | 67 | | |
| | | 68 | | public override string GetId() |
| | 653 | 69 | | { |
| | 653 | 70 | | return $"{Name!}"; |
| | 653 | 71 | | } |
| | | 72 | | |
| | | 73 | | private void SetupProjectAssemblyLoader() |
| | 203 | 74 | | { |
| | 406 | 75 | | Task.Run(async () => await ProjectBuilder.BuildAsync(this)).Wait(); |
| | 203 | 76 | | projectAssemblyLoader ??= new(); |
| | 203 | 77 | | projectAssemblyLoader.LoadFromAssemblyPath(GetAssemblyFileName()); |
| | 203 | 78 | | } |
| | | 79 | | |
| | | 80 | | private void CreateHistoryClassTimeSeries() |
| | 125 | 81 | | { |
| | 125 | 82 | | Classes.Values.ForEach(clazz => |
| | 341 | 83 | | { |
| | 341 | 84 | | clazz.FieldDefinitions.Values.ForEach(fieldDefinition => |
| | 2194 | 85 | | { |
| | 2194 | 86 | | fieldDefinition.ValuePostChangeProcessings |
| | 2194 | 87 | | .OfType<IHistorizationProcessing>() |
| | 2194 | 88 | | .ForEach(hp => |
| | 2414 | 89 | | hp.UpsertInHistoryStorage(Name!, clazz.Name!)); |
| | 2534 | 90 | | }); |
| | 465 | 91 | | }); |
| | 124 | 92 | | } |
| | | 93 | | |
| | | 94 | | private void CreateHistoryRepositories(IHistoryStorageEngine? historyStorageEngine) |
| | 126 | 95 | | { |
| | 126 | 96 | | if (HistoryRepositories.Keys.Count == 0) |
| | 14 | 97 | | { |
| | 14 | 98 | | return; |
| | | 99 | | } |
| | 112 | 100 | | if (historyStorageEngine is null) |
| | 1 | 101 | | { |
| | 1 | 102 | | throw new NoHistoryStorageEngineException(Name); |
| | | 103 | | } |
| | | 104 | | |
| | 111 | 105 | | HistoryRepositories.Values.ForEach(repository => |
| | 111 | 106 | | { |
| | 111 | 107 | | repository.HistoryStorageEngine = historyStorageEngine; |
| | 111 | 108 | | repository.UpsertRepository(Name!, historyStorageEngine); |
| | 222 | 109 | | }); |
| | 125 | 110 | | } |
| | | 111 | | |
| | | 112 | | /// <summary> |
| | | 113 | | /// Creates an instance. |
| | | 114 | | /// </summary> |
| | | 115 | | /// <param name="className">Name of the class.</param> |
| | | 116 | | /// <param name="instanceName">Name of the instance.</param> |
| | | 117 | | /// <param name="addToRunningInstances">Indicates if the created instances should be added to running instances |
| | | 118 | | /// <returns>The newly created instance.</returns> |
| | | 119 | | /// <exception cref="pva.SuperV.Engine.Exceptions.EntityAlreadyExistException"></exception> |
| | | 120 | | public Instance? CreateInstance(string className, string instanceName, bool addToRunningInstances = true) |
| | 79 | 121 | | { |
| | 79 | 122 | | SetupProjectAssemblyLoader(); |
| | 79 | 123 | | if (Instances.ContainsKey(instanceName)) |
| | 1 | 124 | | { |
| | 1 | 125 | | throw new EntityAlreadyExistException("Instance", instanceName); |
| | | 126 | | } |
| | | 127 | | |
| | 78 | 128 | | Class clazz = GetClass(className); |
| | 78 | 129 | | string classFullName = $"{Name}.V{Version}.{clazz.Name}"; |
| | 78 | 130 | | Type? classType = projectAssemblyLoader?.Assemblies.First()?.GetType(classFullName!); |
| | | 131 | | |
| | 78 | 132 | | Instance? instance = CreateInstance(classType!); |
| | 78 | 133 | | if (instance is null) |
| | 0 | 134 | | { |
| | 0 | 135 | | return instance; |
| | | 136 | | } |
| | 78 | 137 | | instance.Name = instanceName; |
| | 78 | 138 | | instance.Class = clazz; |
| | 78 | 139 | | Class? currentClass = clazz; |
| | 186 | 140 | | while (currentClass is not null) |
| | 108 | 141 | | { |
| | 108 | 142 | | currentClass.FieldDefinitions.ForEach((k, v) => |
| | 534 | 143 | | { |
| | 534 | 144 | | instance!.Fields.TryGetValue(k, out IField? field); |
| | 534 | 145 | | field!.FieldDefinition = v; |
| | 642 | 146 | | }); |
| | 108 | 147 | | currentClass = currentClass.BaseClass; |
| | 108 | 148 | | } |
| | 78 | 149 | | if (addToRunningInstances) |
| | 78 | 150 | | { |
| | 78 | 151 | | Instances.Add(instanceName, instance); |
| | 78 | 152 | | } |
| | 78 | 153 | | return instance; |
| | 78 | 154 | | } |
| | | 155 | | |
| | | 156 | | /// <summary> |
| | | 157 | | /// Creates an instance for targetType's <see cref="FieldDefinition{T}"/>. |
| | | 158 | | /// </summary> |
| | | 159 | | /// <param name="targetType">Type of the target.</param> |
| | | 160 | | /// <returns><see cref="IFieldDefinition"/> created instance.</returns> |
| | | 161 | | private static Instance CreateInstance(Type targetType) |
| | 78 | 162 | | { |
| | 78 | 163 | | var ctor = GetConstructor(targetType); |
| | 78 | 164 | | return (Instance)ctor.Invoke([]); |
| | 78 | 165 | | } |
| | | 166 | | |
| | | 167 | | /// <summary> |
| | | 168 | | /// Gets the constructor for targetType's <see cref="FieldDefinition{T}"/>. |
| | | 169 | | /// </summary> |
| | | 170 | | /// <param name="targetType">Type of the target.</param> |
| | | 171 | | /// <param name="argumentType">Type of the argument.</param> |
| | | 172 | | /// <returns></returns> |
| | | 173 | | /// <exception cref="InvalidOperationException">No constructor found for FieldDefinition{targetType.Name}.</exce |
| | | 174 | | private static ConstructorInfo GetConstructor(Type targetType) |
| | 78 | 175 | | { |
| | 78 | 176 | | return targetType.GetConstructor(Type.EmptyTypes) |
| | 78 | 177 | | ?? throw new InvalidOperationException($"No constructor found for {targetType.Name}."); |
| | 78 | 178 | | } |
| | | 179 | | |
| | | 180 | | /// <summary> |
| | | 181 | | /// Removes an instance by its name. |
| | | 182 | | /// </summary> |
| | | 183 | | /// <param name="instanceName">Name of the instance.</param> |
| | | 184 | | public void RemoveInstance(string instanceName) |
| | 1 | 185 | | { |
| | 1 | 186 | | Instances.Remove(instanceName); |
| | 1 | 187 | | } |
| | | 188 | | |
| | | 189 | | /// <summary> |
| | | 190 | | /// Gets an instance by its name. |
| | | 191 | | /// </summary> |
| | | 192 | | /// <param name="instanceName">Name of the instance.</param> |
| | | 193 | | /// <returns>The <see cref="Instance"/></returns> |
| | | 194 | | /// <exception cref="pva.SuperV.Engine.Exceptions.UnknownInstanceException"></exception> |
| | | 195 | | public Instance GetInstance(string instanceName) |
| | 208 | 196 | | { |
| | 208 | 197 | | if (Instances.TryGetValue(instanceName, out var instance)) |
| | 207 | 198 | | { |
| | 207 | 199 | | return instance; |
| | | 200 | | } |
| | | 201 | | |
| | 1 | 202 | | throw new UnknownEntityException("Instance", instanceName); |
| | 207 | 203 | | } |
| | | 204 | | |
| | | 205 | | /// <summary> |
| | | 206 | | /// Recreates the instances from a <see cref="WipProject"/>. |
| | | 207 | | /// </summary> |
| | | 208 | | /// <param name="wipProject">The wip project.</param> |
| | | 209 | | private void RecreateInstances(WipProject wipProject) |
| | 124 | 210 | | { |
| | 124 | 211 | | wipProject.ToLoadInstances |
| | 124 | 212 | | .ForEach((k, v) => |
| | 1 | 213 | | { |
| | 1 | 214 | | string instanceName = k; |
| | 1 | 215 | | Instance oldInstance = v; |
| | 1 | 216 | | Instance? newInstance = CreateInstance(oldInstance.Class.Name, instanceName); |
| | 1 | 217 | | Dictionary<string, IField> newFields = new(newInstance!.Fields.Count); |
| | 1 | 218 | | newInstance.Fields |
| | 1 | 219 | | .ForEach((k1, v2) => |
| | 7 | 220 | | { |
| | 7 | 221 | | string fieldName = k1; |
| | 7 | 222 | | newFields.Add(fieldName, |
| | 7 | 223 | | oldInstance.Fields.GetValueOrDefault(fieldName, v2)); |
| | 8 | 224 | | }); |
| | 1 | 225 | | newInstance.Fields = newFields; |
| | 125 | 226 | | }); |
| | 124 | 227 | | } |
| | | 228 | | |
| | | 229 | | /// <summary> |
| | | 230 | | /// Unloads the project. Clears all instances. |
| | | 231 | | /// </summary> |
| | | 232 | | [MethodImpl(MethodImplOptions.NoInlining)] |
| | | 233 | | public override void Unload() |
| | 136 | 234 | | { |
| | 136 | 235 | | Instances.Values.ForEach(instance => |
| | 212 | 236 | | instance.Dispose()); |
| | 136 | 237 | | Instances.Clear(); |
| | 136 | 238 | | HistoryStorageEngine?.Dispose(); |
| | 136 | 239 | | base.Unload(); |
| | 136 | 240 | | Projects.Remove(GetId(), out _); |
| | 136 | 241 | | projectAssemblyLoader?.Unload(); |
| | 136 | 242 | | projectAssemblyLoader = null; |
| | 136 | 243 | | } |
| | | 244 | | |
| | | 245 | | /// <summary> |
| | | 246 | | /// Gets the C# code for generating the project's assembly with <see cref="Project.BuildAsync(WipProject)"/>. |
| | | 247 | | /// </summary> |
| | | 248 | | /// <returns>C# code.</returns> |
| | | 249 | | public string GetCode() |
| | 124 | 250 | | { |
| | 124 | 251 | | StringBuilder codeBuilder = new(); |
| | 124 | 252 | | codeBuilder.AppendLine($"using {GetType().Namespace};"); |
| | 124 | 253 | | codeBuilder.AppendLine("using System.Collections.Generic;"); |
| | 124 | 254 | | codeBuilder.AppendLine("using System.Reflection;"); |
| | 124 | 255 | | codeBuilder.AppendLine($"[assembly: AssemblyProduct(\"{Name}\")]"); |
| | 124 | 256 | | codeBuilder.AppendLine($"[assembly: AssemblyTitle(\"{Description}\")]"); |
| | 124 | 257 | | codeBuilder.AppendLine($"[assembly: AssemblyVersion(\"{Version}\")]"); |
| | 124 | 258 | | codeBuilder.AppendLine($"[assembly: AssemblyFileVersion(\"{Version}\")]"); |
| | 124 | 259 | | codeBuilder.AppendLine($"[assembly: AssemblyInformationalVersion(\"{Version}\")]"); |
| | 124 | 260 | | codeBuilder.AppendLine($"namespace {Name}.V{Version} {{"); |
| | 124 | 261 | | Classes |
| | 463 | 262 | | .ForEach((_, v) => codeBuilder.AppendLine(v.GetCode())); |
| | 124 | 263 | | codeBuilder.AppendLine("}"); |
| | 124 | 264 | | return codeBuilder.ToString(); |
| | 124 | 265 | | } |
| | | 266 | | |
| | | 267 | | public void SetInstanceValue<T>(string instanceName, string fieldName, T fieldValue) |
| | 12 | 268 | | { |
| | 12 | 269 | | SetInstanceValue(instanceName, fieldName, fieldValue, DateTime.UtcNow, QualityLevel.Good); |
| | 12 | 270 | | } |
| | | 271 | | |
| | | 272 | | public void SetInstanceValue<T>(string instanceName, string fieldName, T fieldValue, DateTime timestamp) |
| | 29 | 273 | | { |
| | 29 | 274 | | SetInstanceValue(instanceName, fieldName, fieldValue, timestamp, QualityLevel.Good); |
| | 29 | 275 | | } |
| | | 276 | | |
| | | 277 | | public void SetInstanceValue<T>(string instanceName, string fieldName, T fieldValue, QualityLevel qualityLevel) |
| | 0 | 278 | | { |
| | 0 | 279 | | SetInstanceValue(instanceName, fieldName, fieldValue, DateTime.UtcNow, qualityLevel); |
| | 0 | 280 | | } |
| | | 281 | | |
| | | 282 | | public void SetInstanceValue<T>(string instanceName, string fieldName, T fieldValue, DateTime timestamp, |
| | | 283 | | QualityLevel qualityLevel) |
| | 41 | 284 | | { |
| | 41 | 285 | | Instance instance = GetInstance(instanceName); |
| | 41 | 286 | | IField field = instance.GetField(fieldName); |
| | 41 | 287 | | if (field is not Field<T> typedField) |
| | 0 | 288 | | { |
| | 0 | 289 | | throw new WrongFieldTypeException(fieldName); |
| | | 290 | | } |
| | | 291 | | |
| | 41 | 292 | | typedField.SetValue(fieldValue, timestamp, qualityLevel); |
| | 41 | 293 | | } |
| | | 294 | | |
| | | 295 | | public List<HistoryRow> GetHistoryValues(string instanceName, HistoryTimeRange query, List<string> fieldNames) |
| | 3 | 296 | | { |
| | 3 | 297 | | Instance instance = GetInstance(instanceName); |
| | 3 | 298 | | GetHistoryParametersForFields(instance, fieldNames, |
| | 3 | 299 | | out List<IFieldDefinition> fields, out HistoryRepository? historyRepository, out string? classTimeSerieI |
| | | 300 | | |
| | 3 | 301 | | return GetHistoryValues(instanceName, query, fields, historyRepository!, classTimeSerieId!); |
| | 2 | 302 | | } |
| | | 303 | | |
| | | 304 | | public List<HistoryRow> GetHistoryValues(string instanceName, HistoryTimeRange query, List<IFieldDefinition> fie |
| | | 305 | | HistoryRepository historyRepository, string classTimeSerieId) |
| | 9 | 306 | | { |
| | 9 | 307 | | ValidateTimeRange(query); |
| | 8 | 308 | | return HistoryStorageEngine!.GetHistoryValues(historyRepository!.HistoryStorageId!, classTimeSerieId!, |
| | 8 | 309 | | instanceName, query, fields); |
| | 8 | 310 | | } |
| | | 311 | | |
| | | 312 | | public List<HistoryStatisticRow> GetHistoryStatistics(string instanceName, HistoryStatisticTimeRange query, List |
| | 3 | 313 | | { |
| | 3 | 314 | | Instance instance = GetInstance(instanceName); |
| | 7 | 315 | | GetHistoryParametersForFields(instance, [.. fieldNames.Select(field => field.Name)], |
| | 3 | 316 | | out List<IFieldDefinition> fields, out HistoryRepository? historyRepository, out string? classTimeSerieI |
| | 3 | 317 | | List<HistoryStatisticField> statFields = []; |
| | 14 | 318 | | for (int index = 0; index < fieldNames.Count; index++) |
| | 4 | 319 | | { |
| | 4 | 320 | | statFields.Add(new(fields[index], fieldNames[index].StatisticFunction)); |
| | 4 | 321 | | } |
| | 3 | 322 | | return GetHistoryStatistics(instanceName, query, statFields, historyRepository!, classTimeSerieId!); |
| | 1 | 323 | | } |
| | | 324 | | |
| | | 325 | | public List<HistoryStatisticRow> GetHistoryStatistics(string instanceName, HistoryStatisticTimeRange query, List |
| | | 326 | | HistoryRepository historyRepository, string classTimeSerieId) |
| | 7 | 327 | | { |
| | | 328 | | // if query range is a multiple of interval, remove 1 microsecond to end time (To) to avoid returning a new |
| | 7 | 329 | | if ((query.To - query.From).Ticks % query.Interval.Ticks == 0) |
| | 4 | 330 | | { |
| | 4 | 331 | | query = query with { To = query.To.AddMicroseconds(-1) }; |
| | 4 | 332 | | } |
| | 7 | 333 | | ValidateTimeRange(query); |
| | 5 | 334 | | return HistoryStorageEngine!.GetHistoryStatistics(historyRepository!.HistoryStorageId!, classTimeSerieId!, |
| | 5 | 335 | | instanceName, query, fields); |
| | 5 | 336 | | } |
| | | 337 | | |
| | | 338 | | private static void ValidateTimeRange(HistoryTimeRange timeRange) |
| | 16 | 339 | | { |
| | 16 | 340 | | if (timeRange.From >= timeRange.To) |
| | 2 | 341 | | { |
| | 2 | 342 | | throw new BadHistoryStartTimeException(timeRange.From, timeRange.To); |
| | | 343 | | } |
| | 14 | 344 | | } |
| | | 345 | | |
| | | 346 | | private static void ValidateTimeRange(HistoryStatisticTimeRange timeRange) |
| | 7 | 347 | | { |
| | 7 | 348 | | ValidateTimeRange(timeRange as HistoryTimeRange); |
| | 6 | 349 | | if (timeRange.Interval.Add(TimeSpan.FromMinutes(-1)) > timeRange.To - timeRange.From) |
| | 1 | 350 | | { |
| | 1 | 351 | | throw new BadHistoryIntervalException(timeRange.Interval, timeRange.From, timeRange.To); |
| | | 352 | | } |
| | 5 | 353 | | } |
| | | 354 | | |
| | | 355 | | public static void GetHistoryParametersForFields(Instance instance, List<string> fieldNames, |
| | | 356 | | out List<IFieldDefinition> fields, out HistoryRepository? historyRepository, out string? classTimeSerieId) |
| | 16 | 357 | | { |
| | 16 | 358 | | fields = []; |
| | 16 | 359 | | historyRepository = null; |
| | 16 | 360 | | classTimeSerieId = null; |
| | 184 | 361 | | foreach (string fieldName in fieldNames) |
| | 68 | 362 | | { |
| | 68 | 363 | | IFieldDefinition field = instance.Class.GetField(fieldName); |
| | 68 | 364 | | IHistorizationProcessing? hp = field.ValuePostChangeProcessings |
| | 68 | 365 | | .OfType<IHistorizationProcessing>() |
| | 68 | 366 | | .FirstOrDefault(); |
| | 68 | 367 | | if (hp != null) |
| | 17 | 368 | | { |
| | 17 | 369 | | historyRepository = hp.HistoryRepository; |
| | 17 | 370 | | classTimeSerieId = hp.ClassTimeSerieId; |
| | 17 | 371 | | } |
| | 68 | 372 | | fields.Add(field); |
| | 68 | 373 | | } |
| | 16 | 374 | | if (historyRepository is null || classTimeSerieId is null) |
| | 0 | 375 | | { |
| | 0 | 376 | | throw new NoHistoryStorageEngineException(); |
| | | 377 | | } |
| | 16 | 378 | | } |
| | | 379 | | } |
| | | 380 | | } |