From b9f6c62bc1641aa89fb1ecf12806c7772695c881 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Wed, 26 Jan 2022 17:40:02 +0200 Subject: [PATCH 1/5] Initialize well known nodes to default values --- .../Server/ServerInternalData.cs | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs index 668102a5fa..16fbada5cc 100644 --- a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs +++ b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs @@ -544,6 +544,102 @@ namespace Opc.Ua.Server #endregion #region Private Methods + /// + /// Initialize a collection of well none nodeIds to a certain value + /// + /// The collection of nodeIds + /// The value to initialize to + private void InitializeNodes(NodeIdCollection nodeIds, object value) + { + foreach (NodeId nodeId in nodeIds) + { + var baseVariableState = (BaseVariableState)m_diagnosticsNodeManager.FindPredefinedNode( + nodeId, + typeof(BaseVariableState)); + if (baseVariableState != null) + { + baseVariableState.Value = value; + } + } + } + + /// + /// Initialize the well known nodes to their default values + /// + private void InitializeWellKnownNodes() + { + // set the WellNone RoleSet propertie node values + IdentityMappingRuleType identityMapping = new IdentityMappingRuleType(); + EndpointType endpointType = new EndpointType(); + + InitializeNodes(new NodeIdCollection(){ + VariableIds.WellKnownRole_Anonymous_Identities, + VariableIds.WellKnownRole_AuthenticatedUser_Identities, + VariableIds.WellKnownRole_Observer_Identities, + VariableIds.WellKnownRole_Operator_Identities, + VariableIds.WellKnownRole_Supervisor_Identities, + VariableIds.WellKnownRole_SecurityAdmin_Identities, + VariableIds.WellKnownRole_ConfigureAdmin_Identities, + VariableIds.WellKnownRole_Engineer_Identities, + }, identityMapping); + + InitializeNodes(new NodeIdCollection(){ + VariableIds.WellKnownRole_Anonymous_Applications, + VariableIds.WellKnownRole_AuthenticatedUser_Applications, + VariableIds.WellKnownRole_Observer_Applications, + VariableIds.WellKnownRole_Operator_Applications, + VariableIds.WellKnownRole_Supervisor_Applications, + VariableIds.WellKnownRole_SecurityAdmin_Applications, + VariableIds.WellKnownRole_ConfigureAdmin_Applications, + VariableIds.WellKnownRole_Engineer_Applications, + }, "initialApp"); + + InitializeNodes(new NodeIdCollection(){ + VariableIds.WellKnownRole_Anonymous_ApplicationsExclude, + VariableIds.WellKnownRole_AuthenticatedUser_ApplicationsExclude, + VariableIds.WellKnownRole_Observer_ApplicationsExclude, + VariableIds.WellKnownRole_Operator_ApplicationsExclude, + VariableIds.WellKnownRole_Supervisor_ApplicationsExclude, + VariableIds.WellKnownRole_SecurityAdmin_ApplicationsExclude, + VariableIds.WellKnownRole_ConfigureAdmin_ApplicationsExclude, + VariableIds.WellKnownRole_Engineer_ApplicationsExclude, + VariableIds.WellKnownRole_Anonymous_EndpointsExclude, + VariableIds.WellKnownRole_AuthenticatedUser_EndpointsExclude, + VariableIds.WellKnownRole_Observer_EndpointsExclude, + VariableIds.WellKnownRole_Operator_EndpointsExclude, + VariableIds.WellKnownRole_Supervisor_EndpointsExclude, + VariableIds.WellKnownRole_SecurityAdmin_EndpointsExclude, + VariableIds.WellKnownRole_ConfigureAdmin_EndpointsExclude, + VariableIds.WellKnownRole_Engineer_EndpointsExclude, + }, false); + + InitializeNodes(new NodeIdCollection(){ + VariableIds.WellKnownRole_Anonymous_Endpoints, + VariableIds.WellKnownRole_AuthenticatedUser_Endpoints, + VariableIds.WellKnownRole_Observer_Endpoints, + VariableIds.WellKnownRole_Operator_Endpoints, + VariableIds.WellKnownRole_Supervisor_Endpoints, + VariableIds.WellKnownRole_SecurityAdmin_Endpoints, + VariableIds.WellKnownRole_ConfigureAdmin_Endpoints, + VariableIds.WellKnownRole_Engineer_Endpoints, + }, endpointType); + + + // setup default values for default role permissions and access restrictions + RolePermissionType rolePermissionType = new RolePermissionType(); + AccessRestrictionType accessRestrictionType = new AccessRestrictionType(); + + InitializeNodes(new NodeIdCollection(){ + VariableIds.OPCUANamespaceMetadata_DefaultRolePermissions, + VariableIds.OPCUANamespaceMetadata_DefaultUserRolePermissions + }, rolePermissionType); + + InitializeNodes(new NodeIdCollection(){ + VariableIds.OPCUANamespaceMetadata_DefaultAccessRestrictions + }, accessRestrictionType); + + } + /// /// Creates the ServerObject and attaches it to the NodeManager. /// @@ -602,6 +698,22 @@ namespace Opc.Ua.Server operationLimits.MaxMonitoredItemsPerCall = null; } + InitializeWellKnownNodes(); + + // setup PublishSubscribe Status State value + PubSubState pubSubState = PubSubState.Disabled; + + var default_PubSubState = (BaseVariableState)m_diagnosticsNodeManager.FindPredefinedNode( + VariableIds.PublishSubscribe_Status_State, + typeof(BaseVariableState)); + default_PubSubState.Value = pubSubState; + + // setup value for SupportedTransportProfiles + var default_SupportedTransportProfiles = (BaseVariableState)m_diagnosticsNodeManager.FindPredefinedNode( + VariableIds.PublishSubscribe_SupportedTransportProfiles, + typeof(BaseVariableState)); + default_SupportedTransportProfiles.Value = "uadp"; + // setup callbacks for dynamic values. serverObject.NamespaceArray.OnSimpleReadValue = OnReadNamespaceArray; serverObject.NamespaceArray.MinimumSamplingInterval = 1000; From b638bb48b77a0de67d51a83ea7413ae1eb2a3de3 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Thu, 27 Jan 2022 09:09:11 +0200 Subject: [PATCH 2/5] Set status code Good for all BaseVariableState in namespace 0 --- .../Server/ServerInternalData.cs | 98 ------------------- Stack/Opc.Ua.Core/Stack/State/NodeState.cs | 9 ++ 2 files changed, 9 insertions(+), 98 deletions(-) diff --git a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs index 16fbada5cc..f253ffc544 100644 --- a/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs +++ b/Libraries/Opc.Ua.Server/Server/ServerInternalData.cs @@ -544,102 +544,6 @@ namespace Opc.Ua.Server #endregion #region Private Methods - /// - /// Initialize a collection of well none nodeIds to a certain value - /// - /// The collection of nodeIds - /// The value to initialize to - private void InitializeNodes(NodeIdCollection nodeIds, object value) - { - foreach (NodeId nodeId in nodeIds) - { - var baseVariableState = (BaseVariableState)m_diagnosticsNodeManager.FindPredefinedNode( - nodeId, - typeof(BaseVariableState)); - if (baseVariableState != null) - { - baseVariableState.Value = value; - } - } - } - - /// - /// Initialize the well known nodes to their default values - /// - private void InitializeWellKnownNodes() - { - // set the WellNone RoleSet propertie node values - IdentityMappingRuleType identityMapping = new IdentityMappingRuleType(); - EndpointType endpointType = new EndpointType(); - - InitializeNodes(new NodeIdCollection(){ - VariableIds.WellKnownRole_Anonymous_Identities, - VariableIds.WellKnownRole_AuthenticatedUser_Identities, - VariableIds.WellKnownRole_Observer_Identities, - VariableIds.WellKnownRole_Operator_Identities, - VariableIds.WellKnownRole_Supervisor_Identities, - VariableIds.WellKnownRole_SecurityAdmin_Identities, - VariableIds.WellKnownRole_ConfigureAdmin_Identities, - VariableIds.WellKnownRole_Engineer_Identities, - }, identityMapping); - - InitializeNodes(new NodeIdCollection(){ - VariableIds.WellKnownRole_Anonymous_Applications, - VariableIds.WellKnownRole_AuthenticatedUser_Applications, - VariableIds.WellKnownRole_Observer_Applications, - VariableIds.WellKnownRole_Operator_Applications, - VariableIds.WellKnownRole_Supervisor_Applications, - VariableIds.WellKnownRole_SecurityAdmin_Applications, - VariableIds.WellKnownRole_ConfigureAdmin_Applications, - VariableIds.WellKnownRole_Engineer_Applications, - }, "initialApp"); - - InitializeNodes(new NodeIdCollection(){ - VariableIds.WellKnownRole_Anonymous_ApplicationsExclude, - VariableIds.WellKnownRole_AuthenticatedUser_ApplicationsExclude, - VariableIds.WellKnownRole_Observer_ApplicationsExclude, - VariableIds.WellKnownRole_Operator_ApplicationsExclude, - VariableIds.WellKnownRole_Supervisor_ApplicationsExclude, - VariableIds.WellKnownRole_SecurityAdmin_ApplicationsExclude, - VariableIds.WellKnownRole_ConfigureAdmin_ApplicationsExclude, - VariableIds.WellKnownRole_Engineer_ApplicationsExclude, - VariableIds.WellKnownRole_Anonymous_EndpointsExclude, - VariableIds.WellKnownRole_AuthenticatedUser_EndpointsExclude, - VariableIds.WellKnownRole_Observer_EndpointsExclude, - VariableIds.WellKnownRole_Operator_EndpointsExclude, - VariableIds.WellKnownRole_Supervisor_EndpointsExclude, - VariableIds.WellKnownRole_SecurityAdmin_EndpointsExclude, - VariableIds.WellKnownRole_ConfigureAdmin_EndpointsExclude, - VariableIds.WellKnownRole_Engineer_EndpointsExclude, - }, false); - - InitializeNodes(new NodeIdCollection(){ - VariableIds.WellKnownRole_Anonymous_Endpoints, - VariableIds.WellKnownRole_AuthenticatedUser_Endpoints, - VariableIds.WellKnownRole_Observer_Endpoints, - VariableIds.WellKnownRole_Operator_Endpoints, - VariableIds.WellKnownRole_Supervisor_Endpoints, - VariableIds.WellKnownRole_SecurityAdmin_Endpoints, - VariableIds.WellKnownRole_ConfigureAdmin_Endpoints, - VariableIds.WellKnownRole_Engineer_Endpoints, - }, endpointType); - - - // setup default values for default role permissions and access restrictions - RolePermissionType rolePermissionType = new RolePermissionType(); - AccessRestrictionType accessRestrictionType = new AccessRestrictionType(); - - InitializeNodes(new NodeIdCollection(){ - VariableIds.OPCUANamespaceMetadata_DefaultRolePermissions, - VariableIds.OPCUANamespaceMetadata_DefaultUserRolePermissions - }, rolePermissionType); - - InitializeNodes(new NodeIdCollection(){ - VariableIds.OPCUANamespaceMetadata_DefaultAccessRestrictions - }, accessRestrictionType); - - } - /// /// Creates the ServerObject and attaches it to the NodeManager. /// @@ -698,8 +602,6 @@ namespace Opc.Ua.Server operationLimits.MaxMonitoredItemsPerCall = null; } - InitializeWellKnownNodes(); - // setup PublishSubscribe Status State value PubSubState pubSubState = PubSubState.Disabled; diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs index 0084c2563d..b98514a46a 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs @@ -1827,6 +1827,15 @@ namespace Opc.Ua // update any children. child.UpdateChildren(context, decoder); + // Set the status code to Good for BaseVariableState nodes with namespace 0 + if (child is BaseVariableState childVar) + { + if (childVar.NodeId.NamespaceIndex == 0) + { + childVar.StatusCode = StatusCodes.Good; + } + } + return child; } From e5121742a11fb744e0ad59a552835e9166ba754b Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Thu, 27 Jan 2022 11:28:20 +0200 Subject: [PATCH 3/5] Assign a default value to anu variable in namespace 0 --- .../Opc.Ua.Server/Diagnostics/CustomNodeManager.cs | 11 +++++++++++ Stack/Opc.Ua.Core/Stack/State/NodeState.cs | 9 --------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs b/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs index 72c3539bcc..314f142336 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/CustomNodeManager.cs @@ -583,6 +583,17 @@ namespace Opc.Ua.Server m_predefinedNodes = new NodeIdDictionary(); } + // assign a default value to any variable in namespace 0 + if (node is BaseVariableState nodeStateVar) + { + if (nodeStateVar.NodeId.NamespaceIndex == 0 && nodeStateVar.Value == null) + { + nodeStateVar.Value = TypeInfo.GetDefaultValue(nodeStateVar.DataType, + nodeStateVar.ValueRank, + Server.TypeTree); + } + } + NodeState activeNode = AddBehaviourToPredefinedNode(context, node); m_predefinedNodes[activeNode.NodeId] = activeNode; diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs index b98514a46a..0084c2563d 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs @@ -1827,15 +1827,6 @@ namespace Opc.Ua // update any children. child.UpdateChildren(context, decoder); - // Set the status code to Good for BaseVariableState nodes with namespace 0 - if (child is BaseVariableState childVar) - { - if (childVar.NodeId.NamespaceIndex == 0) - { - childVar.StatusCode = StatusCodes.Good; - } - } - return child; } From ac131518bead1209000f20e95d46ccbcf6e346c5 Mon Sep 17 00:00:00 2001 From: Martin Regen Date: Thu, 27 Jan 2022 08:46:32 +0100 Subject: [PATCH 4/5] ctt fixes (#1685) * remove the SetDurableSubscription method, to skip the test cases * DeleteSubscription returns BadSubscriptionIdInvalid * MethodState return also BadTooManyArguments * return AttributeIdInvalid for null attributes (fix UA Expert display) * fix ToString of Variant array of ByteString * fix Variant decoding 1-dim array of bytestring --- .../Diagnostics/DiagnosticsNodeManager.cs | 24 +++++++- .../Subscription/SubscriptionManager.cs | 4 +- Stack/Opc.Ua.Core/Stack/State/MethodState.cs | 7 ++- Stack/Opc.Ua.Core/Stack/State/NodeState.cs | 21 ++++++- Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs | 55 +++++++++++++++---- .../Types/Encoders/BinaryDecoder.cs | 2 +- 6 files changed, 91 insertions(+), 22 deletions(-) diff --git a/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs b/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs index 3a46df0b41..1f71be025a 100644 --- a/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs +++ b/Libraries/Opc.Ua.Server/Diagnostics/DiagnosticsNodeManager.cs @@ -141,9 +141,9 @@ namespace Opc.Ua.Server Server.CoreNodeManager.ImportNodes(SystemContext, PredefinedNodes.Values, true); // hook up the server GetMonitoredItems method. - MethodState getMonitoredItems = (MethodState)FindPredefinedNode( + GetMonitoredItemsMethodState getMonitoredItems = (GetMonitoredItemsMethodState)FindPredefinedNode( MethodIds.Server_GetMonitoredItems, - typeof(MethodState)); + typeof(GetMonitoredItemsMethodState)); if (getMonitoredItems != null) { @@ -169,11 +169,29 @@ namespace Opc.Ua.Server getMonitoredItemsOutputArguments.ClearChangeMasks(SystemContext, false); } } + + // Subscription Durable mode not supported by the server. + ServerObjectState serverObject = (ServerObjectState)FindPredefinedNode( + ObjectIds.Server, + typeof(ServerObjectState)); + + if (serverObject != null) + { + NodeState setSubscriptionDurableNode = serverObject.FindChild( + SystemContext, + BrowseNames.SetSubscriptionDurable); + + if (setSubscriptionDurableNode != null) + { + DeleteNode(SystemContext, MethodIds.Server_SetSubscriptionDurable); + serverObject.SetSubscriptionDurable = null; + } + } } } /// - /// Called when a client locks the server. + /// Called when a client gets the monitored items of a subscription. /// public ServiceResult OnGetMonitoredItems( ISystemContext context, diff --git a/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs b/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs index d5d2faef05..d00057f33d 100644 --- a/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs +++ b/Libraries/Opc.Ua.Server/Subscription/SubscriptionManager.cs @@ -435,10 +435,10 @@ namespace Opc.Ua.Server if (!NodeId.IsNull(sessionId)) { - // check that the subscrition is the owner. + // check that the subscription is the owner. if (context != null && !Object.ReferenceEquals(context.Session, subscription.Session)) { - throw new ServiceResultException(StatusCodes.BadSessionIdInvalid); + throw new ServiceResultException(StatusCodes.BadSubscriptionIdInvalid); } SessionPublishQueue queue = null; diff --git a/Stack/Opc.Ua.Core/Stack/State/MethodState.cs b/Stack/Opc.Ua.Core/Stack/State/MethodState.cs index 3755891c54..50e3fa25d6 100644 --- a/Stack/Opc.Ua.Core/Stack/State/MethodState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/MethodState.cs @@ -612,11 +612,16 @@ namespace Opc.Ua expectedCount = InputArguments.Value.Length; } - if (expectedCount != inputArguments.Count) + if (expectedCount > inputArguments.Count) { return StatusCodes.BadArgumentsMissing; } + if (expectedCount < inputArguments.Count) + { + return StatusCodes.BadTooManyArguments; + } + // validate individual arguements. bool error = false; diff --git a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs index 0084c2563d..097b90ee6a 100644 --- a/Stack/Opc.Ua.Core/Stack/State/NodeState.cs +++ b/Stack/Opc.Ua.Core/Stack/State/NodeState.cs @@ -3454,7 +3454,12 @@ namespace Opc.Ua value = description; } - return result; + if (value != null) + { + return result; + } + + break; } case Attributes.WriteMask: @@ -3505,7 +3510,12 @@ namespace Opc.Ua value = rolePermissions; } - return result; + if (value != null) + { + return result; + } + + break; } case Attributes.UserRolePermissions: @@ -3522,7 +3532,12 @@ namespace Opc.Ua value = userRolePermissions; } - return result; + if (value != null) + { + return result; + } + + break; } case Attributes.AccessRestrictions: diff --git a/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs b/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs index 874720042f..b22895a47d 100644 --- a/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs +++ b/Stack/Opc.Ua.Core/Types/BuiltIn/Variant.cs @@ -861,6 +861,24 @@ namespace Opc.Ua throw new FormatException(Utils.Format("Invalid format string: '{0}'.", format)); } + /// + /// Append a ByteString as a hex string. + /// + private void AppendByteString(StringBuilder buffer, byte[] bytes, IFormatProvider formatProvider) + { + if (bytes != null) + { + for (int ii = 0; ii < bytes.Length; ii++) + { + buffer.AppendFormat(formatProvider, "{0:X2}", bytes[ii]); + } + } + else + { + buffer.Append("(null)"); + } + } + /// /// Formats a value as a string. /// @@ -877,12 +895,7 @@ namespace Opc.Ua if (m_typeInfo.BuiltInType == BuiltInType.ByteString && m_typeInfo.ValueRank < 0) { byte[] bytes = (byte[])value; - - for (int ii = 0; ii < bytes.Length; ii++) - { - buffer.AppendFormat(formatProvider, "{0:X2}", bytes[ii]); - } - + AppendByteString(buffer, bytes, formatProvider); return; } @@ -901,15 +914,33 @@ namespace Opc.Ua { buffer.Append('{'); - if (array.Length > 0) + if (m_typeInfo.BuiltInType == BuiltInType.ByteString) { - AppendFormat(buffer, array.GetValue(0), formatProvider); - } + if (array.Length > 0) + { + byte[] bytes = (byte[])array.GetValue(0); + AppendByteString(buffer, bytes, formatProvider); + } - for (int ii = 1; ii < array.Length; ii++) + for (int ii = 1; ii < array.Length; ii++) + { + buffer.Append('|'); + byte[] bytes = (byte[])array.GetValue(ii); + AppendByteString(buffer, bytes, formatProvider); + } + } + else { - buffer.Append(" |"); - AppendFormat(buffer, array.GetValue(ii), formatProvider); + if (array.Length > 0) + { + AppendFormat(buffer, array.GetValue(0), formatProvider); + } + + for (int ii = 1; ii < array.Length; ii++) + { + buffer.Append('|'); + AppendFormat(buffer, array.GetValue(ii), formatProvider); + } } buffer.Append('}'); diff --git a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs index 38ef3379a1..e335ac09e0 100644 --- a/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs +++ b/Stack/Opc.Ua.Core/Types/Encoders/BinaryDecoder.cs @@ -2206,7 +2206,7 @@ namespace Opc.Ua } else { - value = new Variant(array); + value = new Variant(array, new TypeInfo(builtInType, 1)); } } } From f9a65094b977389c958675f4082f39bc1b4be683 Mon Sep 17 00:00:00 2001 From: mrsuciu Date: Thu, 27 Jan 2022 15:17:59 +0200 Subject: [PATCH 5/5] Eliminate duplicate endpoints when Configured base addresses differ by port only --- Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs | 22 +++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs index 5edfa2d1e2..e8e3a0877d 100644 --- a/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs +++ b/Stack/Opc.Ua.Core/Stack/Server/ServerBase.cs @@ -1075,6 +1075,12 @@ namespace Opc.Ua continue; } + if (endpointUrl.Port != baseAddress.Url.Port) + { + continue; + } + + EndpointDescription translation = new EndpointDescription(); translation.EndpointUrl = baseAddress.Url.ToString(); @@ -1094,22 +1100,8 @@ namespace Opc.Ua translation.UserIdentityTokens = endpoint.UserIdentityTokens; translation.Server = application; - // skip duplicates. - bool duplicateFound = false; + translations.Add(translation); - foreach (EndpointDescription existingTranslation in translations) - { - if (existingTranslation.IsEqual(translation)) - { - duplicateFound = true; - break; - } - } - - if (!duplicateFound) - { - translations.Add(translation); - } } }