Phase 0 WP-0.2–0.9: Implement Commons (types, entities, interfaces, messages, protocol, tests)
- WP-0.2: Namespace/folder skeleton (26 directories) - WP-0.3: Shared data types (6 enums, RetryPolicy, Result<T>) - WP-0.4: 24 domain entity POCOs across 10 domain areas - WP-0.5: 7 repository interfaces with full CRUD signatures - WP-0.6: IAuditService cross-cutting interface - WP-0.7: 26 message contract records across 8 concern areas - WP-0.8: IDataConnection protocol abstraction with batch ops - WP-0.9: 8 architectural constraint enforcement tests All 40 tests pass, zero warnings.
This commit is contained in:
@@ -1,6 +0,0 @@
|
|||||||
namespace ScadaLink.Commons;
|
|
||||||
|
|
||||||
public class Class1
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
22
src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs
Normal file
22
src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Audit;
|
||||||
|
|
||||||
|
public class AuditLogEntry
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string User { get; set; }
|
||||||
|
public string Action { get; set; }
|
||||||
|
public string EntityType { get; set; }
|
||||||
|
public string EntityId { get; set; }
|
||||||
|
public string EntityName { get; set; }
|
||||||
|
public string? AfterStateJson { get; set; }
|
||||||
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
|
|
||||||
|
public AuditLogEntry(string user, string action, string entityType, string entityId, string entityName)
|
||||||
|
{
|
||||||
|
User = user ?? throw new ArgumentNullException(nameof(user));
|
||||||
|
Action = action ?? throw new ArgumentNullException(nameof(action));
|
||||||
|
EntityType = entityType ?? throw new ArgumentNullException(nameof(entityType));
|
||||||
|
EntityId = entityId ?? throw new ArgumentNullException(nameof(entityId));
|
||||||
|
EntityName = entityName ?? throw new ArgumentNullException(nameof(entityName));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Entities.Deployment;
|
||||||
|
|
||||||
|
public class DeploymentRecord
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int InstanceId { get; set; }
|
||||||
|
public DeploymentStatus Status { get; set; }
|
||||||
|
public string DeploymentId { get; set; }
|
||||||
|
public string? RevisionHash { get; set; }
|
||||||
|
public string DeployedBy { get; set; }
|
||||||
|
public DateTimeOffset DeployedAt { get; set; }
|
||||||
|
public DateTimeOffset? CompletedAt { get; set; }
|
||||||
|
|
||||||
|
public DeploymentRecord(string deploymentId, string deployedBy)
|
||||||
|
{
|
||||||
|
DeploymentId = deploymentId ?? throw new ArgumentNullException(nameof(deploymentId));
|
||||||
|
DeployedBy = deployedBy ?? throw new ArgumentNullException(nameof(deployedBy));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Deployment;
|
||||||
|
|
||||||
|
public class SystemArtifactDeploymentRecord
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string ArtifactType { get; set; }
|
||||||
|
public string DeployedBy { get; set; }
|
||||||
|
public DateTimeOffset DeployedAt { get; set; }
|
||||||
|
public string? PerSiteStatus { get; set; }
|
||||||
|
|
||||||
|
public SystemArtifactDeploymentRecord(string artifactType, string deployedBy)
|
||||||
|
{
|
||||||
|
ArtifactType = artifactType ?? throw new ArgumentNullException(nameof(artifactType));
|
||||||
|
DeployedBy = deployedBy ?? throw new ArgumentNullException(nameof(deployedBy));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.ExternalSystems;
|
||||||
|
|
||||||
|
public class DatabaseConnectionDefinition
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string ConnectionString { get; set; }
|
||||||
|
public int MaxRetries { get; set; }
|
||||||
|
public TimeSpan RetryDelay { get; set; }
|
||||||
|
|
||||||
|
public DatabaseConnectionDefinition(string name, string connectionString)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
ConnectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.ExternalSystems;
|
||||||
|
|
||||||
|
public class ExternalSystemDefinition
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string EndpointUrl { get; set; }
|
||||||
|
public string AuthType { get; set; }
|
||||||
|
public string? AuthConfiguration { get; set; }
|
||||||
|
public int MaxRetries { get; set; }
|
||||||
|
public TimeSpan RetryDelay { get; set; }
|
||||||
|
|
||||||
|
public ExternalSystemDefinition(string name, string endpointUrl, string authType)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
EndpointUrl = endpointUrl ?? throw new ArgumentNullException(nameof(endpointUrl));
|
||||||
|
AuthType = authType ?? throw new ArgumentNullException(nameof(authType));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.ExternalSystems;
|
||||||
|
|
||||||
|
public class ExternalSystemMethod
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int ExternalSystemDefinitionId { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string HttpMethod { get; set; }
|
||||||
|
public string Path { get; set; }
|
||||||
|
public string? ParameterDefinitions { get; set; }
|
||||||
|
public string? ReturnDefinition { get; set; }
|
||||||
|
|
||||||
|
public ExternalSystemMethod(string name, string httpMethod, string path)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
HttpMethod = httpMethod ?? throw new ArgumentNullException(nameof(httpMethod));
|
||||||
|
Path = path ?? throw new ArgumentNullException(nameof(path));
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/ScadaLink.Commons/Entities/InboundApi/ApiKey.cs
Normal file
15
src/ScadaLink.Commons/Entities/InboundApi/ApiKey.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.InboundApi;
|
||||||
|
|
||||||
|
public class ApiKey
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string KeyValue { get; set; }
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
|
||||||
|
public ApiKey(string name, string keyValue)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
KeyValue = keyValue ?? throw new ArgumentNullException(nameof(keyValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
18
src/ScadaLink.Commons/Entities/InboundApi/ApiMethod.cs
Normal file
18
src/ScadaLink.Commons/Entities/InboundApi/ApiMethod.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.InboundApi;
|
||||||
|
|
||||||
|
public class ApiMethod
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Script { get; set; }
|
||||||
|
public string? ApprovedApiKeyIds { get; set; }
|
||||||
|
public string? ParameterDefinitions { get; set; }
|
||||||
|
public string? ReturnDefinition { get; set; }
|
||||||
|
public int TimeoutSeconds { get; set; }
|
||||||
|
|
||||||
|
public ApiMethod(string name, string script)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
Script = script ?? throw new ArgumentNullException(nameof(script));
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/ScadaLink.Commons/Entities/Instances/Area.cs
Normal file
15
src/ScadaLink.Commons/Entities/Instances/Area.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Instances;
|
||||||
|
|
||||||
|
public class Area
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int SiteId { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public int? ParentAreaId { get; set; }
|
||||||
|
public ICollection<Area> Children { get; set; } = new List<Area>();
|
||||||
|
|
||||||
|
public Area(string name)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/ScadaLink.Commons/Entities/Instances/Instance.cs
Normal file
20
src/ScadaLink.Commons/Entities/Instances/Instance.cs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Entities.Instances;
|
||||||
|
|
||||||
|
public class Instance
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int TemplateId { get; set; }
|
||||||
|
public int SiteId { get; set; }
|
||||||
|
public int? AreaId { get; set; }
|
||||||
|
public string UniqueName { get; set; }
|
||||||
|
public InstanceState State { get; set; }
|
||||||
|
public ICollection<InstanceAttributeOverride> AttributeOverrides { get; set; } = new List<InstanceAttributeOverride>();
|
||||||
|
public ICollection<InstanceConnectionBinding> ConnectionBindings { get; set; } = new List<InstanceConnectionBinding>();
|
||||||
|
|
||||||
|
public Instance(string uniqueName)
|
||||||
|
{
|
||||||
|
UniqueName = uniqueName ?? throw new ArgumentNullException(nameof(uniqueName));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Instances;
|
||||||
|
|
||||||
|
public class InstanceAttributeOverride
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int InstanceId { get; set; }
|
||||||
|
public string AttributeName { get; set; }
|
||||||
|
public string? OverrideValue { get; set; }
|
||||||
|
|
||||||
|
public InstanceAttributeOverride(string attributeName)
|
||||||
|
{
|
||||||
|
AttributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Instances;
|
||||||
|
|
||||||
|
public class InstanceConnectionBinding
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int InstanceId { get; set; }
|
||||||
|
public string AttributeName { get; set; }
|
||||||
|
public int DataConnectionId { get; set; }
|
||||||
|
|
||||||
|
public InstanceConnectionBinding(string attributeName)
|
||||||
|
{
|
||||||
|
AttributeName = attributeName ?? throw new ArgumentNullException(nameof(attributeName));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Notifications;
|
||||||
|
|
||||||
|
public class NotificationList
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public ICollection<NotificationRecipient> Recipients { get; set; } = new List<NotificationRecipient>();
|
||||||
|
|
||||||
|
public NotificationList(string name)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Notifications;
|
||||||
|
|
||||||
|
public class NotificationRecipient
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int NotificationListId { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string EmailAddress { get; set; }
|
||||||
|
|
||||||
|
public NotificationRecipient(string name, string emailAddress)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
EmailAddress = emailAddress ?? throw new ArgumentNullException(nameof(emailAddress));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Notifications;
|
||||||
|
|
||||||
|
public class SmtpConfiguration
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Host { get; set; }
|
||||||
|
public int Port { get; set; }
|
||||||
|
public string AuthType { get; set; }
|
||||||
|
public string? Credentials { get; set; }
|
||||||
|
public string? TlsMode { get; set; }
|
||||||
|
public string FromAddress { get; set; }
|
||||||
|
public int ConnectionTimeoutSeconds { get; set; }
|
||||||
|
public int MaxConcurrentConnections { get; set; }
|
||||||
|
public int MaxRetries { get; set; }
|
||||||
|
public TimeSpan RetryDelay { get; set; }
|
||||||
|
|
||||||
|
public SmtpConfiguration(string host, string authType, string fromAddress)
|
||||||
|
{
|
||||||
|
Host = host ?? throw new ArgumentNullException(nameof(host));
|
||||||
|
AuthType = authType ?? throw new ArgumentNullException(nameof(authType));
|
||||||
|
FromAddress = fromAddress ?? throw new ArgumentNullException(nameof(fromAddress));
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/ScadaLink.Commons/Entities/Scripts/SharedScript.cs
Normal file
16
src/ScadaLink.Commons/Entities/Scripts/SharedScript.cs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Scripts;
|
||||||
|
|
||||||
|
public class SharedScript
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Code { get; set; }
|
||||||
|
public string? ParameterDefinitions { get; set; }
|
||||||
|
public string? ReturnDefinition { get; set; }
|
||||||
|
|
||||||
|
public SharedScript(string name, string code)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
Code = code ?? throw new ArgumentNullException(nameof(code));
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/ScadaLink.Commons/Entities/Security/LdapGroupMapping.cs
Normal file
14
src/ScadaLink.Commons/Entities/Security/LdapGroupMapping.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Security;
|
||||||
|
|
||||||
|
public class LdapGroupMapping
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string LdapGroupName { get; set; }
|
||||||
|
public string Role { get; set; }
|
||||||
|
|
||||||
|
public LdapGroupMapping(string ldapGroupName, string role)
|
||||||
|
{
|
||||||
|
LdapGroupName = ldapGroupName ?? throw new ArgumentNullException(nameof(ldapGroupName));
|
||||||
|
Role = role ?? throw new ArgumentNullException(nameof(role));
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/ScadaLink.Commons/Entities/Security/SiteScopeRule.cs
Normal file
8
src/ScadaLink.Commons/Entities/Security/SiteScopeRule.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Security;
|
||||||
|
|
||||||
|
public class SiteScopeRule
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int LdapGroupMappingId { get; set; }
|
||||||
|
public int SiteId { get; set; }
|
||||||
|
}
|
||||||
15
src/ScadaLink.Commons/Entities/Sites/DataConnection.cs
Normal file
15
src/ScadaLink.Commons/Entities/Sites/DataConnection.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Sites;
|
||||||
|
|
||||||
|
public class DataConnection
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Protocol { get; set; }
|
||||||
|
public string? Configuration { get; set; }
|
||||||
|
|
||||||
|
public DataConnection(string name, string protocol)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
Protocol = protocol ?? throw new ArgumentNullException(nameof(protocol));
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/ScadaLink.Commons/Entities/Sites/Site.cs
Normal file
15
src/ScadaLink.Commons/Entities/Sites/Site.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Sites;
|
||||||
|
|
||||||
|
public class Site
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string SiteIdentifier { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
public Site(string name, string siteIdentifier)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
SiteIdentifier = siteIdentifier ?? throw new ArgumentNullException(nameof(siteIdentifier));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Sites;
|
||||||
|
|
||||||
|
public class SiteDataConnectionAssignment
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int SiteId { get; set; }
|
||||||
|
public int DataConnectionId { get; set; }
|
||||||
|
}
|
||||||
18
src/ScadaLink.Commons/Entities/Templates/Template.cs
Normal file
18
src/ScadaLink.Commons/Entities/Templates/Template.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Templates;
|
||||||
|
|
||||||
|
public class Template
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public int? ParentTemplateId { get; set; }
|
||||||
|
public ICollection<TemplateAttribute> Attributes { get; set; } = new List<TemplateAttribute>();
|
||||||
|
public ICollection<TemplateAlarm> Alarms { get; set; } = new List<TemplateAlarm>();
|
||||||
|
public ICollection<TemplateScript> Scripts { get; set; } = new List<TemplateScript>();
|
||||||
|
public ICollection<TemplateComposition> Compositions { get; set; } = new List<TemplateComposition>();
|
||||||
|
|
||||||
|
public Template(string name)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/ScadaLink.Commons/Entities/Templates/TemplateAlarm.cs
Normal file
21
src/ScadaLink.Commons/Entities/Templates/TemplateAlarm.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Entities.Templates;
|
||||||
|
|
||||||
|
public class TemplateAlarm
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int TemplateId { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public int PriorityLevel { get; set; }
|
||||||
|
public bool IsLocked { get; set; }
|
||||||
|
public AlarmTriggerType TriggerType { get; set; }
|
||||||
|
public string? TriggerConfiguration { get; set; }
|
||||||
|
public int? OnTriggerScriptId { get; set; }
|
||||||
|
|
||||||
|
public TemplateAlarm(string name)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Entities.Templates;
|
||||||
|
|
||||||
|
public class TemplateAttribute
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int TemplateId { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string? Value { get; set; }
|
||||||
|
public DataType DataType { get; set; }
|
||||||
|
public bool IsLocked { get; set; }
|
||||||
|
public string? Description { get; set; }
|
||||||
|
public string? DataSourceReference { get; set; }
|
||||||
|
|
||||||
|
public TemplateAttribute(string name)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Templates;
|
||||||
|
|
||||||
|
public class TemplateComposition
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int TemplateId { get; set; }
|
||||||
|
public int ComposedTemplateId { get; set; }
|
||||||
|
public string InstanceName { get; set; }
|
||||||
|
|
||||||
|
public TemplateComposition(string instanceName)
|
||||||
|
{
|
||||||
|
InstanceName = instanceName ?? throw new ArgumentNullException(nameof(instanceName));
|
||||||
|
}
|
||||||
|
}
|
||||||
21
src/ScadaLink.Commons/Entities/Templates/TemplateScript.cs
Normal file
21
src/ScadaLink.Commons/Entities/Templates/TemplateScript.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace ScadaLink.Commons.Entities.Templates;
|
||||||
|
|
||||||
|
public class TemplateScript
|
||||||
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
|
public int TemplateId { get; set; }
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool IsLocked { get; set; }
|
||||||
|
public string Code { get; set; }
|
||||||
|
public string? TriggerType { get; set; }
|
||||||
|
public string? TriggerConfiguration { get; set; }
|
||||||
|
public string? ParameterDefinitions { get; set; }
|
||||||
|
public string? ReturnDefinition { get; set; }
|
||||||
|
public TimeSpan? MinTimeBetweenRuns { get; set; }
|
||||||
|
|
||||||
|
public TemplateScript(string name, string code)
|
||||||
|
{
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
Code = code ?? throw new ArgumentNullException(nameof(code));
|
||||||
|
}
|
||||||
|
}
|
||||||
25
src/ScadaLink.Commons/Interfaces/Protocol/IDataConnection.cs
Normal file
25
src/ScadaLink.Commons/Interfaces/Protocol/IDataConnection.cs
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Interfaces.Protocol;
|
||||||
|
|
||||||
|
public enum QualityCode { Good, Bad, Uncertain }
|
||||||
|
|
||||||
|
public record TagValue(object? Value, QualityCode Quality, DateTimeOffset Timestamp);
|
||||||
|
public record ReadResult(bool Success, TagValue? Value, string? ErrorMessage);
|
||||||
|
public record WriteResult(bool Success, string? ErrorMessage);
|
||||||
|
|
||||||
|
public delegate void SubscriptionCallback(string tagPath, TagValue value);
|
||||||
|
|
||||||
|
public interface IDataConnection : IAsyncDisposable
|
||||||
|
{
|
||||||
|
Task ConnectAsync(IDictionary<string, string> connectionDetails, CancellationToken cancellationToken = default);
|
||||||
|
Task DisconnectAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<string> SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default);
|
||||||
|
Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default);
|
||||||
|
Task<ReadResult> ReadAsync(string tagPath, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyDictionary<string, ReadResult>> ReadBatchAsync(IEnumerable<string> tagPaths, CancellationToken cancellationToken = default);
|
||||||
|
Task<WriteResult> WriteAsync(string tagPath, object? value, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyDictionary<string, WriteResult>> WriteBatchAsync(IDictionary<string, object?> values, CancellationToken cancellationToken = default);
|
||||||
|
Task<bool> WriteBatchAndWaitAsync(IDictionary<string, object?> values, string flagPath, object? flagValue, string responsePath, object? responseValue, TimeSpan timeout, CancellationToken cancellationToken = default);
|
||||||
|
ConnectionHealth Status { get; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using ScadaLink.Commons.Entities.Deployment;
|
||||||
|
using ScadaLink.Commons.Entities.Instances;
|
||||||
|
using ScadaLink.Commons.Entities.Sites;
|
||||||
|
using ScadaLink.Commons.Entities.Templates;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Interfaces.Repositories;
|
||||||
|
|
||||||
|
public interface ICentralUiRepository
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<Site>> GetAllSitesAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<DataConnection>> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<SiteDataConnectionAssignment>> GetAllSiteDataConnectionAssignmentsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<Template>> GetTemplateTreeAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<Instance>> GetInstancesFilteredAsync(int? siteId = null, int? templateId = null, string? searchTerm = null, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<DeploymentRecord>> GetRecentDeploymentsAsync(int count, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<Area>> GetAreaTreeBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
using ScadaLink.Commons.Entities.Deployment;
|
||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Interfaces.Repositories;
|
||||||
|
|
||||||
|
public interface IDeploymentManagerRepository
|
||||||
|
{
|
||||||
|
// DeploymentRecord
|
||||||
|
Task<DeploymentRecord?> GetDeploymentRecordByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<DeploymentRecord>> GetAllDeploymentRecordsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<DeploymentRecord>> GetDeploymentsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
|
||||||
|
Task<DeploymentRecord?> GetCurrentDeploymentStatusAsync(int instanceId, CancellationToken cancellationToken = default);
|
||||||
|
Task<DeploymentRecord?> GetDeploymentByDeploymentIdAsync(string deploymentId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddDeploymentRecordAsync(DeploymentRecord record, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateDeploymentRecordAsync(DeploymentRecord record, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteDeploymentRecordAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// SystemArtifactDeploymentRecord
|
||||||
|
Task<SystemArtifactDeploymentRecord?> GetSystemArtifactDeploymentByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<SystemArtifactDeploymentRecord>> GetAllSystemArtifactDeploymentsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task AddSystemArtifactDeploymentAsync(SystemArtifactDeploymentRecord record, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateSystemArtifactDeploymentAsync(SystemArtifactDeploymentRecord record, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteSystemArtifactDeploymentAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using ScadaLink.Commons.Entities.ExternalSystems;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Interfaces.Repositories;
|
||||||
|
|
||||||
|
public interface IExternalSystemRepository
|
||||||
|
{
|
||||||
|
// ExternalSystemDefinition
|
||||||
|
Task<ExternalSystemDefinition?> GetExternalSystemByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<ExternalSystemDefinition>> GetAllExternalSystemsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task AddExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateExternalSystemAsync(ExternalSystemDefinition definition, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteExternalSystemAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// ExternalSystemMethod
|
||||||
|
Task<ExternalSystemMethod?> GetExternalSystemMethodByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<ExternalSystemMethod>> GetMethodsByExternalSystemIdAsync(int externalSystemId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateExternalSystemMethodAsync(ExternalSystemMethod method, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteExternalSystemMethodAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// DatabaseConnectionDefinition
|
||||||
|
Task<DatabaseConnectionDefinition?> GetDatabaseConnectionByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<DatabaseConnectionDefinition>> GetAllDatabaseConnectionsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task AddDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateDatabaseConnectionAsync(DatabaseConnectionDefinition definition, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteDatabaseConnectionAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using ScadaLink.Commons.Entities.InboundApi;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Interfaces.Repositories;
|
||||||
|
|
||||||
|
public interface IInboundApiRepository
|
||||||
|
{
|
||||||
|
// ApiKey
|
||||||
|
Task<ApiKey?> GetApiKeyByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<ApiKey>> GetAllApiKeysAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<ApiKey?> GetApiKeyByValueAsync(string keyValue, CancellationToken cancellationToken = default);
|
||||||
|
Task AddApiKeyAsync(ApiKey apiKey, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateApiKeyAsync(ApiKey apiKey, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteApiKeyAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// ApiMethod
|
||||||
|
Task<ApiMethod?> GetApiMethodByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<ApiMethod>> GetAllApiMethodsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<ApiMethod?> GetMethodByNameAsync(string name, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<ApiKey>> GetApprovedKeysForMethodAsync(int methodId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateApiMethodAsync(ApiMethod method, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteApiMethodAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
using ScadaLink.Commons.Entities.Notifications;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Interfaces.Repositories;
|
||||||
|
|
||||||
|
public interface INotificationRepository
|
||||||
|
{
|
||||||
|
// NotificationList
|
||||||
|
Task<NotificationList?> GetNotificationListByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<NotificationList>> GetAllNotificationListsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<NotificationList?> GetListByNameAsync(string name, CancellationToken cancellationToken = default);
|
||||||
|
Task AddNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateNotificationListAsync(NotificationList list, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteNotificationListAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// NotificationRecipient
|
||||||
|
Task<NotificationRecipient?> GetRecipientByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<NotificationRecipient>> GetRecipientsByListIdAsync(int notificationListId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateRecipientAsync(NotificationRecipient recipient, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteRecipientAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// SmtpConfiguration
|
||||||
|
Task<SmtpConfiguration?> GetSmtpConfigurationByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<SmtpConfiguration>> GetAllSmtpConfigurationsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task AddSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateSmtpConfigurationAsync(SmtpConfiguration configuration, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteSmtpConfigurationAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using ScadaLink.Commons.Entities.Security;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Interfaces.Repositories;
|
||||||
|
|
||||||
|
public interface ISecurityRepository
|
||||||
|
{
|
||||||
|
// LdapGroupMapping
|
||||||
|
Task<LdapGroupMapping?> GetMappingByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<LdapGroupMapping>> GetAllMappingsAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<LdapGroupMapping>> GetMappingsByRoleAsync(string role, CancellationToken cancellationToken = default);
|
||||||
|
Task AddMappingAsync(LdapGroupMapping mapping, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateMappingAsync(LdapGroupMapping mapping, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteMappingAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// SiteScopeRule
|
||||||
|
Task<SiteScopeRule?> GetScopeRuleByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<SiteScopeRule>> GetScopeRulesForMappingAsync(int ldapGroupMappingId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddScopeRuleAsync(SiteScopeRule rule, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateScopeRuleAsync(SiteScopeRule rule, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteScopeRuleAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
using ScadaLink.Commons.Entities.Instances;
|
||||||
|
using ScadaLink.Commons.Entities.Templates;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Interfaces.Repositories;
|
||||||
|
|
||||||
|
public interface ITemplateEngineRepository
|
||||||
|
{
|
||||||
|
// Template
|
||||||
|
Task<Template?> GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<Template?> GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<Template>> GetAllTemplatesAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task AddTemplateAsync(Template template, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateTemplateAsync(Template template, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteTemplateAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// TemplateAttribute
|
||||||
|
Task<TemplateAttribute?> GetTemplateAttributeByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<TemplateAttribute>> GetAttributesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateTemplateAttributeAsync(TemplateAttribute attribute, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteTemplateAttributeAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// TemplateAlarm
|
||||||
|
Task<TemplateAlarm?> GetTemplateAlarmByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<TemplateAlarm>> GetAlarmsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateTemplateAlarmAsync(TemplateAlarm alarm, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteTemplateAlarmAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// TemplateScript
|
||||||
|
Task<TemplateScript?> GetTemplateScriptByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<TemplateScript>> GetScriptsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateTemplateScriptAsync(TemplateScript script, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteTemplateScriptAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// TemplateComposition
|
||||||
|
Task<TemplateComposition?> GetTemplateCompositionByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<TemplateComposition>> GetCompositionsByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateTemplateCompositionAsync(TemplateComposition composition, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteTemplateCompositionAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// Instance
|
||||||
|
Task<Instance?> GetInstanceByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<Instance>> GetAllInstancesAsync(CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<Instance>> GetInstancesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<Instance>> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
|
||||||
|
Task<Instance?> GetInstanceByUniqueNameAsync(string uniqueName, CancellationToken cancellationToken = default);
|
||||||
|
Task AddInstanceAsync(Instance instance, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateInstanceAsync(Instance instance, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteInstanceAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// InstanceAttributeOverride
|
||||||
|
Task<IReadOnlyList<InstanceAttributeOverride>> GetOverridesByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateInstanceAttributeOverrideAsync(InstanceAttributeOverride attributeOverride, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteInstanceAttributeOverrideAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// InstanceConnectionBinding
|
||||||
|
Task<IReadOnlyList<InstanceConnectionBinding>> GetBindingsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateInstanceConnectionBindingAsync(InstanceConnectionBinding binding, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteInstanceConnectionBindingAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
// Area
|
||||||
|
Task<Area?> GetAreaByIdAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
Task<IReadOnlyList<Area>> GetAreasBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
|
||||||
|
Task AddAreaAsync(Area area, CancellationToken cancellationToken = default);
|
||||||
|
Task UpdateAreaAsync(Area area, CancellationToken cancellationToken = default);
|
||||||
|
Task DeleteAreaAsync(int id, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<int> SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ScadaLink.Commons.Interfaces.Services;
|
||||||
|
|
||||||
|
public interface IAuditService
|
||||||
|
{
|
||||||
|
Task LogAsync(string user, string action, string entityType, string entityId, string entityName, object? afterState, CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Artifacts;
|
||||||
|
|
||||||
|
public record ArtifactDeploymentResponse(
|
||||||
|
string DeploymentId,
|
||||||
|
string SiteId,
|
||||||
|
bool Success,
|
||||||
|
string? ErrorMessage,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Artifacts;
|
||||||
|
|
||||||
|
public record DatabaseConnectionArtifact(
|
||||||
|
string Name,
|
||||||
|
string ConnectionString,
|
||||||
|
int MaxRetries,
|
||||||
|
TimeSpan RetryDelay);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Artifacts;
|
||||||
|
|
||||||
|
public record DeployArtifactsCommand(
|
||||||
|
string DeploymentId,
|
||||||
|
IReadOnlyList<SharedScriptArtifact>? SharedScripts,
|
||||||
|
IReadOnlyList<ExternalSystemArtifact>? ExternalSystems,
|
||||||
|
IReadOnlyList<DatabaseConnectionArtifact>? DatabaseConnections,
|
||||||
|
IReadOnlyList<NotificationListArtifact>? NotificationLists,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Artifacts;
|
||||||
|
|
||||||
|
public record ExternalSystemArtifact(
|
||||||
|
string Name,
|
||||||
|
string EndpointUrl,
|
||||||
|
string AuthType,
|
||||||
|
string? AuthConfiguration,
|
||||||
|
string? MethodDefinitionsJson);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Artifacts;
|
||||||
|
|
||||||
|
public record NotificationListArtifact(
|
||||||
|
string Name,
|
||||||
|
IReadOnlyList<string> RecipientEmails);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Artifacts;
|
||||||
|
|
||||||
|
public record SharedScriptArtifact(
|
||||||
|
string Name,
|
||||||
|
string Code,
|
||||||
|
string? ParameterDefinitions,
|
||||||
|
string? ReturnDefinition);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Communication;
|
||||||
|
|
||||||
|
public record ConnectionStateChanged(
|
||||||
|
string SiteId,
|
||||||
|
bool IsConnected,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Communication;
|
||||||
|
|
||||||
|
public record RoutingMetadata(
|
||||||
|
string TargetSiteId,
|
||||||
|
string? TargetInstanceUniqueName,
|
||||||
|
string CorrelationId);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Communication;
|
||||||
|
|
||||||
|
public record SiteIdentity(
|
||||||
|
string SiteId,
|
||||||
|
string NodeHostname,
|
||||||
|
bool IsActive);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using ScadaLink.Commons.Messages.Streaming;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Messages.DebugView;
|
||||||
|
|
||||||
|
public record DebugViewSnapshot(
|
||||||
|
string InstanceUniqueName,
|
||||||
|
IReadOnlyList<AttributeValueChanged> AttributeValues,
|
||||||
|
IReadOnlyList<AlarmStateChanged> AlarmStates,
|
||||||
|
DateTimeOffset SnapshotTimestamp);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.DebugView;
|
||||||
|
|
||||||
|
public record SubscribeDebugViewRequest(
|
||||||
|
string InstanceUniqueName,
|
||||||
|
string CorrelationId);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.DebugView;
|
||||||
|
|
||||||
|
public record UnsubscribeDebugViewRequest(
|
||||||
|
string InstanceUniqueName,
|
||||||
|
string CorrelationId);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Deployment;
|
||||||
|
|
||||||
|
public record DeployInstanceCommand(
|
||||||
|
string DeploymentId,
|
||||||
|
string InstanceUniqueName,
|
||||||
|
string RevisionHash,
|
||||||
|
string FlattenedConfigurationJson,
|
||||||
|
string DeployedBy,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Messages.Deployment;
|
||||||
|
|
||||||
|
public record DeploymentStatusResponse(
|
||||||
|
string DeploymentId,
|
||||||
|
string InstanceUniqueName,
|
||||||
|
DeploymentStatus Status,
|
||||||
|
string? ErrorMessage,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Deployment;
|
||||||
|
|
||||||
|
public record DeploymentValidationResult(
|
||||||
|
bool IsValid,
|
||||||
|
IReadOnlyList<string> Errors,
|
||||||
|
IReadOnlyList<string> Warnings);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Deployment;
|
||||||
|
|
||||||
|
public record FlattenedConfigurationSnapshot(
|
||||||
|
string InstanceUniqueName,
|
||||||
|
string RevisionHash,
|
||||||
|
string ConfigurationJson,
|
||||||
|
DateTimeOffset GeneratedAt);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Health;
|
||||||
|
|
||||||
|
public record HeartbeatMessage(
|
||||||
|
string SiteId,
|
||||||
|
string NodeHostname,
|
||||||
|
bool IsActive,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
14
src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs
Normal file
14
src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Messages.Health;
|
||||||
|
|
||||||
|
public record SiteHealthReport(
|
||||||
|
string SiteId,
|
||||||
|
long SequenceNumber,
|
||||||
|
DateTimeOffset ReportTimestamp,
|
||||||
|
IReadOnlyDictionary<string, ConnectionHealth> DataConnectionStatuses,
|
||||||
|
IReadOnlyDictionary<string, TagResolutionStatus> TagResolutionCounts,
|
||||||
|
int ScriptErrorCount,
|
||||||
|
int AlarmEvaluationErrorCount,
|
||||||
|
IReadOnlyDictionary<string, int> StoreAndForwardBufferDepths,
|
||||||
|
int DeadLetterCount);
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Health;
|
||||||
|
|
||||||
|
public record TagResolutionStatus(int TotalSubscribed, int SuccessfullyResolved);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Lifecycle;
|
||||||
|
|
||||||
|
public record DeleteInstanceCommand(
|
||||||
|
string CommandId,
|
||||||
|
string InstanceUniqueName,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Lifecycle;
|
||||||
|
|
||||||
|
public record DisableInstanceCommand(
|
||||||
|
string CommandId,
|
||||||
|
string InstanceUniqueName,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Lifecycle;
|
||||||
|
|
||||||
|
public record EnableInstanceCommand(
|
||||||
|
string CommandId,
|
||||||
|
string InstanceUniqueName,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Lifecycle;
|
||||||
|
|
||||||
|
public record InstanceLifecycleResponse(
|
||||||
|
string CommandId,
|
||||||
|
string InstanceUniqueName,
|
||||||
|
bool Success,
|
||||||
|
string? ErrorMessage,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.ScriptExecution;
|
||||||
|
|
||||||
|
public record ScriptCallRequest(
|
||||||
|
string ScriptName,
|
||||||
|
IReadOnlyDictionary<string, object?>? Parameters,
|
||||||
|
int CurrentCallDepth,
|
||||||
|
string CorrelationId);
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.ScriptExecution;
|
||||||
|
|
||||||
|
public record ScriptCallResult(
|
||||||
|
string CorrelationId,
|
||||||
|
bool Success,
|
||||||
|
object? ReturnValue,
|
||||||
|
string? ErrorMessage);
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Messages.Streaming;
|
||||||
|
|
||||||
|
public record AlarmStateChanged(
|
||||||
|
string InstanceUniqueName,
|
||||||
|
string AlarmName,
|
||||||
|
AlarmState State,
|
||||||
|
int Priority,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ScadaLink.Commons.Messages.Streaming;
|
||||||
|
|
||||||
|
public record AttributeValueChanged(
|
||||||
|
string InstanceUniqueName,
|
||||||
|
string AttributePath,
|
||||||
|
string AttributeName,
|
||||||
|
object? Value,
|
||||||
|
string Quality,
|
||||||
|
DateTimeOffset Timestamp);
|
||||||
7
src/ScadaLink.Commons/Types/Enums/AlarmState.cs
Normal file
7
src/ScadaLink.Commons/Types/Enums/AlarmState.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
public enum AlarmState
|
||||||
|
{
|
||||||
|
Active,
|
||||||
|
Normal
|
||||||
|
}
|
||||||
8
src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs
Normal file
8
src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
namespace ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
public enum AlarmTriggerType
|
||||||
|
{
|
||||||
|
ValueMatch,
|
||||||
|
RangeViolation,
|
||||||
|
RateOfChange
|
||||||
|
}
|
||||||
9
src/ScadaLink.Commons/Types/Enums/ConnectionHealth.cs
Normal file
9
src/ScadaLink.Commons/Types/Enums/ConnectionHealth.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
public enum ConnectionHealth
|
||||||
|
{
|
||||||
|
Connected,
|
||||||
|
Disconnected,
|
||||||
|
Connecting,
|
||||||
|
Error
|
||||||
|
}
|
||||||
12
src/ScadaLink.Commons/Types/Enums/DataType.cs
Normal file
12
src/ScadaLink.Commons/Types/Enums/DataType.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
namespace ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
public enum DataType
|
||||||
|
{
|
||||||
|
Boolean,
|
||||||
|
Int32,
|
||||||
|
Float,
|
||||||
|
Double,
|
||||||
|
String,
|
||||||
|
DateTime,
|
||||||
|
Binary
|
||||||
|
}
|
||||||
9
src/ScadaLink.Commons/Types/Enums/DeploymentStatus.cs
Normal file
9
src/ScadaLink.Commons/Types/Enums/DeploymentStatus.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
public enum DeploymentStatus
|
||||||
|
{
|
||||||
|
Pending,
|
||||||
|
InProgress,
|
||||||
|
Success,
|
||||||
|
Failed
|
||||||
|
}
|
||||||
7
src/ScadaLink.Commons/Types/Enums/InstanceState.cs
Normal file
7
src/ScadaLink.Commons/Types/Enums/InstanceState.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
public enum InstanceState
|
||||||
|
{
|
||||||
|
Enabled,
|
||||||
|
Disabled
|
||||||
|
}
|
||||||
40
src/ScadaLink.Commons/Types/Result.cs
Normal file
40
src/ScadaLink.Commons/Types/Result.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
namespace ScadaLink.Commons.Types;
|
||||||
|
|
||||||
|
public sealed class Result<T>
|
||||||
|
{
|
||||||
|
private readonly T? _value;
|
||||||
|
private readonly string? _error;
|
||||||
|
|
||||||
|
private Result(T value)
|
||||||
|
{
|
||||||
|
_value = value;
|
||||||
|
_error = null;
|
||||||
|
IsSuccess = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Result(string error)
|
||||||
|
{
|
||||||
|
_value = default;
|
||||||
|
_error = error;
|
||||||
|
IsSuccess = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsSuccess { get; }
|
||||||
|
|
||||||
|
public bool IsFailure => !IsSuccess;
|
||||||
|
|
||||||
|
public T Value => IsSuccess
|
||||||
|
? _value!
|
||||||
|
: throw new InvalidOperationException("Cannot access Value on a failed Result. Error: " + _error);
|
||||||
|
|
||||||
|
public string Error => IsFailure
|
||||||
|
? _error!
|
||||||
|
: throw new InvalidOperationException("Cannot access Error on a successful Result.");
|
||||||
|
|
||||||
|
public static Result<T> Success(T value) => new(value);
|
||||||
|
|
||||||
|
public static Result<T> Failure(string error) => new(error);
|
||||||
|
|
||||||
|
public TResult Match<TResult>(Func<T, TResult> onSuccess, Func<string, TResult> onFailure) =>
|
||||||
|
IsSuccess ? onSuccess(_value!) : onFailure(_error!);
|
||||||
|
}
|
||||||
3
src/ScadaLink.Commons/Types/RetryPolicy.cs
Normal file
3
src/ScadaLink.Commons/Types/RetryPolicy.cs
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
namespace ScadaLink.Commons.Types;
|
||||||
|
|
||||||
|
public record RetryPolicy(int MaxRetries, TimeSpan Delay);
|
||||||
187
tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs
Normal file
187
tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Tests;
|
||||||
|
|
||||||
|
public class ArchitecturalConstraintTests
|
||||||
|
{
|
||||||
|
private static readonly Assembly CommonsAssembly = typeof(ScadaLink.Commons.Types.RetryPolicy).Assembly;
|
||||||
|
|
||||||
|
private static string GetCsprojPath()
|
||||||
|
{
|
||||||
|
// Walk up from test output to find the Commons csproj
|
||||||
|
var dir = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
while (dir != null)
|
||||||
|
{
|
||||||
|
var candidate = Path.Combine(dir.FullName, "src", "ScadaLink.Commons", "ScadaLink.Commons.csproj");
|
||||||
|
if (File.Exists(candidate))
|
||||||
|
return candidate;
|
||||||
|
dir = dir.Parent;
|
||||||
|
}
|
||||||
|
throw new InvalidOperationException("Could not find ScadaLink.Commons.csproj");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCommonsSourceDirectory()
|
||||||
|
{
|
||||||
|
var csproj = GetCsprojPath();
|
||||||
|
return Path.GetDirectoryName(csproj)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Csproj_ShouldNotHaveForbiddenPackageReferences()
|
||||||
|
{
|
||||||
|
var csprojPath = GetCsprojPath();
|
||||||
|
var doc = XDocument.Load(csprojPath);
|
||||||
|
var ns = doc.Root!.GetDefaultNamespace();
|
||||||
|
|
||||||
|
var forbiddenPrefixes = new[] { "Akka.", "Microsoft.AspNetCore.", "Microsoft.EntityFrameworkCore." };
|
||||||
|
|
||||||
|
var packageRefs = doc.Descendants(ns + "PackageReference")
|
||||||
|
.Select(e => e.Attribute("Include")?.Value)
|
||||||
|
.Where(v => v != null)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var pkg in packageRefs)
|
||||||
|
{
|
||||||
|
foreach (var prefix in forbiddenPrefixes)
|
||||||
|
{
|
||||||
|
Assert.False(pkg!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase),
|
||||||
|
$"Commons has forbidden package reference: {pkg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Assembly_ShouldNotReferenceForbiddenAssemblies()
|
||||||
|
{
|
||||||
|
var forbiddenPrefixes = new[] { "Akka.", "Microsoft.AspNetCore.", "Microsoft.EntityFrameworkCore." };
|
||||||
|
|
||||||
|
var referencedAssemblies = CommonsAssembly.GetReferencedAssemblies();
|
||||||
|
|
||||||
|
foreach (var asmName in referencedAssemblies)
|
||||||
|
{
|
||||||
|
foreach (var prefix in forbiddenPrefixes)
|
||||||
|
{
|
||||||
|
Assert.False(asmName.Name!.StartsWith(prefix, StringComparison.OrdinalIgnoreCase),
|
||||||
|
$"Commons references forbidden assembly: {asmName.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Commons_ShouldNotContainServiceOrActorImplementations()
|
||||||
|
{
|
||||||
|
// Heuristic: class has > 3 public non-property methods that are not constructors
|
||||||
|
var types = CommonsAssembly.GetTypes()
|
||||||
|
.Where(t => t.IsClass && !t.IsAbstract && !t.IsInterface);
|
||||||
|
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
var publicMethods = type.GetMethods(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)
|
||||||
|
.Where(m => !m.IsSpecialName) // excludes property getters/setters
|
||||||
|
.Where(m => !m.Name.StartsWith("<")) // excludes compiler-generated
|
||||||
|
.Where(m => m.Name != "ToString" && m.Name != "GetHashCode" &&
|
||||||
|
m.Name != "Equals" && m.Name != "Deconstruct" &&
|
||||||
|
m.Name != "PrintMembers" && m.Name != "GetType")
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
Assert.True(publicMethods.Count <= 3,
|
||||||
|
$"Type {type.FullName} has {publicMethods.Count} public methods ({string.Join(", ", publicMethods.Select(m => m.Name))}), which suggests it may contain service/actor logic");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SourceFiles_ShouldNotContainToLocalTime()
|
||||||
|
{
|
||||||
|
var sourceDir = GetCommonsSourceDirectory();
|
||||||
|
var csFiles = Directory.GetFiles(sourceDir, "*.cs", SearchOption.AllDirectories);
|
||||||
|
|
||||||
|
foreach (var file in csFiles)
|
||||||
|
{
|
||||||
|
var content = File.ReadAllText(file);
|
||||||
|
Assert.DoesNotContain("ToLocalTime()", content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllEntities_ShouldHaveNoEfAttributes()
|
||||||
|
{
|
||||||
|
var efAttributeNames = new HashSet<string>
|
||||||
|
{
|
||||||
|
"KeyAttribute", "ForeignKeyAttribute", "TableAttribute",
|
||||||
|
"ColumnAttribute", "RequiredAttribute", "MaxLengthAttribute",
|
||||||
|
"StringLengthAttribute", "DatabaseGeneratedAttribute",
|
||||||
|
"NotMappedAttribute", "IndexAttribute", "InversePropertyAttribute"
|
||||||
|
};
|
||||||
|
|
||||||
|
var entityTypes = CommonsAssembly.GetTypes()
|
||||||
|
.Where(t => t.IsClass && !t.IsAbstract && t.Namespace != null
|
||||||
|
&& t.Namespace.Contains(".Entities."));
|
||||||
|
|
||||||
|
foreach (var type in entityTypes)
|
||||||
|
{
|
||||||
|
foreach (var attr in type.GetCustomAttributes(true))
|
||||||
|
{
|
||||||
|
Assert.DoesNotContain(attr.GetType().Name, efAttributeNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var prop in type.GetProperties())
|
||||||
|
{
|
||||||
|
foreach (var attr in prop.GetCustomAttributes(true))
|
||||||
|
{
|
||||||
|
Assert.False(efAttributeNames.Contains(attr.GetType().Name),
|
||||||
|
$"{type.Name}.{prop.Name} has EF attribute {attr.GetType().Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllEnums_ShouldBeSingularNamed()
|
||||||
|
{
|
||||||
|
var enums = CommonsAssembly.GetTypes().Where(t => t.IsEnum);
|
||||||
|
|
||||||
|
foreach (var enumType in enums)
|
||||||
|
{
|
||||||
|
var name = enumType.Name;
|
||||||
|
// Singular names should not end with 's' (except words ending in 'ss' or 'us' which are singular)
|
||||||
|
Assert.False(name.EndsWith("s") && !name.EndsWith("ss") && !name.EndsWith("us"),
|
||||||
|
$"Enum {name} appears to be plural");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllMessageTypes_ShouldBeRecords()
|
||||||
|
{
|
||||||
|
var messageTypes = CommonsAssembly.GetTypes()
|
||||||
|
.Where(t => t.Namespace != null
|
||||||
|
&& t.Namespace.Contains(".Messages.")
|
||||||
|
&& !t.IsEnum && !t.IsInterface
|
||||||
|
&& (t.IsClass || (t.IsValueType && !t.IsPrimitive)));
|
||||||
|
|
||||||
|
foreach (var type in messageTypes)
|
||||||
|
{
|
||||||
|
var cloneMethod = type.GetMethod("<Clone>$", BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
Assert.True(cloneMethod != null,
|
||||||
|
$"{type.FullName} in Messages namespace should be a record type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void KeyEntities_ShouldHaveDateTimeOffsetTimestamps()
|
||||||
|
{
|
||||||
|
// Spot-check key entity timestamp properties
|
||||||
|
var deploymentRecord = CommonsAssembly.GetType("ScadaLink.Commons.Entities.Deployment.DeploymentRecord");
|
||||||
|
Assert.NotNull(deploymentRecord);
|
||||||
|
Assert.Equal(typeof(DateTimeOffset), deploymentRecord!.GetProperty("DeployedAt")!.PropertyType);
|
||||||
|
Assert.Equal(typeof(DateTimeOffset?), deploymentRecord.GetProperty("CompletedAt")!.PropertyType);
|
||||||
|
|
||||||
|
var artifactRecord = CommonsAssembly.GetType("ScadaLink.Commons.Entities.Deployment.SystemArtifactDeploymentRecord");
|
||||||
|
Assert.NotNull(artifactRecord);
|
||||||
|
Assert.Equal(typeof(DateTimeOffset), artifactRecord!.GetProperty("DeployedAt")!.PropertyType);
|
||||||
|
|
||||||
|
var auditEntry = CommonsAssembly.GetType("ScadaLink.Commons.Entities.Audit.AuditLogEntry");
|
||||||
|
Assert.NotNull(auditEntry);
|
||||||
|
Assert.Equal(typeof(DateTimeOffset), auditEntry!.GetProperty("Timestamp")!.PropertyType);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Tests.Entities;
|
||||||
|
|
||||||
|
public class EntityConventionTests
|
||||||
|
{
|
||||||
|
private static readonly Assembly CommonsAssembly = typeof(ScadaLink.Commons.Types.RetryPolicy).Assembly;
|
||||||
|
|
||||||
|
private static IEnumerable<Type> GetEntityTypes() =>
|
||||||
|
CommonsAssembly.GetTypes()
|
||||||
|
.Where(t => t.IsClass && !t.IsAbstract && t.Namespace != null
|
||||||
|
&& t.Namespace.Contains(".Entities."));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllEntities_ShouldHaveNoEfAttributes()
|
||||||
|
{
|
||||||
|
var efAttributeNames = new HashSet<string>
|
||||||
|
{
|
||||||
|
"KeyAttribute", "ForeignKeyAttribute", "TableAttribute",
|
||||||
|
"ColumnAttribute", "RequiredAttribute", "MaxLengthAttribute",
|
||||||
|
"StringLengthAttribute", "DatabaseGeneratedAttribute",
|
||||||
|
"NotMappedAttribute", "IndexAttribute", "InversePropertyAttribute"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var entityType in GetEntityTypes())
|
||||||
|
{
|
||||||
|
// Check class-level attributes
|
||||||
|
var classAttrs = entityType.GetCustomAttributes(true);
|
||||||
|
foreach (var attr in classAttrs)
|
||||||
|
{
|
||||||
|
Assert.DoesNotContain(attr.GetType().Name, efAttributeNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check property-level attributes
|
||||||
|
foreach (var prop in entityType.GetProperties())
|
||||||
|
{
|
||||||
|
var propAttrs = prop.GetCustomAttributes(true);
|
||||||
|
foreach (var attr in propAttrs)
|
||||||
|
{
|
||||||
|
Assert.False(efAttributeNames.Contains(attr.GetType().Name),
|
||||||
|
$"Entity {entityType.Name}.{prop.Name} has EF attribute {attr.GetType().Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllTimestampProperties_ShouldBeDateTimeOffset()
|
||||||
|
{
|
||||||
|
var timestampNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
"Timestamp", "DeployedAt", "CompletedAt", "GeneratedAt",
|
||||||
|
"ReportTimestamp", "SnapshotTimestamp"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var entityType in GetEntityTypes())
|
||||||
|
{
|
||||||
|
foreach (var prop in entityType.GetProperties())
|
||||||
|
{
|
||||||
|
if (timestampNames.Contains(prop.Name))
|
||||||
|
{
|
||||||
|
var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||||
|
Assert.True(underlyingType == typeof(DateTimeOffset),
|
||||||
|
$"{entityType.Name}.{prop.Name} should be DateTimeOffset but is {prop.PropertyType.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NavigationProperties_ShouldBeICollection()
|
||||||
|
{
|
||||||
|
foreach (var entityType in GetEntityTypes())
|
||||||
|
{
|
||||||
|
foreach (var prop in entityType.GetProperties())
|
||||||
|
{
|
||||||
|
if (prop.PropertyType.IsGenericType &&
|
||||||
|
typeof(IEnumerable).IsAssignableFrom(prop.PropertyType) &&
|
||||||
|
prop.PropertyType != typeof(string))
|
||||||
|
{
|
||||||
|
Assert.True(
|
||||||
|
prop.PropertyType.GetGenericTypeDefinition() == typeof(ICollection<>),
|
||||||
|
$"{entityType.Name}.{prop.Name} should be ICollection<T> but is {prop.PropertyType.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
130
tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs
Normal file
130
tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Tests.Messages;
|
||||||
|
|
||||||
|
public class MessageConventionTests
|
||||||
|
{
|
||||||
|
private static readonly Assembly CommonsAssembly = typeof(ScadaLink.Commons.Types.RetryPolicy).Assembly;
|
||||||
|
|
||||||
|
private static IEnumerable<Type> GetMessageTypes() =>
|
||||||
|
CommonsAssembly.GetTypes()
|
||||||
|
.Where(t => t.Namespace != null
|
||||||
|
&& t.Namespace.Contains(".Messages.")
|
||||||
|
&& !t.IsEnum
|
||||||
|
&& !t.IsInterface
|
||||||
|
&& (t.IsClass || (t.IsValueType && !t.IsPrimitive)));
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllMessageTypes_ShouldBeRecords()
|
||||||
|
{
|
||||||
|
foreach (var type in GetMessageTypes())
|
||||||
|
{
|
||||||
|
// Records have a compiler-generated <Clone>$ method
|
||||||
|
var cloneMethod = type.GetMethod("<Clone>$", BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
Assert.True(cloneMethod != null,
|
||||||
|
$"{type.FullName} in Messages namespace should be a record type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllMessageTimestampProperties_ShouldBeDateTimeOffset()
|
||||||
|
{
|
||||||
|
foreach (var type in GetMessageTypes())
|
||||||
|
{
|
||||||
|
foreach (var prop in type.GetProperties())
|
||||||
|
{
|
||||||
|
if (prop.Name.Contains("Timestamp") || prop.Name == "GeneratedAt" || prop.Name == "DeployedAt")
|
||||||
|
{
|
||||||
|
var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType) ?? prop.PropertyType;
|
||||||
|
Assert.True(underlyingType == typeof(DateTimeOffset),
|
||||||
|
$"{type.Name}.{prop.Name} should be DateTimeOffset but is {prop.PropertyType.Name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonRoundTrip_DeployInstanceCommand_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var msg = new ScadaLink.Commons.Messages.Deployment.DeployInstanceCommand(
|
||||||
|
"dep-1", "instance-1", "abc123", "{}", "admin", DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(msg);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ScadaLink.Commons.Messages.Deployment.DeployInstanceCommand>(json);
|
||||||
|
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
Assert.Equal(msg.DeploymentId, deserialized!.DeploymentId);
|
||||||
|
Assert.Equal(msg.InstanceUniqueName, deserialized.InstanceUniqueName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonForwardCompatibility_UnknownField_ShouldDeserialize()
|
||||||
|
{
|
||||||
|
// Simulate a newer version with an extra field
|
||||||
|
var json = """{"DeploymentId":"dep-1","InstanceUniqueName":"inst-1","RevisionHash":"abc","FlattenedConfigurationJson":"{}","DeployedBy":"admin","Timestamp":"2025-01-01T00:00:00+00:00","NewField":"extra"}""";
|
||||||
|
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ScadaLink.Commons.Messages.Deployment.DeployInstanceCommand>(json);
|
||||||
|
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
Assert.Equal("dep-1", deserialized!.DeploymentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonBackwardCompatibility_MissingOptionalField_ShouldDeserialize()
|
||||||
|
{
|
||||||
|
// DeploymentStatusResponse has nullable ErrorMessage
|
||||||
|
var json = """{"DeploymentId":"dep-1","InstanceUniqueName":"inst-1","Status":2,"Timestamp":"2025-01-01T00:00:00+00:00"}""";
|
||||||
|
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ScadaLink.Commons.Messages.Deployment.DeploymentStatusResponse>(json);
|
||||||
|
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
Assert.Equal("dep-1", deserialized!.DeploymentId);
|
||||||
|
Assert.Null(deserialized.ErrorMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonRoundTrip_SiteHealthReport_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var msg = new ScadaLink.Commons.Messages.Health.SiteHealthReport(
|
||||||
|
"site-1", 1, DateTimeOffset.UtcNow,
|
||||||
|
new Dictionary<string, ScadaLink.Commons.Types.Enums.ConnectionHealth>
|
||||||
|
{
|
||||||
|
["conn1"] = ScadaLink.Commons.Types.Enums.ConnectionHealth.Connected
|
||||||
|
},
|
||||||
|
new Dictionary<string, ScadaLink.Commons.Messages.Health.TagResolutionStatus>
|
||||||
|
{
|
||||||
|
["conn1"] = new(10, 8)
|
||||||
|
},
|
||||||
|
0, 0,
|
||||||
|
new Dictionary<string, int> { ["queue1"] = 5 },
|
||||||
|
0);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(msg);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ScadaLink.Commons.Messages.Health.SiteHealthReport>(json);
|
||||||
|
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
Assert.Equal("site-1", deserialized!.SiteId);
|
||||||
|
Assert.Equal(1, deserialized.SequenceNumber);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void JsonRoundTrip_DeployArtifactsCommand_ShouldSucceed()
|
||||||
|
{
|
||||||
|
var msg = new ScadaLink.Commons.Messages.Artifacts.DeployArtifactsCommand(
|
||||||
|
"dep-1",
|
||||||
|
new List<ScadaLink.Commons.Messages.Artifacts.SharedScriptArtifact>
|
||||||
|
{
|
||||||
|
new("script1", "code", null, null)
|
||||||
|
},
|
||||||
|
null, null, null,
|
||||||
|
DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
|
var json = JsonSerializer.Serialize(msg);
|
||||||
|
var deserialized = JsonSerializer.Deserialize<ScadaLink.Commons.Messages.Artifacts.DeployArtifactsCommand>(json);
|
||||||
|
|
||||||
|
Assert.NotNull(deserialized);
|
||||||
|
Assert.Equal("dep-1", deserialized!.DeploymentId);
|
||||||
|
Assert.Single(deserialized.SharedScripts!);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
tests/ScadaLink.Commons.Tests/Types/EnumTests.cs
Normal file
36
tests/ScadaLink.Commons.Tests/Types/EnumTests.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using ScadaLink.Commons.Types.Enums;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Tests.Types;
|
||||||
|
|
||||||
|
public class EnumTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(DataType), new[] { "Boolean", "Int32", "Float", "Double", "String", "DateTime", "Binary" })]
|
||||||
|
[InlineData(typeof(InstanceState), new[] { "Enabled", "Disabled" })]
|
||||||
|
[InlineData(typeof(DeploymentStatus), new[] { "Pending", "InProgress", "Success", "Failed" })]
|
||||||
|
[InlineData(typeof(AlarmState), new[] { "Active", "Normal" })]
|
||||||
|
[InlineData(typeof(AlarmTriggerType), new[] { "ValueMatch", "RangeViolation", "RateOfChange" })]
|
||||||
|
[InlineData(typeof(ConnectionHealth), new[] { "Connected", "Disconnected", "Connecting", "Error" })]
|
||||||
|
public void Enum_ShouldHaveExpectedValues(Type enumType, string[] expectedNames)
|
||||||
|
{
|
||||||
|
var actualNames = Enum.GetNames(enumType);
|
||||||
|
Assert.Equal(expectedNames, actualNames);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(typeof(DataType))]
|
||||||
|
[InlineData(typeof(InstanceState))]
|
||||||
|
[InlineData(typeof(DeploymentStatus))]
|
||||||
|
[InlineData(typeof(AlarmState))]
|
||||||
|
[InlineData(typeof(AlarmTriggerType))]
|
||||||
|
[InlineData(typeof(ConnectionHealth))]
|
||||||
|
public void Enum_ShouldBeSingularNamed(Type enumType)
|
||||||
|
{
|
||||||
|
// Singular names should not end with 's' (except 'Status' which is singular)
|
||||||
|
var name = enumType.Name;
|
||||||
|
Assert.False(name.EndsWith("es") && !name.EndsWith("Status"),
|
||||||
|
$"Enum {name} appears to be plural (ends with 'es').");
|
||||||
|
Assert.False(name.EndsWith("s") && !name.EndsWith("ss") && !name.EndsWith("us"),
|
||||||
|
$"Enum {name} appears to be plural (ends with 's').");
|
||||||
|
}
|
||||||
|
}
|
||||||
75
tests/ScadaLink.Commons.Tests/Types/ResultTests.cs
Normal file
75
tests/ScadaLink.Commons.Tests/Types/ResultTests.cs
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
using ScadaLink.Commons.Types;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Tests.Types;
|
||||||
|
|
||||||
|
public class ResultTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Success_ShouldCreateSuccessfulResult()
|
||||||
|
{
|
||||||
|
var result = Result<int>.Success(42);
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.False(result.IsFailure);
|
||||||
|
Assert.Equal(42, result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Failure_ShouldCreateFailedResult()
|
||||||
|
{
|
||||||
|
var result = Result<int>.Failure("something went wrong");
|
||||||
|
|
||||||
|
Assert.True(result.IsFailure);
|
||||||
|
Assert.False(result.IsSuccess);
|
||||||
|
Assert.Equal("something went wrong", result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Value_OnFailure_ShouldThrow()
|
||||||
|
{
|
||||||
|
var result = Result<int>.Failure("error");
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => result.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Error_OnSuccess_ShouldThrow()
|
||||||
|
{
|
||||||
|
var result = Result<int>.Success(42);
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => result.Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Match_OnSuccess_ShouldCallOnSuccess()
|
||||||
|
{
|
||||||
|
var result = Result<int>.Success(42);
|
||||||
|
|
||||||
|
var output = result.Match(
|
||||||
|
v => $"value={v}",
|
||||||
|
e => $"error={e}");
|
||||||
|
|
||||||
|
Assert.Equal("value=42", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Match_OnFailure_ShouldCallOnFailure()
|
||||||
|
{
|
||||||
|
var result = Result<int>.Failure("bad");
|
||||||
|
|
||||||
|
var output = result.Match(
|
||||||
|
v => $"value={v}",
|
||||||
|
e => $"error={e}");
|
||||||
|
|
||||||
|
Assert.Equal("error=bad", output);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Success_WithNullableReferenceType_ShouldWork()
|
||||||
|
{
|
||||||
|
var result = Result<string>.Success("hello");
|
||||||
|
|
||||||
|
Assert.True(result.IsSuccess);
|
||||||
|
Assert.Equal("hello", result.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
35
tests/ScadaLink.Commons.Tests/Types/RetryPolicyTests.cs
Normal file
35
tests/ScadaLink.Commons.Tests/Types/RetryPolicyTests.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using ScadaLink.Commons.Types;
|
||||||
|
|
||||||
|
namespace ScadaLink.Commons.Tests.Types;
|
||||||
|
|
||||||
|
public class RetryPolicyTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void RetryPolicy_ShouldBeImmutableRecord()
|
||||||
|
{
|
||||||
|
var policy = new RetryPolicy(3, TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
Assert.Equal(3, policy.MaxRetries);
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(5), policy.Delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RetryPolicy_WithExpression_ShouldCreateNewInstance()
|
||||||
|
{
|
||||||
|
var original = new RetryPolicy(3, TimeSpan.FromSeconds(5));
|
||||||
|
var modified = original with { MaxRetries = 5 };
|
||||||
|
|
||||||
|
Assert.Equal(3, original.MaxRetries);
|
||||||
|
Assert.Equal(5, modified.MaxRetries);
|
||||||
|
Assert.Equal(original.Delay, modified.Delay);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void RetryPolicy_EqualValues_ShouldBeEqual()
|
||||||
|
{
|
||||||
|
var a = new RetryPolicy(3, TimeSpan.FromSeconds(5));
|
||||||
|
var b = new RetryPolicy(3, TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
Assert.Equal(a, b);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
namespace ScadaLink.Commons.Tests;
|
|
||||||
|
|
||||||
public class UnitTest1
|
|
||||||
{
|
|
||||||
[Fact]
|
|
||||||
public void Test1()
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user