/// <summary> /// Retrieve Codelist /// </summary> /// <param name="info"> /// The current StructureRetrieval state /// </param> /// <returns> /// A <see cref="CodeListBean"/> /// </returns> public ICodelistMutableObject GetCodeList(StructureRetrievalInfo info) { var dataflowrRef = new MaintainableRefObjectImpl(info.MappingSet.Dataflow.Agency, info.MappingSet.Dataflow.Id, info.MappingSet.Dataflow.Version); ISet<ICodelistMutableObject> codeLists = info.MastoreAccess.GetMutableCodelistObjects(info.CodelistRef,dataflowrRef, info.RequestedComponent, this._isTranscoded, info.AllowedDataflows); return CodeListHelper.GetFirstCodeList(codeLists); }
/// <summary> /// Generate the SQL for executing on the DDB /// </summary> /// <param name="info"> /// The current structure retrieval information /// </param> /// <returns> /// The generated sql. /// </returns> public override string GenerateSql(StructureRetrievalInfo info) { MappingEntity[] mapping; if (info.RequestedComponentInfo != null) { mapping = new[] { info.RequestedComponentInfo.Mapping }; } else if (info.RequestedComponent.Equals(info.TimeDimension)) { mapping = info.FrequencyInfo != null ? new[] { info.TimeMapping, info.FrequencyInfo.Mapping } : new[] { info.TimeMapping }; } else { return null; } var columnList = ToColumnNameString(mapping); return string.Format( CultureInfo.InvariantCulture, "SELECT DISTINCT {2} \n FROM ({0}) virtualDataset {1} \n ORDER BY {2} ", info.InnerSqlQuery, GenerateWhere(info), columnList); }
/// <summary> /// Retrieve Codelist /// </summary> /// <param name="info"> /// The current StructureRetrieval state /// </param> /// <returns> /// A <see cref="CodeListBean"/> /// </returns> public ICodelistMutableObject GetCodeList(StructureRetrievalInfo info) { ISet<ICodelistMutableObject> codeLists = info.MastoreAccess.GetMutableCodelistObjects(new MaintainableRefObjectImpl(info.CodelistRef.AgencyId, info.CodelistRef.MaintainableId, info.CodelistRef.Version)); return CodeListHelper.GetFirstCodeList(codeLists); }
/// <summary> /// Generate the SQL for executing on the DDB /// </summary> /// <param name="info"> /// The current structure retrieval information /// </param> /// <returns> /// The generated sql. /// </returns> public override string GenerateSql(StructureRetrievalInfo info) { return string.Format( CultureInfo.InvariantCulture, "SELECT COUNT(*) \n FROM ({0}) virtualDataset {1}", info.InnerSqlQuery, GenerateWhere(info)); }
/// <summary> /// Retrieve Codelist /// </summary> /// <param name="info"> /// The current StructureRetrieval state /// </param> /// <returns> /// A <see cref="CodeListBean"/> /// </returns> public ICodelistMutableObject GetCodeList(StructureRetrievalInfo info) { if (info.RequestedComponentInfo == null) { return null; } ISet<ICodelistMutableObject> codeLists = info.MastoreAccess.GetMutableCodelistObjects( info.CodelistRef, new[] { info.RequestedComponentInfo.Mapping.Constant }); return CodeListHelper.GetFirstCodeList(codeLists); }
/// <summary> /// Retrieve Codelist /// </summary> /// <param name="info"> /// The current StructureRetrieval state /// </param> /// <returns> /// A <see cref="CodeListBean"/> /// </returns> public ICodelistMutableObject GetCodeList(StructureRetrievalInfo info) { if (info.RequestedComponentInfo == null) { return null; } var codesSet = new Dictionary<string, object>(); IComponentMapping cmap = info.RequestedComponentInfo.ComponentMapping; using (DbConnection ddbConnection = DDbConnectionBuilder.Instance.Build(info)) { using (DbCommand cmd = ddbConnection.CreateCommand()) { cmd.CommandText = info.SqlQuery; cmd.CommandTimeout = 0; using (IDataReader reader = cmd.ExecuteReader()) { while (reader.Read()) { string dsdCode = cmap.MapComponent(reader); if (dsdCode != null && !codesSet.ContainsKey(dsdCode)) { codesSet.Add(dsdCode, null); } } } } } if (codesSet.Count > 0) { var subset = new List<string>(codesSet.Keys); ISet<ICodelistMutableObject> codeLists = info.MastoreAccess.GetMutableCodelistObjects( new MaintainableRefObjectImpl(info.CodelistRef.AgencyId, info.CodelistRef.MaintainableId, info.CodelistRef.Version ), subset); return CodeListHelper.GetFirstCodeList(codeLists); } return null; }
/// <summary> /// Retrieve Codelist /// </summary> /// <param name="info"> /// The current StructureRetrieval state /// </param> /// <returns> /// A <see cref="ICodelistMutableObject"/> /// </returns> public ICodelistMutableObject GetCodeList(StructureRetrievalInfo info) { var codelist = new CodelistMutableCore(); var name = new TextTypeWrapperMutableCore { Locale = CustomCodelistConstants.Lang, Value = CustomCodelistConstants.CountCodeListName, }; codelist.Names.Add(name); codelist.Id = CustomCodelistConstants.CountCodeList; codelist.AgencyId = CustomCodelistConstants.Agency; codelist.Version = CustomCodelistConstants.Version; int xsMeasureMult = 1; if (info.MeasureComponent != null) { info.Logger.Info("|-- Get XS Measure count"); xsMeasureMult = GetXsMeasureCount(info); } object value = ExecuteSql(info); // setup count codelist var countCode = new CodeMutableCore(); var text = new TextTypeWrapperMutableCore { Locale = CustomCodelistConstants.Lang, Value = CustomCodelistConstants.CountCodeDescription, }; countCode.Names.Add(text); // normally count(*) should always return a number. Checking just in case I missed something. if (value != null && !Convert.IsDBNull(value)) { // in .net, oracle will return 128bit decimal, sql server 32bit int, while mysql & sqlite 64bit long. long count = Convert.ToInt64(value, CultureInfo.InvariantCulture); // check if there are XS measure mappings. In this case there could be multiple measures/obs per row. // even if they are not, then will be static mappings count *= xsMeasureMult; countCode.Id = count.ToString(CultureInfo.InvariantCulture); codelist.AddItem(countCode); } else { countCode.Id = CustomCodelistConstants.CountCodeDefault; } return codelist; }
/// <summary> /// Retrieve Codelist /// </summary> /// <param name="info"> /// The current StructureRetrieval state /// </param> /// <returns> /// A <see cref="ICodelistMutableObject"/> /// </returns> public ICodelistMutableObject GetCodeList(StructureRetrievalInfo info) { ISdmxDate minPeriod = new SdmxDateCore(CustomCodelistConstants.TimePeriodMax); ISdmxDate maxPeriod = new SdmxDateCore(CustomCodelistConstants.TimePeriodMin); var frequencyMapping = info.FrequencyInfo != null ? info.FrequencyInfo.ComponentMapping : null; var timeTranscoder = info.TimeTranscoder; using (DbConnection ddbConnection = DDbConnectionBuilder.Instance.Build(info)) using (DbCommand cmd = ddbConnection.CreateCommand()) { cmd.CommandTimeout = 0; cmd.CommandText = info.SqlQuery; using (IDataReader reader = cmd.ExecuteReader()) { while (reader.Read()) { string frequency = frequencyMapping != null ? frequencyMapping.MapComponent(reader) : null; string period = timeTranscoder.MapComponent(reader, frequency); if (!string.IsNullOrEmpty(period)) { ISdmxDate currentPeriod = new SdmxDateCore(period); if (currentPeriod.StartsBefore(minPeriod)) { minPeriod = currentPeriod; } if (currentPeriod.EndsAfter(maxPeriod)) { maxPeriod = currentPeriod; } } } } } return TimeDimensionCodeListHelper.BuildTimeCodelist(minPeriod, maxPeriod); }
/// <summary> /// Retrieve Codelist /// </summary> /// <param name="info"> /// The current StructureRetrieval state /// </param> /// <returns> /// A <see cref="ICodelistMutableObject"/> /// </returns> public ICodelistMutableObject GetCodeList(StructureRetrievalInfo info) { ISdmxDate constantDate = new SdmxDateCore(info.TimeMapping.Constant); return TimeDimensionCodeListHelper.BuildTimeCodelist(constantDate, constantDate); }
/// <summary> /// Process the <paramref name="current"/> and populate the <paramref name="mutableObjects"/> /// </summary> /// <param name="av"> /// The current Structure Retrieval state /// </param> /// <param name="current"> /// The <see cref="IMaintainableRefObject"/> codelist request /// </param> /// <param name="mutableObjects">The output mutable objects</param> /// <returns> /// <c>true</c> if the partial codelist is found found is larger than 0. Else <c>false</c> /// </returns> private static bool ProcessReference(StructureRetrievalInfo av, IMaintainableRefObject current, IMutableObjects mutableObjects) { ICodelistMutableObject availableData = _specialRequestManager.RetrieveAvailableData(current, av); if (availableData != null) { mutableObjects.AddCodelist(availableData); return true; } return false; }
/// <summary> /// Get CrossSectional Measure count. /// </summary> /// <param name="info"> /// The current Structure retrieval state /// </param> /// <returns> /// The CrossSectional Measure count. /// </returns> private static int GetXsMeasureCount(StructureRetrievalInfo info) { int xsMeasureCount = info.XSMeasureDimensionConstraints.Count; foreach (var member in info.Criteria) { if (member.Id.Equals(info.MeasureComponent)) { // get the unmapped measure dimension XS codes to display xsMeasureCount = 0; foreach (string value in member.Values) { if (value != null && info.XSMeasureDimensionConstraints.ContainsKey(value)) { xsMeasureCount++; } } } } return xsMeasureCount == 0 ? 1 : xsMeasureCount; }
/// <summary> /// Execute the <see cref="StructureRetrievalInfo.SqlQuery"/> against the DDB and get a single value /// </summary> /// <param name="info"> /// The current Structure retrieval state /// </param> /// <returns> /// The scalar value /// </returns> /// <exception cref="DbException"> /// DDB communication error /// </exception> private static object ExecuteSql(StructureRetrievalInfo info) { object value; using (DbConnection ddbConnection = DDbConnectionBuilder.Instance.Build(info)) { using (DbCommand cmd = ddbConnection.CreateCommand()) { cmd.CommandText = info.SqlQuery; cmd.CommandTimeout = 0; value = cmd.ExecuteScalar(); } } return value; }
/// <summary> /// Build a <see cref="StructureRetrievalInfo"/> from the specified parameters /// </summary> /// <param name="dataflow"> /// The dataflow to get the available data for /// </param> /// <param name="connectionStringSettings"> /// The Mapping Store connection string settings /// </param> /// <param name="allowedDataflows"> /// The collection of allowed dataflows /// </param> /// <exception cref="ArgumentNullException"> /// connectionStringSettings is null /// </exception> /// <exception cref="ArgumentNullException"> /// dataflow is null /// </exception> /// <exception cref="StructureRetrieverException"> /// Parsing error or mapping store exception error /// </exception> /// <returns> /// a <see cref="StructureRetrievalInfo"/> from the specified parameters /// </returns> public StructureRetrievalInfo Build( IConstrainableStructureReference dataflow, ConnectionStringSettings connectionStringSettings, IList<IMaintainableRefObject> allowedDataflows) { if (connectionStringSettings == null) { throw new ArgumentNullException("connectionStringSettings"); } if (dataflow == null) { throw new ArgumentNullException("dataflow"); } var info = new StructureRetrievalInfo(_logger, allowedDataflows, connectionStringSettings); try { info.MappingSet = MappingSetRetriever.GetMappingSet( info.ConnectionStringSettings, dataflow.MaintainableReference.MaintainableId, dataflow.MaintainableReference.Version, dataflow.MaintainableReference.AgencyId, info.AllowedDataflows); if (info.MappingSet != null) { ParserDataflowRef(dataflow, info); Initialize(info); } } catch (SdmxException) { throw; } catch (StructureRetrieverException e) { _logger.Error(e.Message, e); switch (e.ErrorType) { case StructureRetrieverErrorTypes.ParsingError: throw new SdmxSyntaxException(e, ExceptionCode.XmlParseException); case StructureRetrieverErrorTypes.MissingStructure: case StructureRetrieverErrorTypes.MissingStructureRef: throw new SdmxNoResultsException(e.Message); default: throw new SdmxException(e, SdmxErrorCode.GetFromEnum(SdmxErrorCodeEnumType.InternalServerError), e.Message); } } catch (DbException e) { string mesage = "Mapping Store connection error." + e.Message; _logger.Error(mesage); _logger.Error(e.ToString()); throw new SdmxException(e, SdmxErrorCode.GetFromEnum(SdmxErrorCodeEnumType.InternalServerError), mesage); } catch (Exception e) { string mesage = string.Format( CultureInfo.CurrentCulture, ErrorMessages.ErrorRetrievingMappingSetFormat4, dataflow.MaintainableReference.AgencyId, dataflow.MaintainableReference.MaintainableId, dataflow.MaintainableReference.Version, e.Message); _logger.Error(mesage); _logger.Error(e.ToString()); throw new SdmxException(e, SdmxErrorCode.GetFromEnum(SdmxErrorCodeEnumType.InternalServerError), mesage); } return info; }
/// <summary> /// Parse the specified DataflowRefBean object and populate the <see cref="StructureRetrievalInfo.RequestedComponent"/> and <see cref="StructureRetrievalInfo.Criteria"/> fields /// </summary> /// <param name="d"> /// The DataflowRefBean to parse /// </param> /// <param name="info"> /// The current structure retrieval state /// </param> private static void ParserDataflowRef(IConstrainableStructureReference d, StructureRetrievalInfo info) { if (d.ConstraintObject != null && d.ConstraintObject.IncludedCubeRegion != null) { foreach (IKeyValues member in d.ConstraintObject.IncludedCubeRegion.KeyValues) { if (member.Values.Count == 0 || (member.Values.Count == 1 && SpecialValues.DummyMemberValue.Equals(member.Values[0]))) { info.RequestedComponent = GetRequestedComponentId(info.MappingSet.Dataflow.Dsd, member.Id); } else { IKeyValuesMutable normalizedMember = new KeyValuesMutableImpl(member) { Id = GetRequestedComponentId(info.MappingSet.Dataflow.Dsd, member.Id) }; var keyValuesCore = new KeyValuesCore(normalizedMember, member.Parent); info.Criteria.Add(keyValuesCore); } } info.ReferencePeriod = d.ConstraintObject.MutableInstance.ReferencePeriod; } }
/// <summary> /// Initialize component mappings for coded components and time dimension used in mappings in the dataflow /// </summary> /// <param name="info"> /// The current structure retrieval state /// </param> /// <remarks> /// This method should be called only once /// </remarks> private static void Initialize(StructureRetrievalInfo info) { NormallizeDatabaseProvider(info.ConnectionStringSettings); info.MastoreAccess = new SpecialMutableObjectRetrievalManager(info.ConnectionStringSettings); info.XSMeasureDimensionConstraints.Clear(); info.ComponentMapping.Clear(); info.InnerSqlQuery = info.MappingSet.DataSet.Query; info.MeasureComponent = null; info.TimeTranscoder = null; info.TimeMapping = null; info.TimeDimension = null; bool measureDimensionMapped = false; foreach (MappingEntity mapping in info.MappingSet.Mappings) { foreach (ComponentEntity component in mapping.Components) { if (component.ComponentType == SdmxComponentType.TimeDimension) { info.TimeMapping = mapping; info.TimeDimension = component.Id; } else if (component.CodeList != null) { if (component.MeasureDimension) { measureDimensionMapped = true; } var compInfo = new ComponentInfo { Mapping = mapping, ComponentMapping = ComponentMapping.CreateComponentMapping(component, mapping) }; compInfo.CodelistRef.MaintainableId = component.CodeList.Id; compInfo.CodelistRef.Version = component.CodeList.Version; compInfo.CodelistRef.AgencyId = component.CodeList.Agency; var id = component.Id; info.ComponentMapping.Add(id, compInfo); if (id.Equals(info.RequestedComponent)) { info.RequestedComponentInfo = compInfo; } if (component.FrequencyDimension) { info.FrequencyInfo = compInfo; } } } } if (info.TimeMapping != null) { info.TimeTranscoder = TimeDimensionMapping.Create( info.TimeMapping, info.FrequencyInfo != null ? info.FrequencyInfo.ComponentMapping : null, info.MappingSet.DataSet.Connection.DBType); } if (!measureDimensionMapped) { foreach (ComponentEntity component in info.MappingSet.Dataflow.Dsd.Dimensions) { if (component.MeasureDimension) { info.MeasureComponent = component.Id; } } foreach (ComponentEntity xsMeasure in info.MappingSet.Dataflow.Dsd.CrossSectionalMeasures) { info.XSMeasureDimensionConstraints.Add(xsMeasure.Id, xsMeasure); } } }
/// <summary> /// Retrieve the codelist that is referenced by the given <paramref name="codelistRef"/> . The codelist /// </summary> /// <param name="codelistRef"> /// The codelist reference containing the id, agency and version of the requested dimension. Can be empty for time dimension /// </param> /// <param name="info"> /// The current structure retrieval state. /// </param> /// <returns> /// The partial codelist /// </returns> /// <exception cref="ArgumentNullException"> /// <paramref name="codelistRef"/> /// is null /// </exception> public ICodelistMutableObject RetrieveAvailableData(IMaintainableRefObject codelistRef, StructureRetrievalInfo info) { info.Logger.Info("Starting special request manager for codelist" + codelistRef); if (codelistRef == null) { throw new ArgumentNullException("codelistRef"); } if (info.MappingSet == null) { info.Logger.Warn("|-- Warning: Codes not retrieved. Couldn't find a mapping set."); return null; } // set the codelist ref info.CodelistRef = codelistRef; ISqlBuilder sqlBuilder = null; ICodeListRetrievalEngine codeListRetrievalEngine = null; ISqlBuilder fallBackSqlBuilder = null; ICodeListRetrievalEngine fallBackCodeListRetrievalEngine = null; if (!IsRequestedComponentCodelist(codelistRef, info)) { if (CustomCodelistConstants.IsCountRequest(codelistRef)) { // COUNT data request sqlBuilder = _countSqlBuilder; codeListRetrievalEngine = _countCodeListRetrieval; } } else if (info.RequestedComponent.Equals(info.MeasureComponent)) { // measure dimension codelist request if measure dimension is not mapped // get the entire codelist for measure dimension codeListRetrievalEngine = _simpleCodeListRetrieval; } else if (info.RequestedComponent.Equals(info.TimeDimension)) { // time dimension codelist request // get the time dimension special codelist with start and possibly end codes. if (info.TimeMapping.Columns.Count > 0) { sqlBuilder = _componentSqlBuilder; codeListRetrievalEngine = _timeDimensionCodeListRetrieval; } else { codeListRetrievalEngine = _constantTimeDimensionCodeListRetrieval; } } else { // partial code list request for mapped coded components if (info.RequestedComponentInfo != null) { if (info.RequestedComponentInfo.Mapping.Columns.Count > 0) { if (info.Criteria.Count == 0) { if (info.RequestedComponentInfo.Mapping.Transcoding != null) { codeListRetrievalEngine = _transcodedCodeListRetrieval; } else { codeListRetrievalEngine = _localCodeListRetrieval; fallBackSqlBuilder = _componentSqlBuilder; fallBackCodeListRetrievalEngine = _partialCodeListRetrieval; } } else { sqlBuilder = _componentSqlBuilder; codeListRetrievalEngine = _partialCodeListRetrieval; } } else { codeListRetrievalEngine = _constantCodeListRetrieval; } } } if (codeListRetrievalEngine == null) { info.Logger.Warn("|-- Warning: Codes not retrieved. Could not determine which code list retrieval engine to use."); return null; } var codeListBean = Execute(info, codeListRetrievalEngine, sqlBuilder); if ((codeListBean == null || codeListBean.Items.Count == 0) && fallBackCodeListRetrievalEngine != null) { codeListBean = Execute(info, fallBackCodeListRetrievalEngine, fallBackSqlBuilder); } return codeListBean; }
/// <summary> /// This method generates the WHERE part of the SQL query that will be used against the DDB for retrieving the available codes /// </summary> /// <param name="info"> /// The current structure retrieval information /// </param> /// <returns> /// A string containing the WHERE part of the SQL Query or an Empty string /// </returns> protected static string GenerateWhere(StructureRetrievalInfo info) { var sb = new StringBuilder(); int lastClause = 0; foreach (IKeyValues member in info.Criteria) { if (!string.IsNullOrEmpty(member.Id)) { if (member.Id.Equals(info.TimeDimension)) { if (member.Values.Count > 0) { ISdmxDate startDate = new SdmxDateCore(member.Values[0]); ISdmxDate endDate = null; if (member.Values.Count > 1) { endDate = new SdmxDateCore(member.Values[1]); } sb.Append("("); sb.Append( info.TimeTranscoder.GenerateWhere(startDate, endDate, null)); sb.Append(")"); lastClause = sb.Length; sb.Append(" AND "); } } else { ComponentInfo compInfo; if (info.ComponentMapping.TryGetValue(member.Id, out compInfo)) { sb.Append("("); foreach (string value in member.Values) { sb.Append(compInfo.ComponentMapping.GenerateComponentWhere(value)); lastClause = sb.Length; sb.Append(" OR "); } sb.Length = lastClause; if (lastClause > 0) { sb.Append(")"); lastClause = sb.Length; sb.Append(" AND "); } } } } } if (info.ReferencePeriod != null) { // TODO DEPRECIATED. We should not use it. We never did. But leaving it in case a 3rd party client uses it. IReferencePeriodMutableObject time = info.ReferencePeriod; sb.Append("("); sb.Append(info.TimeTranscoder.GenerateWhere(new SdmxDateCore(time.StartTime, TimeFormatEnumType.DateTime), new SdmxDateCore(time.StartTime, TimeFormatEnumType.DateTime), null)); sb.Append(")"); lastClause = sb.Length; } sb.Length = lastClause; if (sb.Length > 0) { return " where " + sb; } return string.Empty; }
/// <summary> /// Generate the SQL for executing on the DDB /// </summary> /// <param name="info"> /// The current structure retrieval information /// </param> /// <returns> /// The generated SQL. /// </returns> public abstract string GenerateSql(StructureRetrievalInfo info);
/// <summary> /// Execute the specified <paramref name="sqlBuilder"/>, if not null and <paramref name="codeListRetrievalEngine"/> /// </summary> /// <param name="info"> /// The current codelist retrieval information /// </param> /// <param name="codeListRetrievalEngine"> /// The code list retrieval engine. /// </param> /// <param name="sqlBuilder"> /// The sql builder. /// </param> /// <returns> /// The partial codelist /// </returns> private static ICodelistMutableObject Execute( StructureRetrievalInfo info, ICodeListRetrievalEngine codeListRetrievalEngine, ISqlBuilder sqlBuilder) { if (sqlBuilder != null) { info.Logger.InfoFormat( CultureInfo.InvariantCulture, "|-- Generating SQL for dissemination database... using sql builder :{0}", sqlBuilder); info.SqlQuery = sqlBuilder.GenerateSql(info); info.Logger.Info("|-- SQL for dissemination database generated:\n" + info.SqlQuery); } info.Logger.Info("|-- Retrieving codes... using codelist retrieval engine " + codeListRetrievalEngine); ICodelistMutableObject codeListBean = codeListRetrievalEngine.GetCodeList(info); if (codeListBean != null) { info.Logger.Info("|-- Codes retrieved successfully, found : " + codeListBean.Items.Count + " codes"); } else { info.Logger.Warn( "|-- Warning: Codes not retrieved. The engine " + codeListRetrievalEngine + " returned no codelist."); } return codeListBean; }
/// <summary> /// Check if the requested Component is set and if the requested codelist ref is for that component /// </summary> /// <param name="codelistRef"> /// The requested codelistRef /// </param> /// <param name="structureRetrievalInfo"> /// The current structure retrieval state /// </param> /// <returns> /// The is requested component codelist. /// </returns> private static bool IsRequestedComponentCodelist( IMaintainableRefObject codelistRef, StructureRetrievalInfo structureRetrievalInfo) { if (structureRetrievalInfo.RequestedComponent == null) { return false; } if (structureRetrievalInfo.RequestedComponent.Equals(structureRetrievalInfo.TimeDimension) && (string.IsNullOrEmpty(codelistRef.MaintainableId) || CustomCodelistConstants.IsTimeDimensionRequest(codelistRef))) { return true; } if (structureRetrievalInfo.MeasureComponent != null && structureRetrievalInfo.MeasureComponent.Equals(structureRetrievalInfo.RequestedComponent)) { return true; } if (structureRetrievalInfo.RequestedComponentInfo != null) { IMaintainableRefObject c = structureRetrievalInfo.CodelistRef; return string.Equals(c.MaintainableId, codelistRef.MaintainableId) && string.Equals(c.AgencyId, codelistRef.AgencyId); } return false; }