diff --git a/src/ScadaLink.Commons/Class1.cs b/src/ScadaLink.Commons/Class1.cs
deleted file mode 100644
index 68ec3ce..0000000
--- a/src/ScadaLink.Commons/Class1.cs
+++ /dev/null
@@ -1,6 +0,0 @@
-namespace ScadaLink.Commons;
-
-public class Class1
-{
-
-}
diff --git a/src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs b/src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs
new file mode 100644
index 0000000..5f0e344
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Deployment/DeploymentRecord.cs b/src/ScadaLink.Commons/Entities/Deployment/DeploymentRecord.cs
new file mode 100644
index 0000000..d0c95ed
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Deployment/DeploymentRecord.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Deployment/SystemArtifactDeploymentRecord.cs b/src/ScadaLink.Commons/Entities/Deployment/SystemArtifactDeploymentRecord.cs
new file mode 100644
index 0000000..c73fdf1
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Deployment/SystemArtifactDeploymentRecord.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/ExternalSystems/DatabaseConnectionDefinition.cs b/src/ScadaLink.Commons/Entities/ExternalSystems/DatabaseConnectionDefinition.cs
new file mode 100644
index 0000000..c6f58de
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/ExternalSystems/DatabaseConnectionDefinition.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemDefinition.cs b/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemDefinition.cs
new file mode 100644
index 0000000..49c46b5
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemDefinition.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemMethod.cs b/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemMethod.cs
new file mode 100644
index 0000000..fb84a82
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/ExternalSystems/ExternalSystemMethod.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/InboundApi/ApiKey.cs b/src/ScadaLink.Commons/Entities/InboundApi/ApiKey.cs
new file mode 100644
index 0000000..7d5fd7c
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/InboundApi/ApiKey.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/InboundApi/ApiMethod.cs b/src/ScadaLink.Commons/Entities/InboundApi/ApiMethod.cs
new file mode 100644
index 0000000..9058b67
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/InboundApi/ApiMethod.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Instances/Area.cs b/src/ScadaLink.Commons/Entities/Instances/Area.cs
new file mode 100644
index 0000000..bcd14e8
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Instances/Area.cs
@@ -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 Children { get; set; } = new List();
+
+ public Area(string name)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Instances/Instance.cs b/src/ScadaLink.Commons/Entities/Instances/Instance.cs
new file mode 100644
index 0000000..e1c2ca6
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Instances/Instance.cs
@@ -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 AttributeOverrides { get; set; } = new List();
+ public ICollection ConnectionBindings { get; set; } = new List();
+
+ public Instance(string uniqueName)
+ {
+ UniqueName = uniqueName ?? throw new ArgumentNullException(nameof(uniqueName));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Instances/InstanceAttributeOverride.cs b/src/ScadaLink.Commons/Entities/Instances/InstanceAttributeOverride.cs
new file mode 100644
index 0000000..bd59428
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Instances/InstanceAttributeOverride.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Instances/InstanceConnectionBinding.cs b/src/ScadaLink.Commons/Entities/Instances/InstanceConnectionBinding.cs
new file mode 100644
index 0000000..6dc027b
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Instances/InstanceConnectionBinding.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Notifications/NotificationList.cs b/src/ScadaLink.Commons/Entities/Notifications/NotificationList.cs
new file mode 100644
index 0000000..1c78946
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Notifications/NotificationList.cs
@@ -0,0 +1,13 @@
+namespace ScadaLink.Commons.Entities.Notifications;
+
+public class NotificationList
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public ICollection Recipients { get; set; } = new List();
+
+ public NotificationList(string name)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Notifications/NotificationRecipient.cs b/src/ScadaLink.Commons/Entities/Notifications/NotificationRecipient.cs
new file mode 100644
index 0000000..706d047
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Notifications/NotificationRecipient.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Notifications/SmtpConfiguration.cs b/src/ScadaLink.Commons/Entities/Notifications/SmtpConfiguration.cs
new file mode 100644
index 0000000..0481ef8
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Notifications/SmtpConfiguration.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Scripts/SharedScript.cs b/src/ScadaLink.Commons/Entities/Scripts/SharedScript.cs
new file mode 100644
index 0000000..ede07e5
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Scripts/SharedScript.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Security/LdapGroupMapping.cs b/src/ScadaLink.Commons/Entities/Security/LdapGroupMapping.cs
new file mode 100644
index 0000000..c6c5827
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Security/LdapGroupMapping.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Security/SiteScopeRule.cs b/src/ScadaLink.Commons/Entities/Security/SiteScopeRule.cs
new file mode 100644
index 0000000..06e92b9
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Security/SiteScopeRule.cs
@@ -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; }
+}
diff --git a/src/ScadaLink.Commons/Entities/Sites/DataConnection.cs b/src/ScadaLink.Commons/Entities/Sites/DataConnection.cs
new file mode 100644
index 0000000..397cf38
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Sites/DataConnection.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Sites/Site.cs b/src/ScadaLink.Commons/Entities/Sites/Site.cs
new file mode 100644
index 0000000..d953e90
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Sites/Site.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Sites/SiteDataConnectionAssignment.cs b/src/ScadaLink.Commons/Entities/Sites/SiteDataConnectionAssignment.cs
new file mode 100644
index 0000000..2caca44
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Sites/SiteDataConnectionAssignment.cs
@@ -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; }
+}
diff --git a/src/ScadaLink.Commons/Entities/Templates/Template.cs b/src/ScadaLink.Commons/Entities/Templates/Template.cs
new file mode 100644
index 0000000..35633ac
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Templates/Template.cs
@@ -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 Attributes { get; set; } = new List();
+ public ICollection Alarms { get; set; } = new List();
+ public ICollection Scripts { get; set; } = new List();
+ public ICollection Compositions { get; set; } = new List();
+
+ public Template(string name)
+ {
+ Name = name ?? throw new ArgumentNullException(nameof(name));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Templates/TemplateAlarm.cs b/src/ScadaLink.Commons/Entities/Templates/TemplateAlarm.cs
new file mode 100644
index 0000000..fe79fb9
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Templates/TemplateAlarm.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Templates/TemplateAttribute.cs b/src/ScadaLink.Commons/Entities/Templates/TemplateAttribute.cs
new file mode 100644
index 0000000..f3fb9ef
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Templates/TemplateAttribute.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Templates/TemplateComposition.cs b/src/ScadaLink.Commons/Entities/Templates/TemplateComposition.cs
new file mode 100644
index 0000000..2c6e536
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Templates/TemplateComposition.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Entities/Templates/TemplateScript.cs b/src/ScadaLink.Commons/Entities/Templates/TemplateScript.cs
new file mode 100644
index 0000000..896e2fa
--- /dev/null
+++ b/src/ScadaLink.Commons/Entities/Templates/TemplateScript.cs
@@ -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));
+ }
+}
diff --git a/src/ScadaLink.Commons/Interfaces/Protocol/IDataConnection.cs b/src/ScadaLink.Commons/Interfaces/Protocol/IDataConnection.cs
new file mode 100644
index 0000000..7d959b6
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Protocol/IDataConnection.cs
@@ -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 connectionDetails, CancellationToken cancellationToken = default);
+ Task DisconnectAsync(CancellationToken cancellationToken = default);
+ Task SubscribeAsync(string tagPath, SubscriptionCallback callback, CancellationToken cancellationToken = default);
+ Task UnsubscribeAsync(string subscriptionId, CancellationToken cancellationToken = default);
+ Task ReadAsync(string tagPath, CancellationToken cancellationToken = default);
+ Task> ReadBatchAsync(IEnumerable tagPaths, CancellationToken cancellationToken = default);
+ Task WriteAsync(string tagPath, object? value, CancellationToken cancellationToken = default);
+ Task> WriteBatchAsync(IDictionary values, CancellationToken cancellationToken = default);
+ Task WriteBatchAndWaitAsync(IDictionary values, string flagPath, object? flagValue, string responsePath, object? responseValue, TimeSpan timeout, CancellationToken cancellationToken = default);
+ ConnectionHealth Status { get; }
+}
diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/ICentralUiRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/ICentralUiRepository.cs
new file mode 100644
index 0000000..753c3aa
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Repositories/ICentralUiRepository.cs
@@ -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> GetAllSitesAsync(CancellationToken cancellationToken = default);
+ Task> GetDataConnectionsBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
+ Task> GetAllSiteDataConnectionAssignmentsAsync(CancellationToken cancellationToken = default);
+ Task> GetTemplateTreeAsync(CancellationToken cancellationToken = default);
+ Task> GetInstancesFilteredAsync(int? siteId = null, int? templateId = null, string? searchTerm = null, CancellationToken cancellationToken = default);
+ Task> GetRecentDeploymentsAsync(int count, CancellationToken cancellationToken = default);
+ Task> GetAreaTreeBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
+
+ Task SaveChangesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/IDeploymentManagerRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/IDeploymentManagerRepository.cs
new file mode 100644
index 0000000..ebe597a
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Repositories/IDeploymentManagerRepository.cs
@@ -0,0 +1,26 @@
+using ScadaLink.Commons.Entities.Deployment;
+using ScadaLink.Commons.Types.Enums;
+
+namespace ScadaLink.Commons.Interfaces.Repositories;
+
+public interface IDeploymentManagerRepository
+{
+ // DeploymentRecord
+ Task GetDeploymentRecordByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> GetAllDeploymentRecordsAsync(CancellationToken cancellationToken = default);
+ Task> GetDeploymentsByInstanceIdAsync(int instanceId, CancellationToken cancellationToken = default);
+ Task GetCurrentDeploymentStatusAsync(int instanceId, CancellationToken cancellationToken = default);
+ Task 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 GetSystemArtifactDeploymentByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 SaveChangesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/IExternalSystemRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/IExternalSystemRepository.cs
new file mode 100644
index 0000000..0410030
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Repositories/IExternalSystemRepository.cs
@@ -0,0 +1,29 @@
+using ScadaLink.Commons.Entities.ExternalSystems;
+
+namespace ScadaLink.Commons.Interfaces.Repositories;
+
+public interface IExternalSystemRepository
+{
+ // ExternalSystemDefinition
+ Task GetExternalSystemByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 GetExternalSystemMethodByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 GetDatabaseConnectionByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 SaveChangesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/IInboundApiRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/IInboundApiRepository.cs
new file mode 100644
index 0000000..bef4ef6
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Repositories/IInboundApiRepository.cs
@@ -0,0 +1,25 @@
+using ScadaLink.Commons.Entities.InboundApi;
+
+namespace ScadaLink.Commons.Interfaces.Repositories;
+
+public interface IInboundApiRepository
+{
+ // ApiKey
+ Task GetApiKeyByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> GetAllApiKeysAsync(CancellationToken cancellationToken = default);
+ Task 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 GetApiMethodByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> GetAllApiMethodsAsync(CancellationToken cancellationToken = default);
+ Task GetMethodByNameAsync(string name, CancellationToken cancellationToken = default);
+ Task> 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 SaveChangesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/INotificationRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/INotificationRepository.cs
new file mode 100644
index 0000000..512e85d
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Repositories/INotificationRepository.cs
@@ -0,0 +1,30 @@
+using ScadaLink.Commons.Entities.Notifications;
+
+namespace ScadaLink.Commons.Interfaces.Repositories;
+
+public interface INotificationRepository
+{
+ // NotificationList
+ Task GetNotificationListByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> GetAllNotificationListsAsync(CancellationToken cancellationToken = default);
+ Task 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 GetRecipientByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 GetSmtpConfigurationByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 SaveChangesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/ISecurityRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/ISecurityRepository.cs
new file mode 100644
index 0000000..c9e7ccb
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Repositories/ISecurityRepository.cs
@@ -0,0 +1,23 @@
+using ScadaLink.Commons.Entities.Security;
+
+namespace ScadaLink.Commons.Interfaces.Repositories;
+
+public interface ISecurityRepository
+{
+ // LdapGroupMapping
+ Task GetMappingByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> GetAllMappingsAsync(CancellationToken cancellationToken = default);
+ Task> 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 GetScopeRuleByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 SaveChangesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/ScadaLink.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs b/src/ScadaLink.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs
new file mode 100644
index 0000000..ceedde9
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Repositories/ITemplateEngineRepository.cs
@@ -0,0 +1,74 @@
+using ScadaLink.Commons.Entities.Instances;
+using ScadaLink.Commons.Entities.Templates;
+
+namespace ScadaLink.Commons.Interfaces.Repositories;
+
+public interface ITemplateEngineRepository
+{
+ // Template
+ Task GetTemplateByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task GetTemplateWithChildrenAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 GetTemplateAttributeByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 GetTemplateAlarmByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 GetTemplateScriptByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 GetTemplateCompositionByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 GetInstanceByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> GetAllInstancesAsync(CancellationToken cancellationToken = default);
+ Task> GetInstancesByTemplateIdAsync(int templateId, CancellationToken cancellationToken = default);
+ Task> GetInstancesBySiteIdAsync(int siteId, CancellationToken cancellationToken = default);
+ Task 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> 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> 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 GetAreaByIdAsync(int id, CancellationToken cancellationToken = default);
+ Task> 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 SaveChangesAsync(CancellationToken cancellationToken = default);
+}
diff --git a/src/ScadaLink.Commons/Interfaces/Services/IAuditService.cs b/src/ScadaLink.Commons/Interfaces/Services/IAuditService.cs
new file mode 100644
index 0000000..984bc3f
--- /dev/null
+++ b/src/ScadaLink.Commons/Interfaces/Services/IAuditService.cs
@@ -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);
+}
diff --git a/src/ScadaLink.Commons/Messages/Artifacts/ArtifactDeploymentResponse.cs b/src/ScadaLink.Commons/Messages/Artifacts/ArtifactDeploymentResponse.cs
new file mode 100644
index 0000000..ab7c578
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Artifacts/ArtifactDeploymentResponse.cs
@@ -0,0 +1,8 @@
+namespace ScadaLink.Commons.Messages.Artifacts;
+
+public record ArtifactDeploymentResponse(
+ string DeploymentId,
+ string SiteId,
+ bool Success,
+ string? ErrorMessage,
+ DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.Commons/Messages/Artifacts/DatabaseConnectionArtifact.cs b/src/ScadaLink.Commons/Messages/Artifacts/DatabaseConnectionArtifact.cs
new file mode 100644
index 0000000..7d95d81
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Artifacts/DatabaseConnectionArtifact.cs
@@ -0,0 +1,7 @@
+namespace ScadaLink.Commons.Messages.Artifacts;
+
+public record DatabaseConnectionArtifact(
+ string Name,
+ string ConnectionString,
+ int MaxRetries,
+ TimeSpan RetryDelay);
diff --git a/src/ScadaLink.Commons/Messages/Artifacts/DeployArtifactsCommand.cs b/src/ScadaLink.Commons/Messages/Artifacts/DeployArtifactsCommand.cs
new file mode 100644
index 0000000..db0db25
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Artifacts/DeployArtifactsCommand.cs
@@ -0,0 +1,9 @@
+namespace ScadaLink.Commons.Messages.Artifacts;
+
+public record DeployArtifactsCommand(
+ string DeploymentId,
+ IReadOnlyList? SharedScripts,
+ IReadOnlyList? ExternalSystems,
+ IReadOnlyList? DatabaseConnections,
+ IReadOnlyList? NotificationLists,
+ DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.Commons/Messages/Artifacts/ExternalSystemArtifact.cs b/src/ScadaLink.Commons/Messages/Artifacts/ExternalSystemArtifact.cs
new file mode 100644
index 0000000..f212ae8
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Artifacts/ExternalSystemArtifact.cs
@@ -0,0 +1,8 @@
+namespace ScadaLink.Commons.Messages.Artifacts;
+
+public record ExternalSystemArtifact(
+ string Name,
+ string EndpointUrl,
+ string AuthType,
+ string? AuthConfiguration,
+ string? MethodDefinitionsJson);
diff --git a/src/ScadaLink.Commons/Messages/Artifacts/NotificationListArtifact.cs b/src/ScadaLink.Commons/Messages/Artifacts/NotificationListArtifact.cs
new file mode 100644
index 0000000..36a4d9a
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Artifacts/NotificationListArtifact.cs
@@ -0,0 +1,5 @@
+namespace ScadaLink.Commons.Messages.Artifacts;
+
+public record NotificationListArtifact(
+ string Name,
+ IReadOnlyList RecipientEmails);
diff --git a/src/ScadaLink.Commons/Messages/Artifacts/SharedScriptArtifact.cs b/src/ScadaLink.Commons/Messages/Artifacts/SharedScriptArtifact.cs
new file mode 100644
index 0000000..20e2e61
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Artifacts/SharedScriptArtifact.cs
@@ -0,0 +1,7 @@
+namespace ScadaLink.Commons.Messages.Artifacts;
+
+public record SharedScriptArtifact(
+ string Name,
+ string Code,
+ string? ParameterDefinitions,
+ string? ReturnDefinition);
diff --git a/src/ScadaLink.Commons/Messages/Communication/ConnectionStateChanged.cs b/src/ScadaLink.Commons/Messages/Communication/ConnectionStateChanged.cs
new file mode 100644
index 0000000..ab06763
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Communication/ConnectionStateChanged.cs
@@ -0,0 +1,6 @@
+namespace ScadaLink.Commons.Messages.Communication;
+
+public record ConnectionStateChanged(
+ string SiteId,
+ bool IsConnected,
+ DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.Commons/Messages/Communication/RoutingMetadata.cs b/src/ScadaLink.Commons/Messages/Communication/RoutingMetadata.cs
new file mode 100644
index 0000000..1801ae4
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Communication/RoutingMetadata.cs
@@ -0,0 +1,6 @@
+namespace ScadaLink.Commons.Messages.Communication;
+
+public record RoutingMetadata(
+ string TargetSiteId,
+ string? TargetInstanceUniqueName,
+ string CorrelationId);
diff --git a/src/ScadaLink.Commons/Messages/Communication/SiteIdentity.cs b/src/ScadaLink.Commons/Messages/Communication/SiteIdentity.cs
new file mode 100644
index 0000000..8e7fc63
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Communication/SiteIdentity.cs
@@ -0,0 +1,6 @@
+namespace ScadaLink.Commons.Messages.Communication;
+
+public record SiteIdentity(
+ string SiteId,
+ string NodeHostname,
+ bool IsActive);
diff --git a/src/ScadaLink.Commons/Messages/DebugView/DebugViewSnapshot.cs b/src/ScadaLink.Commons/Messages/DebugView/DebugViewSnapshot.cs
new file mode 100644
index 0000000..a09c6f9
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/DebugView/DebugViewSnapshot.cs
@@ -0,0 +1,9 @@
+using ScadaLink.Commons.Messages.Streaming;
+
+namespace ScadaLink.Commons.Messages.DebugView;
+
+public record DebugViewSnapshot(
+ string InstanceUniqueName,
+ IReadOnlyList AttributeValues,
+ IReadOnlyList AlarmStates,
+ DateTimeOffset SnapshotTimestamp);
diff --git a/src/ScadaLink.Commons/Messages/DebugView/SubscribeDebugViewRequest.cs b/src/ScadaLink.Commons/Messages/DebugView/SubscribeDebugViewRequest.cs
new file mode 100644
index 0000000..7bca95e
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/DebugView/SubscribeDebugViewRequest.cs
@@ -0,0 +1,5 @@
+namespace ScadaLink.Commons.Messages.DebugView;
+
+public record SubscribeDebugViewRequest(
+ string InstanceUniqueName,
+ string CorrelationId);
diff --git a/src/ScadaLink.Commons/Messages/DebugView/UnsubscribeDebugViewRequest.cs b/src/ScadaLink.Commons/Messages/DebugView/UnsubscribeDebugViewRequest.cs
new file mode 100644
index 0000000..2e6fd83
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/DebugView/UnsubscribeDebugViewRequest.cs
@@ -0,0 +1,5 @@
+namespace ScadaLink.Commons.Messages.DebugView;
+
+public record UnsubscribeDebugViewRequest(
+ string InstanceUniqueName,
+ string CorrelationId);
diff --git a/src/ScadaLink.Commons/Messages/Deployment/DeployInstanceCommand.cs b/src/ScadaLink.Commons/Messages/Deployment/DeployInstanceCommand.cs
new file mode 100644
index 0000000..841ed01
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Deployment/DeployInstanceCommand.cs
@@ -0,0 +1,9 @@
+namespace ScadaLink.Commons.Messages.Deployment;
+
+public record DeployInstanceCommand(
+ string DeploymentId,
+ string InstanceUniqueName,
+ string RevisionHash,
+ string FlattenedConfigurationJson,
+ string DeployedBy,
+ DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.Commons/Messages/Deployment/DeploymentStatusResponse.cs b/src/ScadaLink.Commons/Messages/Deployment/DeploymentStatusResponse.cs
new file mode 100644
index 0000000..95ef59d
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Deployment/DeploymentStatusResponse.cs
@@ -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);
diff --git a/src/ScadaLink.Commons/Messages/Deployment/DeploymentValidationResult.cs b/src/ScadaLink.Commons/Messages/Deployment/DeploymentValidationResult.cs
new file mode 100644
index 0000000..eb6d0b9
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Deployment/DeploymentValidationResult.cs
@@ -0,0 +1,6 @@
+namespace ScadaLink.Commons.Messages.Deployment;
+
+public record DeploymentValidationResult(
+ bool IsValid,
+ IReadOnlyList Errors,
+ IReadOnlyList Warnings);
diff --git a/src/ScadaLink.Commons/Messages/Deployment/FlattenedConfigurationSnapshot.cs b/src/ScadaLink.Commons/Messages/Deployment/FlattenedConfigurationSnapshot.cs
new file mode 100644
index 0000000..f6c4c11
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Deployment/FlattenedConfigurationSnapshot.cs
@@ -0,0 +1,7 @@
+namespace ScadaLink.Commons.Messages.Deployment;
+
+public record FlattenedConfigurationSnapshot(
+ string InstanceUniqueName,
+ string RevisionHash,
+ string ConfigurationJson,
+ DateTimeOffset GeneratedAt);
diff --git a/src/ScadaLink.Commons/Messages/Health/HeartbeatMessage.cs b/src/ScadaLink.Commons/Messages/Health/HeartbeatMessage.cs
new file mode 100644
index 0000000..4c53e74
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Health/HeartbeatMessage.cs
@@ -0,0 +1,7 @@
+namespace ScadaLink.Commons.Messages.Health;
+
+public record HeartbeatMessage(
+ string SiteId,
+ string NodeHostname,
+ bool IsActive,
+ DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs b/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs
new file mode 100644
index 0000000..bdd8af1
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Health/SiteHealthReport.cs
@@ -0,0 +1,14 @@
+using ScadaLink.Commons.Types.Enums;
+
+namespace ScadaLink.Commons.Messages.Health;
+
+public record SiteHealthReport(
+ string SiteId,
+ long SequenceNumber,
+ DateTimeOffset ReportTimestamp,
+ IReadOnlyDictionary DataConnectionStatuses,
+ IReadOnlyDictionary TagResolutionCounts,
+ int ScriptErrorCount,
+ int AlarmEvaluationErrorCount,
+ IReadOnlyDictionary StoreAndForwardBufferDepths,
+ int DeadLetterCount);
diff --git a/src/ScadaLink.Commons/Messages/Health/TagResolutionStatus.cs b/src/ScadaLink.Commons/Messages/Health/TagResolutionStatus.cs
new file mode 100644
index 0000000..f07030b
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Health/TagResolutionStatus.cs
@@ -0,0 +1,3 @@
+namespace ScadaLink.Commons.Messages.Health;
+
+public record TagResolutionStatus(int TotalSubscribed, int SuccessfullyResolved);
diff --git a/src/ScadaLink.Commons/Messages/Lifecycle/DeleteInstanceCommand.cs b/src/ScadaLink.Commons/Messages/Lifecycle/DeleteInstanceCommand.cs
new file mode 100644
index 0000000..f4e2b65
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Lifecycle/DeleteInstanceCommand.cs
@@ -0,0 +1,6 @@
+namespace ScadaLink.Commons.Messages.Lifecycle;
+
+public record DeleteInstanceCommand(
+ string CommandId,
+ string InstanceUniqueName,
+ DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.Commons/Messages/Lifecycle/DisableInstanceCommand.cs b/src/ScadaLink.Commons/Messages/Lifecycle/DisableInstanceCommand.cs
new file mode 100644
index 0000000..1e6e0af
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Lifecycle/DisableInstanceCommand.cs
@@ -0,0 +1,6 @@
+namespace ScadaLink.Commons.Messages.Lifecycle;
+
+public record DisableInstanceCommand(
+ string CommandId,
+ string InstanceUniqueName,
+ DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.Commons/Messages/Lifecycle/EnableInstanceCommand.cs b/src/ScadaLink.Commons/Messages/Lifecycle/EnableInstanceCommand.cs
new file mode 100644
index 0000000..a85c7a3
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Lifecycle/EnableInstanceCommand.cs
@@ -0,0 +1,6 @@
+namespace ScadaLink.Commons.Messages.Lifecycle;
+
+public record EnableInstanceCommand(
+ string CommandId,
+ string InstanceUniqueName,
+ DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.Commons/Messages/Lifecycle/InstanceLifecycleResponse.cs b/src/ScadaLink.Commons/Messages/Lifecycle/InstanceLifecycleResponse.cs
new file mode 100644
index 0000000..db05295
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Lifecycle/InstanceLifecycleResponse.cs
@@ -0,0 +1,8 @@
+namespace ScadaLink.Commons.Messages.Lifecycle;
+
+public record InstanceLifecycleResponse(
+ string CommandId,
+ string InstanceUniqueName,
+ bool Success,
+ string? ErrorMessage,
+ DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallRequest.cs b/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallRequest.cs
new file mode 100644
index 0000000..5d06582
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallRequest.cs
@@ -0,0 +1,7 @@
+namespace ScadaLink.Commons.Messages.ScriptExecution;
+
+public record ScriptCallRequest(
+ string ScriptName,
+ IReadOnlyDictionary? Parameters,
+ int CurrentCallDepth,
+ string CorrelationId);
diff --git a/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallResult.cs b/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallResult.cs
new file mode 100644
index 0000000..00412dd
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/ScriptExecution/ScriptCallResult.cs
@@ -0,0 +1,7 @@
+namespace ScadaLink.Commons.Messages.ScriptExecution;
+
+public record ScriptCallResult(
+ string CorrelationId,
+ bool Success,
+ object? ReturnValue,
+ string? ErrorMessage);
diff --git a/src/ScadaLink.Commons/Messages/Streaming/AlarmStateChanged.cs b/src/ScadaLink.Commons/Messages/Streaming/AlarmStateChanged.cs
new file mode 100644
index 0000000..ad7de6e
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Streaming/AlarmStateChanged.cs
@@ -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);
diff --git a/src/ScadaLink.Commons/Messages/Streaming/AttributeValueChanged.cs b/src/ScadaLink.Commons/Messages/Streaming/AttributeValueChanged.cs
new file mode 100644
index 0000000..64680c9
--- /dev/null
+++ b/src/ScadaLink.Commons/Messages/Streaming/AttributeValueChanged.cs
@@ -0,0 +1,9 @@
+namespace ScadaLink.Commons.Messages.Streaming;
+
+public record AttributeValueChanged(
+ string InstanceUniqueName,
+ string AttributePath,
+ string AttributeName,
+ object? Value,
+ string Quality,
+ DateTimeOffset Timestamp);
diff --git a/src/ScadaLink.Commons/Types/Enums/AlarmState.cs b/src/ScadaLink.Commons/Types/Enums/AlarmState.cs
new file mode 100644
index 0000000..1df94bb
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/Enums/AlarmState.cs
@@ -0,0 +1,7 @@
+namespace ScadaLink.Commons.Types.Enums;
+
+public enum AlarmState
+{
+ Active,
+ Normal
+}
diff --git a/src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs b/src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs
new file mode 100644
index 0000000..314e006
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/Enums/AlarmTriggerType.cs
@@ -0,0 +1,8 @@
+namespace ScadaLink.Commons.Types.Enums;
+
+public enum AlarmTriggerType
+{
+ ValueMatch,
+ RangeViolation,
+ RateOfChange
+}
diff --git a/src/ScadaLink.Commons/Types/Enums/ConnectionHealth.cs b/src/ScadaLink.Commons/Types/Enums/ConnectionHealth.cs
new file mode 100644
index 0000000..03fee5c
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/Enums/ConnectionHealth.cs
@@ -0,0 +1,9 @@
+namespace ScadaLink.Commons.Types.Enums;
+
+public enum ConnectionHealth
+{
+ Connected,
+ Disconnected,
+ Connecting,
+ Error
+}
diff --git a/src/ScadaLink.Commons/Types/Enums/DataType.cs b/src/ScadaLink.Commons/Types/Enums/DataType.cs
new file mode 100644
index 0000000..3ae2dbf
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/Enums/DataType.cs
@@ -0,0 +1,12 @@
+namespace ScadaLink.Commons.Types.Enums;
+
+public enum DataType
+{
+ Boolean,
+ Int32,
+ Float,
+ Double,
+ String,
+ DateTime,
+ Binary
+}
diff --git a/src/ScadaLink.Commons/Types/Enums/DeploymentStatus.cs b/src/ScadaLink.Commons/Types/Enums/DeploymentStatus.cs
new file mode 100644
index 0000000..5a64e2f
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/Enums/DeploymentStatus.cs
@@ -0,0 +1,9 @@
+namespace ScadaLink.Commons.Types.Enums;
+
+public enum DeploymentStatus
+{
+ Pending,
+ InProgress,
+ Success,
+ Failed
+}
diff --git a/src/ScadaLink.Commons/Types/Enums/InstanceState.cs b/src/ScadaLink.Commons/Types/Enums/InstanceState.cs
new file mode 100644
index 0000000..a331955
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/Enums/InstanceState.cs
@@ -0,0 +1,7 @@
+namespace ScadaLink.Commons.Types.Enums;
+
+public enum InstanceState
+{
+ Enabled,
+ Disabled
+}
diff --git a/src/ScadaLink.Commons/Types/Result.cs b/src/ScadaLink.Commons/Types/Result.cs
new file mode 100644
index 0000000..791ef32
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/Result.cs
@@ -0,0 +1,40 @@
+namespace ScadaLink.Commons.Types;
+
+public sealed class Result
+{
+ 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 Success(T value) => new(value);
+
+ public static Result Failure(string error) => new(error);
+
+ public TResult Match(Func onSuccess, Func onFailure) =>
+ IsSuccess ? onSuccess(_value!) : onFailure(_error!);
+}
diff --git a/src/ScadaLink.Commons/Types/RetryPolicy.cs b/src/ScadaLink.Commons/Types/RetryPolicy.cs
new file mode 100644
index 0000000..5dcb76f
--- /dev/null
+++ b/src/ScadaLink.Commons/Types/RetryPolicy.cs
@@ -0,0 +1,3 @@
+namespace ScadaLink.Commons.Types;
+
+public record RetryPolicy(int MaxRetries, TimeSpan Delay);
diff --git a/tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs b/tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs
new file mode 100644
index 0000000..356ff54
--- /dev/null
+++ b/tests/ScadaLink.Commons.Tests/ArchitecturalConstraintTests.cs
@@ -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
+ {
+ "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("$", 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);
+ }
+}
diff --git a/tests/ScadaLink.Commons.Tests/Entities/EntityConventionTests.cs b/tests/ScadaLink.Commons.Tests/Entities/EntityConventionTests.cs
new file mode 100644
index 0000000..084c516
--- /dev/null
+++ b/tests/ScadaLink.Commons.Tests/Entities/EntityConventionTests.cs
@@ -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 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
+ {
+ "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(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 but is {prop.PropertyType.Name}");
+ }
+ }
+ }
+ }
+}
diff --git a/tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs b/tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs
new file mode 100644
index 0000000..5932e11
--- /dev/null
+++ b/tests/ScadaLink.Commons.Tests/Messages/MessageConventionTests.cs
@@ -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 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 $ method
+ var cloneMethod = type.GetMethod("$", 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(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(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(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
+ {
+ ["conn1"] = ScadaLink.Commons.Types.Enums.ConnectionHealth.Connected
+ },
+ new Dictionary
+ {
+ ["conn1"] = new(10, 8)
+ },
+ 0, 0,
+ new Dictionary { ["queue1"] = 5 },
+ 0);
+
+ var json = JsonSerializer.Serialize(msg);
+ var deserialized = JsonSerializer.Deserialize(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
+ {
+ new("script1", "code", null, null)
+ },
+ null, null, null,
+ DateTimeOffset.UtcNow);
+
+ var json = JsonSerializer.Serialize(msg);
+ var deserialized = JsonSerializer.Deserialize(json);
+
+ Assert.NotNull(deserialized);
+ Assert.Equal("dep-1", deserialized!.DeploymentId);
+ Assert.Single(deserialized.SharedScripts!);
+ }
+}
diff --git a/tests/ScadaLink.Commons.Tests/Types/EnumTests.cs b/tests/ScadaLink.Commons.Tests/Types/EnumTests.cs
new file mode 100644
index 0000000..a205cef
--- /dev/null
+++ b/tests/ScadaLink.Commons.Tests/Types/EnumTests.cs
@@ -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').");
+ }
+}
diff --git a/tests/ScadaLink.Commons.Tests/Types/ResultTests.cs b/tests/ScadaLink.Commons.Tests/Types/ResultTests.cs
new file mode 100644
index 0000000..17c286f
--- /dev/null
+++ b/tests/ScadaLink.Commons.Tests/Types/ResultTests.cs
@@ -0,0 +1,75 @@
+using ScadaLink.Commons.Types;
+
+namespace ScadaLink.Commons.Tests.Types;
+
+public class ResultTests
+{
+ [Fact]
+ public void Success_ShouldCreateSuccessfulResult()
+ {
+ var result = Result.Success(42);
+
+ Assert.True(result.IsSuccess);
+ Assert.False(result.IsFailure);
+ Assert.Equal(42, result.Value);
+ }
+
+ [Fact]
+ public void Failure_ShouldCreateFailedResult()
+ {
+ var result = Result.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.Failure("error");
+
+ Assert.Throws(() => result.Value);
+ }
+
+ [Fact]
+ public void Error_OnSuccess_ShouldThrow()
+ {
+ var result = Result.Success(42);
+
+ Assert.Throws(() => result.Error);
+ }
+
+ [Fact]
+ public void Match_OnSuccess_ShouldCallOnSuccess()
+ {
+ var result = Result.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.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.Success("hello");
+
+ Assert.True(result.IsSuccess);
+ Assert.Equal("hello", result.Value);
+ }
+}
diff --git a/tests/ScadaLink.Commons.Tests/Types/RetryPolicyTests.cs b/tests/ScadaLink.Commons.Tests/Types/RetryPolicyTests.cs
new file mode 100644
index 0000000..2c495ee
--- /dev/null
+++ b/tests/ScadaLink.Commons.Tests/Types/RetryPolicyTests.cs
@@ -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);
+ }
+}
diff --git a/tests/ScadaLink.Commons.Tests/UnitTest1.cs b/tests/ScadaLink.Commons.Tests/UnitTest1.cs
deleted file mode 100644
index 5253d8b..0000000
--- a/tests/ScadaLink.Commons.Tests/UnitTest1.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-namespace ScadaLink.Commons.Tests;
-
-public class UnitTest1
-{
- [Fact]
- public void Test1()
- {
-
- }
-}