From bee4b91d2ddbcdfc3bb79b7bcb5a90285d3ba5fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Thu, 25 Sep 2025 20:23:52 +0200 Subject: [PATCH 01/14] Field value mapping is considered when validating field allowed values --- .../FieldMappingTool/FieldMappingInfo.cs | 49 ++++++++++ .../FieldMappingToolExtentions.cs | 90 +++++++++++++++++++ .../FieldMaps/FieldValueMap.cs | 6 +- .../Tools/TfsWorkItemTypeValidatorTool.cs | 70 +++++++++++++-- .../TfsWorkItemTypeValidatorToolOptions.cs | 2 +- 5 files changed, 203 insertions(+), 14 deletions(-) create mode 100644 src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingInfo.cs create mode 100644 src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingInfo.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingInfo.cs new file mode 100644 index 000000000..d0e2802ea --- /dev/null +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingInfo.cs @@ -0,0 +1,49 @@ +using System; + +namespace MigrationTools.Tools +{ + internal enum FieldMappingType + { + None, + Skip, + SourceToTarget, + TargetToTarget + } + + internal class FieldMappingInfo + { + public static readonly FieldMappingInfo None = new(FieldMappingType.None, string.Empty, string.Empty); + public static readonly FieldMappingInfo Skip = new(FieldMappingType.Skip, string.Empty, string.Empty); + + public FieldMappingInfo CreateSourceToTarget(string sourceFieldName, string targetFieldName) + { + if (string.IsNullOrWhiteSpace(sourceFieldName)) + { + const string msg = $"Source field name cannot be empty for '{nameof(FieldMappingType.SourceToTarget)}' mappping type."; + throw new ArgumentException(nameof(sourceFieldName), msg); + } + return new(FieldMappingType.SourceToTarget, sourceFieldName, targetFieldName); + } + + public FieldMappingInfo CreateTargetToTarget(string sourceFieldName, string targetFieldName) + { + if (string.IsNullOrWhiteSpace(sourceFieldName)) + { + const string msg = $"Source field name cannot be empty for '{nameof(FieldMappingType.TargetToTarget)}' mappping type."; + throw new ArgumentException(nameof(sourceFieldName), msg); + } + return new(FieldMappingType.TargetToTarget, sourceFieldName, targetFieldName); + } + + private FieldMappingInfo(FieldMappingType type, string sourceFieldName, string targetFieldName) + { + MappingType = type; + SourceFieldName = sourceFieldName; + TargetFieldName = targetFieldName; + } + + public FieldMappingType MappingType { get; } + public string SourceFieldName { get; } + public string TargetFieldName { get; } + } +} diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs new file mode 100644 index 000000000..202630b99 --- /dev/null +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using MigrationTools.FieldMaps.AzureDevops.ObjectModel; +using MigrationTools.Tools.Infrastructure; +using MigrationTools.Tools.Interfaces; + +namespace MigrationTools.Tools +{ + internal static class FieldMappingToolExtentions + { + /// + /// Returns all defined field maps of type for work item type . + /// If no field map is defined for given work item type, empty collection is returned. + /// + /// Type of field maps to look for. + /// Field mapping tool. + /// Work item type name. + public static IEnumerable GetFieldMaps( + this IFieldMappingTool fieldMappingTool, + string witName) + where TFieldMap : IFieldMap + { + if (fieldMappingTool.Items.TryGetValue(witName, out List? fieldMaps)) + { + return fieldMaps + .Where(fm => fm is TFieldMap) + .Cast(); + } + return []; + } + + /// + /// Returns defined field maps of type for work item type , + /// which are defined between fields and . + /// + /// Field mapping tool. + /// Work item type name. + /// Source field reference name. + /// Target field reference name. + public static IEnumerable GetFieldValueMaps( + this IFieldMappingTool fieldMappingTool, + string witName, + string sourceFieldReferenceName, + string targetFieldReferenceName) + => fieldMappingTool.GetFieldMaps(witName) + .Where(fvm => sourceFieldReferenceName.Equals(fvm.Config.sourceField, StringComparison.OrdinalIgnoreCase) + && targetFieldReferenceName.Equals(fvm.Config.targetField, StringComparison.OrdinalIgnoreCase)); + + /// + /// Returns all defined value mappings for work item type which are defined between + /// fields and . + /// + /// Field mapping tool. + /// Work item type name. + /// Source field reference name. + /// Target field reference name. + /// Dictionary with mappings source field values to target field values. + /// Thrown when there are defined more than one target values for + /// specific source value. + public static Dictionary GetFieldValueMappings( + this IFieldMappingTool fieldMappingTool, + string witName, + string sourceFieldReferenceName, + string targetFieldReferenceName) + { + Dictionary result = new(StringComparer.OrdinalIgnoreCase); + + IEnumerable fieldValueMaps = fieldMappingTool + .GetFieldValueMaps(witName, sourceFieldReferenceName, targetFieldReferenceName); + foreach (FieldValueMap fieldValueMap in fieldValueMaps) + { + foreach (KeyValuePair map in fieldValueMap.Config.valueMapping) + { + string sourceValue = map.Key; + string targetValue = map.Value; + if (result.TryGetValue(sourceValue, out string existingTargetValue) + && !existingTargetValue.Equals(targetValue, StringComparison.OrdinalIgnoreCase)) + { + string msg = $"Conflict in field value mapping for '{sourceFieldReferenceName}' to '{targetFieldReferenceName}' in '{witName}': " + + $"Value '{sourceValue}' maps to both '{existingTargetValue}' and '{targetValue}'."; + throw new InvalidOperationException(msg); + } + result[sourceValue] = targetValue; + } + } + return result; + } + } +} diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldValueMap.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldValueMap.cs index 93be345cf..3471d652a 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldValueMap.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldValueMap.cs @@ -1,8 +1,6 @@ using System; using Microsoft.Extensions.Logging; using Microsoft.TeamFoundation.WorkItemTracking.Client; -using MigrationTools.Tools.Infrastructure; -using MigrationTools._EngineV1.DataContracts; using MigrationTools.DataContracts; using MigrationTools.Tools; @@ -22,7 +20,7 @@ public FieldValueMap(ILogger logger, ITelemetryLogger telemetryLo { } - private FieldValueMapOptions Config { get { return (FieldValueMapOptions)_Config; } } + internal FieldValueMapOptions Config { get { return (FieldValueMapOptions)_Config; } } public override string MappingDisplayName => $"{Config.sourceField} {Config.targetField}"; @@ -64,4 +62,4 @@ internal override void InternalExecute(WorkItemData source, WorkItemData target) } } } -} \ No newline at end of file +} diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs index 2558fc60d..899d59359 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs @@ -17,15 +17,18 @@ namespace MigrationTools.Tools public class TfsWorkItemTypeValidatorTool : Tool { private readonly IWorkItemTypeMappingTool _witMappingTool; + private readonly CommonTools _commonTools; public TfsWorkItemTypeValidatorTool( IOptions options, IWorkItemTypeMappingTool witMappingTool, + CommonTools commonTools, IServiceProvider services, ILogger logger, ITelemetryLogger telemetry) : base(options, services, logger, telemetry) { + _commonTools = commonTools ?? throw new ArgumentNullException(nameof(commonTools)); _witMappingTool = witMappingTool ?? throw new ArgumentNullException(nameof(witMappingTool)); } @@ -172,7 +175,7 @@ private bool ValidateField(FieldDefinition sourceField, FieldDefinition targetFi ? LogLevel.Information : LogLevel.Warning; bool isValid = ValidateFieldType(sourceField, targetField, logLevel); - isValid &= ValidateFieldAllowedValues(sourceField, targetField, logLevel); + isValid &= ValidateFieldAllowedValues(targetWitName, sourceField, targetField, logLevel); if (isValid) { Log.LogDebug(" Target field '{targetFieldName}' exists in '{targetWit}' and is valid.", @@ -200,7 +203,11 @@ private bool ValidateFieldType(FieldDefinition sourceField, FieldDefinition targ return true; } - private bool ValidateFieldAllowedValues(FieldDefinition sourceField, FieldDefinition targetField, LogLevel logLevel) + private bool ValidateFieldAllowedValues( + string targetWitName, + FieldDefinition sourceField, + FieldDefinition targetField, + LogLevel logLevel) { bool isValid = true; (string sourceValueType, List sourceAllowedValues) = GetAllowedValues(sourceField); @@ -213,23 +220,68 @@ private bool ValidateFieldAllowedValues(FieldDefinition sourceField, FieldDefini + " source = '{sourceFieldAllowedValueType}', target = '{targetFieldAllowedValueType}'.", sourceField.ReferenceName, targetField.ReferenceName, sourceValueType, targetValueType); } - if (!DoesTargetContainsAllSourceValues(sourceAllowedValues, targetAllowedValues)) + if (!ValidateAllowedValues(targetWitName, sourceField.ReferenceName, sourceAllowedValues, + targetField.ReferenceName, targetAllowedValues, out List missingValues)) { isValid = false; Log.Log(logLevel, " Source field '{sourceField}' and target field '{targetField}' have different allowed values.", sourceField.ReferenceName, targetField.ReferenceName); - Log.LogInformation(" Source allowed values: {sourceAllowedValues}", - string.Join(", ", sourceAllowedValues.Select(val => $"'{val}'"))); - Log.LogInformation(" Target allowed values: {targetAllowedValues}", - string.Join(", ", targetAllowedValues.Select(val => $"'{val}'"))); + LogAllowedValues(" Source allowed values: {sourceAllowedValues}", sourceAllowedValues); + LogAllowedValues(" Target allowed values: {targetAllowedValues}", targetAllowedValues); + LogAllowedValues(" Missing values in target are: {missingValues}", missingValues); + Log.LogInformation($" You can configure value mapping using '{nameof(FieldValueMap)}' in '{nameof(FieldMappingTool)}'," + + " or change the process of target system to contain all missing allowed values."); } return isValid; + + void LogAllowedValues(string message, List values) + => Log.LogInformation(message, string.Join(", ", values.Select(value => $"'{value}'"))); } - private bool DoesTargetContainsAllSourceValues(List sourceAllowedValues, List targetAllowedValues) => - sourceAllowedValues.Except(targetAllowedValues, StringComparer.OrdinalIgnoreCase).Count() == 0; + private bool ValidateAllowedValues( + string targetWitName, + string sourceFieldReferenceName, + List sourceAllowedValues, + string targetFieldReferenceName, + List targetAllowedValues, + out List missingValues) + { + missingValues = sourceAllowedValues + .Except(targetAllowedValues, StringComparer.OrdinalIgnoreCase) + .ToList(); + if (missingValues.Count > 0) + { + Log.LogDebug(" Allowed values in target do not match allowed values in source. Checking field value maps."); + Log.LogInformation(" Missing values are: {missingValues}", string.Join(", ", missingValues.Select(val => $"'{val}'"))); + List mappedValues = []; + Dictionary valueMaps = _commonTools.FieldMappingTool + .GetFieldValueMappings(targetWitName, sourceFieldReferenceName, targetFieldReferenceName); + foreach (string missingValue in missingValues) + { + if (valueMaps.TryGetValue(missingValue, out string mappedValue)) + { + if (targetAllowedValues.Contains(mappedValue, StringComparer.OrdinalIgnoreCase)) + { + mappedValues.Add(missingValue); + Log.LogDebug(" Value '{missingValue}' is mapped to '{mappedValue}', which exists in target.", + missingValue, mappedValue); + } + else + { + Log.LogWarning(" Value '{missingValue}' is mapped to '{mappedValue}', which does not exists in target." + + $" This is probably invalid '{nameof(FieldValueMap)}' configuration.", + missingValue, mappedValue); + } + } + } + missingValues = missingValues + .Except(mappedValues, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + return missingValues.Count == 0; + } private (string valueType, List allowedValues) GetAllowedValues(FieldDefinition field) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs index d5028d8ec..d322270a5 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs @@ -14,7 +14,7 @@ public class TfsWorkItemTypeValidatorToolOptions : ToolOptions private static readonly StringComparer _normalizedComparer = StringComparer.OrdinalIgnoreCase; private bool _isNormalized = false; - private static string[] _defaultExcludedWorkItemTypes = [ + private static readonly string[] _defaultExcludedWorkItemTypes = [ "Code Review Request", "Code Review Response", "Feedback Request", From 488198f24c3723216be201b52b8a26aa4354c7de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Tue, 30 Sep 2025 14:50:16 +0200 Subject: [PATCH 02/14] Implement FieldMappingTool.GetFieldMappings --- .../FieldMappingToolExtentions.cs | 11 ++--- .../Tools/MockFieldMappingTool.cs | 12 ++--- src/MigrationTools/Tools/FieldMappingTool.cs | 49 +++++++------------ .../Tools/Interfaces/IFieldMappingTool.cs | 5 +- 4 files changed, 29 insertions(+), 48 deletions(-) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs index 202630b99..abe756a45 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs @@ -21,13 +21,10 @@ public static IEnumerable GetFieldMaps( string witName) where TFieldMap : IFieldMap { - if (fieldMappingTool.Items.TryGetValue(witName, out List? fieldMaps)) - { - return fieldMaps - .Where(fm => fm is TFieldMap) - .Cast(); - } - return []; + List allMaps = fieldMappingTool.GetFieldMappings(witName); + return allMaps + .Where(fm => fm is TFieldMap) + .Cast(); } /// diff --git a/src/MigrationTools.Shadows/Tools/MockFieldMappingTool.cs b/src/MigrationTools.Shadows/Tools/MockFieldMappingTool.cs index 03a2ee962..965fcdd67 100644 --- a/src/MigrationTools.Shadows/Tools/MockFieldMappingTool.cs +++ b/src/MigrationTools.Shadows/Tools/MockFieldMappingTool.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using MigrationTools.DataContracts; +using MigrationTools.DataContracts; using MigrationTools.Tools.Infrastructure; using MigrationTools.Tools.Interfaces; @@ -18,6 +13,11 @@ public void AddFieldMap(string workItemTypeName, IFieldMap fieldToTagFieldMap) throw new NotImplementedException(); } + public List GetFieldMappings(string witName) + { + throw new NotImplementedException(); + } + public void ApplyFieldMappings(WorkItemData source, WorkItemData target) { throw new NotImplementedException(); diff --git a/src/MigrationTools/Tools/FieldMappingTool.cs b/src/MigrationTools/Tools/FieldMappingTool.cs index d6d4eca8a..23b22ff10 100644 --- a/src/MigrationTools/Tools/FieldMappingTool.cs +++ b/src/MigrationTools/Tools/FieldMappingTool.cs @@ -1,16 +1,10 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using MigrationTools._EngineV1.Configuration; -using MigrationTools._EngineV1.Containers; using MigrationTools.DataContracts; -using MigrationTools.Enrichers; -using MigrationTools.Processors; using MigrationTools.Tools.Infrastructure; using MigrationTools.Tools.Interfaces; @@ -22,7 +16,7 @@ namespace MigrationTools.Tools public class FieldMappingTool : Tool, IFieldMappingTool { - private Dictionary> fieldMapps = new Dictionary>(); + private readonly Dictionary> _fieldMaps = new Dictionary>(StringComparer.OrdinalIgnoreCase); /// /// Initializes a new instance of the FieldMappingTool class. @@ -37,7 +31,7 @@ public FieldMappingTool(IOptions options, IServiceProvi { foreach (IFieldMapOptions fieldmapConfig in Options.FieldMaps) { - Log.LogInformation("FieldMappingTool: Adding FieldMap {FieldMapName} for {WorkItemTypeName}", fieldmapConfig.ConfigurationMetadata.OptionFor, fieldmapConfig.ApplyTo.Count == 0? "*ApplyTo is missing*" : string.Join(", ", fieldmapConfig.ApplyTo)); + Log.LogInformation("FieldMappingTool: Adding FieldMap {FieldMapName} for {WorkItemTypeName}", fieldmapConfig.ConfigurationMetadata.OptionFor, fieldmapConfig.ApplyTo.Count == 0 ? "*ApplyTo is missing*" : string.Join(", ", fieldmapConfig.ApplyTo)); string typePattern = $"MigrationTools.Sinks.*.FieldMaps.{fieldmapConfig.ConfigurationMetadata.OptionFor}"; Type type = AppDomain.CurrentDomain.GetAssemblies() @@ -60,13 +54,9 @@ public FieldMappingTool(IOptions options, IServiceProvi } } - public int Count { get { return fieldMapps.Count; } } - - public Dictionary> Items - { - get { return fieldMapps; } - } + public int Count => _fieldMaps.Count; + public Dictionary> Items => _fieldMaps; public void AddFieldMap(string workItemTypeName, IFieldMap fieldToTagFieldMap) { @@ -74,36 +64,31 @@ public void AddFieldMap(string workItemTypeName, IFieldMap fieldToTagFieldMap) { throw new IndexOutOfRangeException("workItemTypeName on all fieldmaps must be set to at least '*'."); } - if (!fieldMapps.ContainsKey(workItemTypeName)) + if (!_fieldMaps.ContainsKey(workItemTypeName)) { - fieldMapps.Add(workItemTypeName, new List()); + _fieldMaps.Add(workItemTypeName, new List()); } - fieldMapps[workItemTypeName].Add(fieldToTagFieldMap); + _fieldMaps[workItemTypeName].Add(fieldToTagFieldMap); } - public void ApplyFieldMappings(WorkItemData source, WorkItemData target) + public List GetFieldMappings(string witName) { - if (fieldMapps.ContainsKey("*")) + if (_fieldMaps.TryGetValue("*", out List fieldMaps)) { - ProcessFieldMapList(source, target, fieldMapps["*"]); + return fieldMaps; } - if (fieldMapps.ContainsKey(source.Fields["System.WorkItemType"].Value.ToString())) + else if (_fieldMaps.TryGetValue(witName, out fieldMaps)) { - ProcessFieldMapList(source, target, fieldMapps[source.Fields["System.WorkItemType"].Value.ToString()]); + return fieldMaps; } + return []; } + public void ApplyFieldMappings(WorkItemData source, WorkItemData target) + => ProcessFieldMapList(source, target, GetFieldMappings(source.Fields["System.WorkItemType"].Value.ToString())); + public void ApplyFieldMappings(WorkItemData target) - { - if (fieldMapps.ContainsKey("*")) - { - ProcessFieldMapList(target, target, fieldMapps["*"]); - } - if (fieldMapps.ContainsKey(target.Fields["System.WorkItemType"].Value.ToString())) - { - ProcessFieldMapList(target, target, fieldMapps[target.Fields["System.WorkItemType"].Value.ToString()]); - } - } + => ApplyFieldMappings(target, target); private void ProcessFieldMapList(WorkItemData source, WorkItemData target, List list) { diff --git a/src/MigrationTools/Tools/Interfaces/IFieldMappingTool.cs b/src/MigrationTools/Tools/Interfaces/IFieldMappingTool.cs index d7645bc3a..d198da393 100644 --- a/src/MigrationTools/Tools/Interfaces/IFieldMappingTool.cs +++ b/src/MigrationTools/Tools/Interfaces/IFieldMappingTool.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; +using System.Collections.Generic; using MigrationTools.DataContracts; using MigrationTools.Tools.Infrastructure; @@ -11,6 +9,7 @@ public interface IFieldMappingTool Dictionary> Items { get; } void AddFieldMap(string workItemTypeName, IFieldMap fieldToTagFieldMap); + List GetFieldMappings(string witName); void ApplyFieldMappings(WorkItemData source, WorkItemData target); void ApplyFieldMappings(WorkItemData target); } From 85dce30f05e22ed23efbee129116fa7c60c80ba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Tue, 30 Sep 2025 20:03:42 +0200 Subject: [PATCH 03/14] Improve getting list of work item types to validate --- .../Tools/TfsWorkItemTypeValidatorTool.cs | 93 +++++++++++-------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs index 899d59359..1edb1f9c2 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs @@ -16,6 +16,22 @@ namespace MigrationTools.Tools /// public class TfsWorkItemTypeValidatorTool : Tool { + private class WitMapping + { + public WitMapping(WorkItemType sourceWit, string expectedTargetWitName, WorkItemType? targetWit, bool isMapped) + { + SourceWit = sourceWit; + ExpectedTargetWitName = expectedTargetWitName; + TargetWit = targetWit; + IsMapped = isMapped; + } + + public WorkItemType SourceWit { get; } + public string ExpectedTargetWitName { get; } + public WorkItemType? TargetWit { get; } + public bool IsMapped { get; } + } + private readonly IWorkItemTypeMappingTool _witMappingTool; private readonly CommonTools _commonTools; @@ -40,17 +56,22 @@ public bool ValidateReflectedWorkItemIdField( Log.LogInformation("Validating presence of reflected work item ID field '{reflectedWorkItemIdField}'" + " in target work item types.", reflectedWorkItemIdField); bool isValid = true; - List wits = GetTargetWitsToValidate(sourceWits, targetWits); - foreach (WorkItemType targetWit in wits) + List witPairs = GetWitsToValidate(sourceWits, targetWits); + foreach (WitMapping witPair in witPairs) { + WorkItemType? targetWit = witPair.TargetWit; + if (targetWit is null) + { + continue; + } if (targetWit.FieldDefinitions.Contains(reflectedWorkItemIdField)) { - Log.LogDebug(" '{targetWit}' contains reflected work item ID field '{fieldName}'.", + Log.LogDebug("'{targetWit}' contains reflected work item ID field '{fieldName}'.", targetWit.Name, reflectedWorkItemIdField); } else { - Log.LogError(" '{targetWit}' does not contain reflected work item ID field '{fieldName}'.", + Log.LogError("'{targetWit}' does not contain reflected work item ID field '{fieldName}'.", targetWit.Name, reflectedWorkItemIdField); isValid = false; } @@ -59,25 +80,29 @@ public bool ValidateReflectedWorkItemIdField( return isValid; } - private List GetTargetWitsToValidate(List sourceWits, List targetWits) + // Returns list of target work item types with respect to work item type mapping. + private List GetWitsToValidate(List sourceWits, List targetWits) { - List targetWitsToValidate = []; + List witMappings = []; foreach (WorkItemType sourceWit in sourceWits) { - string sourceWitName = sourceWit.Name; - if (!ShouldValidateWorkItemType(sourceWitName)) - { - continue; - } - string targetWitName = GetTargetWorkItemType(sourceWitName); - WorkItemType targetWit = targetWits - .FirstOrDefault(wit => wit.Name.Equals(targetWitName, StringComparison.OrdinalIgnoreCase)); - if (targetWit is not null) + if (ShouldValidateWorkItemType(sourceWit.Name)) { - targetWitsToValidate.Add(targetWit); + bool isMapped = false; + if (_witMappingTool.Mappings.TryGetValue(sourceWit.Name, out string targetWitName)) + { + isMapped = true; + } + else + { + targetWitName = sourceWit.Name; + } + WorkItemType? targetWit = targetWits + .FirstOrDefault(wit => wit.Name.Equals(targetWitName, StringComparison.OrdinalIgnoreCase)); + witMappings.Add(new WitMapping(sourceWit, targetWitName, targetWit, isMapped)); } } - return targetWitsToValidate; + return witMappings; } public bool ValidateWorkItemTypes(List sourceWits, List targetWits) @@ -86,20 +111,19 @@ public bool ValidateWorkItemTypes(List sourceWits, List targetWitNames = targetWits.Select(wit => wit.Name).ToList(); bool isValid = true; - foreach (WorkItemType sourceWit in sourceWits) + List witPairs = GetWitsToValidate(sourceWits, targetWits); + foreach (WitMapping witPair in witPairs) { - string sourceWitName = sourceWit.Name; - if (!ShouldValidateWorkItemType(sourceWitName)) + string sourceWitName = witPair.SourceWit.Name; + Log.LogInformation("Validating work item type '{sourceWit}'.", sourceWitName); + if (witPair.IsMapped) { - continue; + Log.LogInformation(" Work item type '{sourceWit}' is mapped to '{targetWit}'.", + sourceWitName, witPair.ExpectedTargetWitName); } - Log.LogInformation("Validating work item type '{sourceWit}'", sourceWitName); - string targetWitName = GetTargetWorkItemType(sourceWitName); - WorkItemType targetWit = targetWits - .FirstOrDefault(wit => wit.Name.Equals(targetWitName, StringComparison.OrdinalIgnoreCase)); - if (targetWit is null) + if (witPair.TargetWit is null) { - Log.LogWarning("Work item type '{targetWit}' does not exist in target system.", targetWitName); + Log.LogWarning("Work item type '{targetWit}' does not exist in target system.", witPair.ExpectedTargetWitName); if (TryFindSimilarWorkItemType(sourceWitName, targetWitNames, out string suggestedName)) { Log.LogInformation(" Suggested mapping: '{0}' – '{1}'", sourceWitName, suggestedName); @@ -108,7 +132,7 @@ public bool ValidateWorkItemTypes(List sourceWits, List sourceWits, ICollection targetWits) { Log.LogInformation( From afbe02006cf0b03a6808c2b42fb27841a5aea15b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Tue, 30 Sep 2025 20:15:06 +0200 Subject: [PATCH 04/14] Order work item types by name to improve log reading --- .../Processors/TfsWorkItemMigrationProcessor.cs | 2 ++ .../Processors/TfsWorkItemTypeValidatorProcessor.cs | 2 ++ .../Tools/TfsWorkItemTypeValidatorTool.cs | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessor.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessor.cs index e3b39707c..3355c9ed9 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessor.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemMigrationProcessor.cs @@ -259,11 +259,13 @@ private void ValidateWorkItemTypes() .ToProject() .WorkItemTypes .Cast() + .OrderBy(wit => wit.Name) .ToList(); var targetWits = Target.WorkItems.Project .ToProject() .WorkItemTypes .Cast() + .OrderBy(wit => wit.Name) .ToList(); // Reflected work item ID field is mandatory for migration, so it is validated even if the validator tool is disabled. diff --git a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemTypeValidatorProcessor.cs b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemTypeValidatorProcessor.cs index 6d44d49bb..8d80761af 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemTypeValidatorProcessor.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Processors/TfsWorkItemTypeValidatorProcessor.cs @@ -41,11 +41,13 @@ protected override void InternalExecute() .ToProject() .WorkItemTypes .Cast() + .OrderBy(wit => wit.Name) .ToList(); List targetWits = Target.WorkItems.Project .ToProject() .WorkItemTypes .Cast() + .OrderBy(wit => wit.Name) .ToList(); bool containsReflectedWorkItemId = CommonTools.WorkItemTypeValidatorTool .ValidateReflectedWorkItemIdField(sourceWits, targetWits, Target.Options.ReflectedWorkItemIdField); diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs index 1edb1f9c2..d5f60d124 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs @@ -64,6 +64,11 @@ public bool ValidateReflectedWorkItemIdField( { continue; } + if (witPair.IsMapped) + { + Log.LogInformation("Work item type '{sourceWit}' is mapped to '{targetWit}'.", + witPair.SourceWit.Name, witPair.ExpectedTargetWitName); + } if (targetWit.FieldDefinitions.Contains(reflectedWorkItemIdField)) { Log.LogDebug("'{targetWit}' contains reflected work item ID field '{fieldName}'.", From b4960cc172b1188aef9b701cc4c0312bf475628a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Tue, 30 Sep 2025 21:14:09 +0200 Subject: [PATCH 05/14] Respect field to field mappings for field validation --- .../FieldMappingToolExtentions.cs | 14 ++ .../FieldMaps/FieldToFieldMap.cs | 15 +-- .../Tools/TfsWorkItemTypeValidatorTool.cs | 126 ++++++++++++------ 3 files changed, 105 insertions(+), 50 deletions(-) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs index abe756a45..585554d08 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs @@ -27,6 +27,20 @@ public static IEnumerable GetFieldMaps( .Cast(); } + public static IEnumerable GetFieldToFieldMaps( + this IFieldMappingTool fieldMappingTool, + string witName, + string sourceFieldReferenceName, + FieldMapMode? mapMode) + { + IEnumerable allMaps = fieldMappingTool.GetFieldMaps(witName) + .Where(fvm => sourceFieldReferenceName.Equals(fvm.Config.sourceField, StringComparison.OrdinalIgnoreCase)); + + return mapMode.HasValue + ? allMaps.Where(fvm => fvm.Config.fieldMapMode == mapMode.Value) + : allMaps; + } + /// /// Returns defined field maps of type for work item type , /// which are defined between fields and . diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldToFieldMap.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldToFieldMap.cs index a9b17aaa1..bbaf17410 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldToFieldMap.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldToFieldMap.cs @@ -1,13 +1,8 @@ using System; -using System.Windows.Forms; using Microsoft.Extensions.Logging; -using Microsoft.IdentityModel.Tokens; using Microsoft.TeamFoundation.WorkItemTracking.Client; -using MigrationTools.DataContracts.Pipelines; using MigrationTools.Tools; using MigrationTools.Tools.Infrastructure; -using static System.Windows.Forms.VisualStyles.VisualStyleElement.TextBox; -using static Microsoft.TeamFoundation.Client.CommandLine.Options; namespace MigrationTools.FieldMaps.AzureDevops.ObjectModel; @@ -19,7 +14,7 @@ namespace MigrationTools.FieldMaps.AzureDevops.ObjectModel; public class FieldToFieldMap : FieldMapBase { public override string MappingDisplayName => $"{Config.sourceField} {Config.targetField}"; - private FieldToFieldMapOptions Config => (FieldToFieldMapOptions)_Config; + internal FieldToFieldMapOptions Config => (FieldToFieldMapOptions)_Config; /// /// Initializes a new instance of the FieldToFieldMap class. @@ -87,13 +82,13 @@ private bool IsValid(WorkItem source, WorkItem target) case FieldMapMode.SourceToTarget: if (!source.Fields.Contains(Config.sourceField)) { - Log.LogWarning("FieldToFieldMap: [VALIDATION FAILED] Source field '{SourceField}' does not exist on source WorkItem {SourceId}. Please verify the field name is correct and exists in the source work item type. Available fields can be checked in Azure DevOps work item customization.", + Log.LogWarning("FieldToFieldMap: [VALIDATION FAILED] Source field '{SourceField}' does not exist on source WorkItem {SourceId}. Please verify the field name is correct and exists in the source work item type. Available fields can be checked in Azure DevOps work item customization.", Config.sourceField, source.Id); valid = false; } if (!target.Fields.Contains(Config.targetField)) { - Log.LogWarning("FieldToFieldMap: [VALIDATION FAILED] Target field '{TargetField}' does not exist on target WorkItem {TargetId}. Please verify the field name is correct and exists in the target work item type. You may need to add this field to the target work item type or update your field mapping configuration.", + Log.LogWarning("FieldToFieldMap: [VALIDATION FAILED] Target field '{TargetField}' does not exist on target WorkItem {TargetId}. Please verify the field name is correct and exists in the target work item type. You may need to add this field to the target work item type or update your field mapping configuration.", Config.targetField, target.Id); valid = false; } @@ -101,13 +96,13 @@ private bool IsValid(WorkItem source, WorkItem target) case FieldMapMode.TargetToTarget: if (!target.Fields.Contains(Config.sourceField)) { - Log.LogWarning("FieldToFieldMap: [VALIDATION FAILED] Source field '{SourceField}' does not exist on target WorkItem {TargetId}. In TargetToTarget mode, both source and target fields must exist on the target work item. Please verify the source field name is correct.", + Log.LogWarning("FieldToFieldMap: [VALIDATION FAILED] Source field '{SourceField}' does not exist on target WorkItem {TargetId}. In TargetToTarget mode, both source and target fields must exist on the target work item. Please verify the source field name is correct.", Config.sourceField, target.Id); valid = false; } if (!target.Fields.Contains(Config.targetField)) { - Log.LogWarning("FieldToFieldMap: [VALIDATION FAILED] Target field '{TargetField}' does not exist on target WorkItem {TargetId}. In TargetToTarget mode, both source and target fields must exist on the target work item. Please verify the target field name is correct.", + Log.LogWarning("FieldToFieldMap: [VALIDATION FAILED] Target field '{TargetField}' does not exist on target WorkItem {TargetId}. In TargetToTarget mode, both source and target fields must exist on the target work item. Please verify the target field name is correct.", Config.targetField, target.Id); valid = false; } diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs index d5f60d124..b875bf87f 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs @@ -18,7 +18,11 @@ public class TfsWorkItemTypeValidatorTool : Tool TargetWit is not null; + public bool IsMapped { get; } + } + + private class FieldMapping + { + public FieldMapping( + FieldDefinition sourceField, + string expectedTargetFieldName, + FieldDefinition? targetField, + bool isMapped) + { + SourceField = sourceField; + ExpectedTargetFieldName = expectedTargetFieldName; + TargetField = targetField; + IsMapped = isMapped; + } + + public FieldDefinition SourceField { get; } + public string ExpectedTargetFieldName { get; } + public FieldDefinition? TargetField { get; } + public bool HasTargetField => TargetField is not null; public bool IsMapped { get; } } @@ -59,11 +85,11 @@ public bool ValidateReflectedWorkItemIdField( List witPairs = GetWitsToValidate(sourceWits, targetWits); foreach (WitMapping witPair in witPairs) { - WorkItemType? targetWit = witPair.TargetWit; - if (targetWit is null) + if (!witPair.HasTargetWit) { continue; } + WorkItemType targetWit = witPair.TargetWit; if (witPair.IsMapped) { Log.LogInformation("Work item type '{sourceWit}' is mapped to '{targetWit}'.", @@ -126,7 +152,7 @@ public bool ValidateWorkItemTypes(List sourceWits, List f.ReferenceName, f => f, StringComparer.OrdinalIgnoreCase); foreach (FieldDefinition sourceField in sourceFields) { - string sourceFieldName = sourceField.ReferenceName; - string targetFieldName = GetTargetFieldName(targetWit.Name, sourceFieldName); - if (string.IsNullOrEmpty(targetFieldName)) - { - continue; - } - - if (targetFields.ContainsKey(targetFieldName)) + List fieldsToValidate = GetTargetFieldsToValidate(targetWit.Name, sourceField, targetFields); + foreach (FieldMapping fieldPair in fieldsToValidate) { - if (sourceField.IsIdentity) + if (fieldPair.IsMapped) { - const string message = " Source field '{sourceFieldName}' is identity field." - + " Validation is not performed on identity fields, because they usually differ in allowed values."; - Log.LogDebug(message, sourceFieldName); + Log.LogInformation(" Source field '{sourceFieldName}' is mapped to '{targetFieldName}'.", + fieldPair.SourceField.ReferenceName, fieldPair.ExpectedTargetFieldName); + } + if (fieldPair.HasTargetField) + { + if (sourceField.IsIdentity) + { + const string message = " Source field '{sourceFieldName}' is identity field." + + " Validation is not performed on identity fields, because they usually differ in allowed values."; + Log.LogDebug(message, fieldPair.SourceField.ReferenceName); + } + else + { + result &= ValidateField(sourceField, fieldPair.TargetField, targetWit.Name); + } } else { - result &= ValidateField(sourceField, targetFields[targetFieldName], targetWit.Name); + LogMissingField(targetWit, sourceField, fieldPair.ExpectedTargetFieldName); + result = false; } } - else - { - Log.LogWarning(" Missing field '{targetFieldName}' in '{targetWit}'.", targetFieldName, targetWit.Name); - Log.LogInformation(" Source field reference name: {sourceFieldReferenceName}", sourceFieldName); - Log.LogInformation(" Source field name: {sourceFieldName}", sourceField.Name); - Log.LogInformation(" Field type: {fieldType}", sourceField.FieldType); - (string valueType, List allowedValues) = GetAllowedValues(sourceField); - Log.LogInformation(" Allowed values: {allowedValues}", string.Join(", ", allowedValues.Select(v => $"'{v}'"))); - Log.LogInformation(" Allowed values type: {allowedValuesType}", valueType); - result = false; - } } if (result) { - Log.LogInformation(" All fields are either present or mapped."); + Log.LogInformation(" All fields are either present or mapped for '{targetWit}'.", targetWit.Name); } return result; } + private List GetTargetFieldsToValidate( + string targetWitName, + FieldDefinition sourceField, + Dictionary targetFields) + { + List result = []; + + foreach (FieldToFieldMap fieldToFieldMap in _commonTools.FieldMappingTool + .GetFieldToFieldMaps(targetWitName, sourceField.ReferenceName, FieldMapMode.SourceToTarget)) + { + targetFields.TryGetValue(fieldToFieldMap.Config.targetField, out FieldDefinition targetField); + result.Add(new FieldMapping(sourceField, fieldToFieldMap.Config.targetField, targetField, true)); + } + + if (result.Count == 0) + { + // If no field to field mapping is configured, just use the same field in target. + targetFields.TryGetValue(sourceField.ReferenceName, out FieldDefinition targetField); + result.Add(new FieldMapping(sourceField, sourceField.ReferenceName, targetField, false)); + } + + return result; + } + + private void LogMissingField(WorkItemType targetWit, FieldDefinition sourceField, string targetFieldName) + { + Log.LogWarning(" Missing field '{targetFieldName}' in '{targetWit}'.", targetFieldName, targetWit.Name); + Log.LogInformation(" Source field reference name: {sourceFieldReferenceName}", sourceField.ReferenceName); + Log.LogInformation(" Source field name: {sourceFieldName}", sourceField.Name); + Log.LogInformation(" Field type: {fieldType}", sourceField.FieldType); + (string valueType, List allowedValues) = GetAllowedValues(sourceField); + Log.LogInformation(" Allowed values: {allowedValues}", string.Join(", ", allowedValues.Select(v => $"'{v}'"))); + Log.LogInformation(" Allowed values type: {allowedValuesType}", valueType); + } + private bool ValidateField(FieldDefinition sourceField, FieldDefinition targetField, string targetWitName) { // If target field is in 'FixedTargetFields' list, it means, that user resolved this filed somehow. @@ -345,17 +402,6 @@ private bool ShouldValidateWorkItemType(string workItemTypeName) return true; } - private string GetTargetWorkItemType(string sourceWit) - { - string targetWit = sourceWit; - if (_witMappingTool.Mappings.ContainsKey(sourceWit)) - { - targetWit = _witMappingTool.Mappings[sourceWit]; - Log.LogInformation(" This work item type is mapped to '{targetWit}' in target.", targetWit); - } - return targetWit; - } - private void LogWorkItemTypes(ICollection sourceWits, ICollection targetWits) { Log.LogInformation( From b86c7a96928048f27672b88909e05ca0c5ef7a5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Wed, 1 Oct 2025 16:32:32 +0200 Subject: [PATCH 06/14] Respect FieldToFieldMultiMap configuration --- .../FieldMappingToolExtentions.cs | 28 +++- .../FieldMaps/FieldtoFieldMultiMap.cs | 5 +- .../Tools/TfsWorkItemTypeValidatorTool.cs | 131 ++++++++++++------ 3 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs index 585554d08..a4ae60e1b 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs @@ -41,6 +41,29 @@ public static IEnumerable GetFieldToFieldMaps( : allMaps; } + public static IEnumerable<(string sourceFieldName, string targetFieldName)> GetFieldToFieldMultiMaps( + this IFieldMappingTool fieldMappingTool, + string witName, + string sourceFieldReferenceName) + { + List<(string, string)> fieldMaps = []; + + foreach (FieldToFieldMultiMap multiMap in fieldMappingTool.GetFieldMaps(witName)) + { + // Iterating through dictionary to be able to ignore casing of field names. + foreach (KeyValuePair mapping in multiMap.Config.SourceToTargetMappings) + { + if (sourceFieldReferenceName.Equals(mapping.Key, StringComparison.OrdinalIgnoreCase)) + { + fieldMaps.Add((mapping.Key, mapping.Value)); + break; + } + } + } + + return fieldMaps; + } + /// /// Returns defined field maps of type for work item type , /// which are defined between fields and . @@ -88,8 +111,9 @@ public static Dictionary GetFieldValueMappings( if (result.TryGetValue(sourceValue, out string existingTargetValue) && !existingTargetValue.Equals(targetValue, StringComparison.OrdinalIgnoreCase)) { - string msg = $"Conflict in field value mapping for '{sourceFieldReferenceName}' to '{targetFieldReferenceName}' in '{witName}': " - + $"Value '{sourceValue}' maps to both '{existingTargetValue}' and '{targetValue}'."; + string msg = $"Conflict in field value mapping for field '{sourceFieldReferenceName}'" + + $" to field '{targetFieldReferenceName}' in work item type '{witName}':" + + $" Value '{sourceValue}' maps to both '{existingTargetValue}' and '{targetValue}'."; throw new InvalidOperationException(msg); } result[sourceValue] = targetValue; diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldtoFieldMultiMap.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldtoFieldMultiMap.cs index 68b37b378..205aeeb66 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldtoFieldMultiMap.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMaps/FieldtoFieldMultiMap.cs @@ -1,7 +1,6 @@ using System.Collections.Generic; using Microsoft.Extensions.Logging; using Microsoft.TeamFoundation.WorkItemTracking.Client; -using MigrationTools._EngineV1.Configuration; using MigrationTools.Tools; using MigrationTools.Tools.Infrastructure; @@ -14,7 +13,7 @@ public FieldToFieldMultiMap(ILogger logger, ITelemetryLogg } public override string MappingDisplayName => string.Empty; - private FieldToFieldMultiMapOptions Config { get { return (FieldToFieldMultiMapOptions)_Config; } } + internal FieldToFieldMultiMapOptions Config { get { return (FieldToFieldMultiMapOptions)_Config; } } public override void Configure(IFieldMapOptions config) { @@ -57,4 +56,4 @@ private void mapFields(Dictionary fieldMap, WorkItem source, Wor } } } -} \ No newline at end of file +} diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs index b875bf87f..b4aa5dfb0 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs @@ -146,7 +146,7 @@ public bool ValidateWorkItemTypes(List sourceWits, List sourceWits, List fieldsToValidate = GetTargetFieldsToValidate(targetWit.Name, sourceField, targetFields); foreach (FieldMapping fieldPair in fieldsToValidate) { + Log.LogInformation(" Validating source field '{sourceFieldReferenceName}' ({sourceFieldName}).", + sourceField.ReferenceName, sourceField.Name); if (fieldPair.IsMapped) { - Log.LogInformation(" Source field '{sourceFieldName}' is mapped to '{targetFieldName}'.", + Log.LogInformation(" Source field '{sourceFieldName}' is mapped to '{targetFieldName}'.", fieldPair.SourceField.ReferenceName, fieldPair.ExpectedTargetFieldName); } if (fieldPair.HasTargetField) { if (sourceField.IsIdentity) { - const string message = " Source field '{sourceFieldName}' is identity field." + const string message = " Source field '{sourceFieldName}' is identity field." + " Validation is not performed on identity fields, because they usually differ in allowed values."; Log.LogDebug(message, fieldPair.SourceField.ReferenceName); } @@ -231,9 +234,16 @@ private List GetTargetFieldsToValidate( result.Add(new FieldMapping(sourceField, fieldToFieldMap.Config.targetField, targetField, true)); } + foreach ((string _, string targetFieldName) in _commonTools.FieldMappingTool + .GetFieldToFieldMultiMaps(targetWitName, sourceField.ReferenceName)) + { + targetFields.TryGetValue(targetFieldName, out FieldDefinition targetField); + result.Add(new FieldMapping(sourceField, targetFieldName, targetField, true)); + } + if (result.Count == 0) { - // If no field to field mapping is configured, just use the same field in target. + // If no field mapping is configured for this source field, just use the same field in the target. targetFields.TryGetValue(sourceField.ReferenceName, out FieldDefinition targetField); result.Add(new FieldMapping(sourceField, sourceField.ReferenceName, targetField, false)); } @@ -243,35 +253,37 @@ private List GetTargetFieldsToValidate( private void LogMissingField(WorkItemType targetWit, FieldDefinition sourceField, string targetFieldName) { - Log.LogWarning(" Missing field '{targetFieldName}' in '{targetWit}'.", targetFieldName, targetWit.Name); - Log.LogInformation(" Source field reference name: {sourceFieldReferenceName}", sourceField.ReferenceName); - Log.LogInformation(" Source field name: {sourceFieldName}", sourceField.Name); - Log.LogInformation(" Field type: {fieldType}", sourceField.FieldType); + Log.LogWarning(" Missing field '{targetFieldName}' in '{targetWit}'.", targetFieldName, targetWit.Name); + Log.LogInformation(" Source field reference name: {sourceFieldReferenceName}", sourceField.ReferenceName); + Log.LogInformation(" Source field name: {sourceFieldName}", sourceField.Name); + Log.LogInformation(" Field type: {fieldType}", sourceField.FieldType); (string valueType, List allowedValues) = GetAllowedValues(sourceField); - Log.LogInformation(" Allowed values: {allowedValues}", string.Join(", ", allowedValues.Select(v => $"'{v}'"))); - Log.LogInformation(" Allowed values type: {allowedValuesType}", valueType); + LogAllowedValues(" Allowed values: {allowedValues}", allowedValues); + Log.LogInformation(" Allowed values type: {allowedValuesType}", valueType); } - private bool ValidateField(FieldDefinition sourceField, FieldDefinition targetField, string targetWitName) + private bool ValidateField(FieldDefinition sourceField, FieldDefinition? targetField, string targetWitName) { - // If target field is in 'FixedTargetFields' list, it means, that user resolved this filed somehow. - // For example by value mapping. So any discrepancies found will be logged just as information. - // Otherwise, discrepancies are logged as warning. - LogLevel logLevel = Options.IsFieldFixed(targetWitName, targetField.ReferenceName) - ? LogLevel.Information - : LogLevel.Warning; + //// If target field is in 'FixedTargetFields' list, it means, that user resolved this filed somehow. + //// For example by value mapping. So any discrepancies found will be logged just as information. + //// Otherwise, discrepancies are logged as warning. + //LogLevel logLevel = Options.IsFieldFixed(targetWitName, targetField.ReferenceName) + // ? LogLevel.Information + // : LogLevel.Warning; + LogLevel logLevel = LogLevel.Warning; + bool isValid = ValidateFieldType(sourceField, targetField, logLevel); isValid &= ValidateFieldAllowedValues(targetWitName, sourceField, targetField, logLevel); if (isValid) { - Log.LogDebug(" Target field '{targetFieldName}' exists in '{targetWit}' and is valid.", - targetField.ReferenceName, targetWitName); + Log.LogDebug(" Target field '{targetFieldReferenceName}' ({targetFieldName}) exists in '{targetWit}' and is valid.", + targetField.ReferenceName, targetField.Name, targetWitName); } else if (logLevel == LogLevel.Information) { - Log.LogInformation(" Target field '{targetFieldName}' in '{targetWit}' is considered valid," - + $" because it is listed in '{nameof(Options.FixedTargetFields)}'.", - targetField.ReferenceName, targetWitName, sourceField.ReferenceName); + //Log.LogInformation(" Target field '{targetFieldName}' in '{targetWit}' is considered valid," + // + $" because it is listed in '{nameof(Options.FixedTargetFields)}'.", + // targetField.ReferenceName, targetWitName, sourceField.ReferenceName); } return (logLevel == LogLevel.Information) || isValid; } @@ -281,7 +293,7 @@ private bool ValidateFieldType(FieldDefinition sourceField, FieldDefinition targ if (sourceField.FieldType != targetField.FieldType) { Log.Log(logLevel, - " Source field '{sourceField}' and target field '{targetField}' have different types:" + " Source field '{sourceField}' and target field '{targetField}' have different types:" + " source = '{sourceFieldType}', target = '{targetFieldType}'.", sourceField.ReferenceName, targetField.ReferenceName, sourceField.FieldType, targetField.FieldType); return false; @@ -302,7 +314,7 @@ private bool ValidateFieldAllowedValues( { isValid = false; Log.Log(logLevel, - " Source field '{sourceField}' and target field '{targetField}' have different allowed values types:" + " Source field '{sourceField}' and target field '{targetField}' have different allowed values types:" + " source = '{sourceFieldAllowedValueType}', target = '{targetFieldAllowedValueType}'.", sourceField.ReferenceName, targetField.ReferenceName, sourceValueType, targetValueType); } @@ -311,21 +323,20 @@ private bool ValidateFieldAllowedValues( { isValid = false; Log.Log(logLevel, - " Source field '{sourceField}' and target field '{targetField}' have different allowed values.", + " Source field '{sourceField}' and target field '{targetField}' have different allowed values.", sourceField.ReferenceName, targetField.ReferenceName); - LogAllowedValues(" Source allowed values: {sourceAllowedValues}", sourceAllowedValues); - LogAllowedValues(" Target allowed values: {targetAllowedValues}", targetAllowedValues); - LogAllowedValues(" Missing values in target are: {missingValues}", missingValues); - Log.LogInformation($" You can configure value mapping using '{nameof(FieldValueMap)}' in '{nameof(FieldMappingTool)}'," - + " or change the process of target system to contain all missing allowed values."); + LogAllowedValues(" Source allowed values: {sourceAllowedValues}", sourceAllowedValues); + LogAllowedValues(" Target allowed values: {targetAllowedValues}", targetAllowedValues); + LogAllowedValues(" Missing values in target are: {missingValues}", missingValues); } return isValid; - void LogAllowedValues(string message, List values) - => Log.LogInformation(message, string.Join(", ", values.Select(value => $"'{value}'"))); } + private void LogAllowedValues(string message, List values) + => Log.LogInformation(message, string.Join(", ", values.Select(value => $"'{value}'"))); + private bool ValidateAllowedValues( string targetWitName, string sourceFieldReferenceName, @@ -339,8 +350,8 @@ private bool ValidateAllowedValues( .ToList(); if (missingValues.Count > 0) { - Log.LogDebug(" Allowed values in target do not match allowed values in source. Checking field value maps."); - Log.LogInformation(" Missing values are: {missingValues}", string.Join(", ", missingValues.Select(val => $"'{val}'"))); + Log.LogDebug(" Allowed values in target field do not match allowed values in source. Checking field value maps."); + LogAllowedValues(" Missing values are: {missingValues}", missingValues); List mappedValues = []; Dictionary valueMaps = _commonTools.FieldMappingTool .GetFieldValueMappings(targetWitName, sourceFieldReferenceName, targetFieldReferenceName); @@ -351,12 +362,12 @@ private bool ValidateAllowedValues( if (targetAllowedValues.Contains(mappedValue, StringComparer.OrdinalIgnoreCase)) { mappedValues.Add(missingValue); - Log.LogDebug(" Value '{missingValue}' is mapped to '{mappedValue}', which exists in target.", + Log.LogDebug(" Value '{missingValue}' is mapped to '{mappedValue}', which exists in target.", missingValue, mappedValue); } else { - Log.LogWarning(" Value '{missingValue}' is mapped to '{mappedValue}', which does not exists in target." + Log.LogWarning(" Value '{missingValue}' is mapped to '{mappedValue}', which does not exists in target." + $" This is probably invalid '{nameof(FieldValueMap)}' configuration.", missingValue, mappedValue); } @@ -378,6 +389,7 @@ private bool ValidateAllowedValues( { allowedValues.Add(field.AllowedValues[i]); } + allowedValues.Sort(StringComparer.OrdinalIgnoreCase); return (valueType, allowedValues); } @@ -435,16 +447,43 @@ private void LogValidationResult(bool isValid) return; } - const string message1 = "Some work item types or their fields are not present in the target system (see previous logs)." - + " Either add these fields into target work items, or map source fields to other target fields" - + $" in options ({nameof(TfsWorkItemTypeValidatorToolOptions.SourceFieldMappings)})."; - Log.LogError(message1); - const string message2 = "If you have some field mappings defined for validation, do not forget also to configure" - + $" proper field mapping in {nameof(FieldMappingTool)} so data will preserved during migration."; - Log.LogInformation(message2); - const string message3 = "If you have different allowed values in some field, either update target field to match" - + $" allowed values from source, or configure {nameof(FieldValueMap)} in {nameof(FieldMappingTool)}."; - Log.LogInformation(message3); + //const string msg1 = "Some work item types or their fields are not valid in the target system (see previous logs)." + // + " Either add these fields into target work items, or map source fields to other target fields" + // + $" in options ({nameof(TfsWorkItemTypeValidatorToolOptions.SourceFieldMappings)})."; + Log.LogError("Some work item types or their fields are not valid in the target system (see previous logs)."); + + Log.LogInformation("If the work item type does not exist in target system, you can:"); + Log.LogInformation(" - Create it there."); + Log.LogInformation(" - Configure mapping to another work item type which exists in target using" + + $" '{nameof(WorkItemTypeMappingTool)}' configuration."); + Log.LogInformation(" - Exclude it from validation. To configure which work item types are validated, use either" + + $" '{nameof(TfsWorkItemTypeValidatorToolOptions.IncludeWorkItemtypes)}' or" + + $" '{nameof(TfsWorkItemTypeValidatorToolOptions.ExcludeWorkItemtypes)}'" + + $" of '{nameof(TfsWorkItemTypeValidatorTool)}' configuration (but not both at the same time)."); + + Log.LogInformation("If field is missing in target, you can:"); + Log.LogInformation(" - Add missing field to the target work item type."); + Log.LogInformation($" - Configure field mapping using '{nameof(FieldToFieldMultiMap)}'" + + $" in '{nameof(FieldMappingTool)}' configuration." + + " This is simpler method for source to target field mapping, which allows to map multiple fields at once."); + Log.LogInformation($" - Configure field mapping using '{nameof(FieldToFieldMap)}'" + + $" in '{nameof(FieldMappingTool)}' configuration." + + $" Mapping mode must be of type '{nameof(FieldMapMode.SourceToTarget)}'."); + + Log.LogInformation("If allowed values of the source and target fields do not match, you can:"); + Log.LogInformation(" - Add missing allowed values to the target field."); + Log.LogInformation($" - Configure value mapping. Add field maps of type '{nameof(FieldValueMap)}'" + + $" to '{nameof(FieldMappingTool)}' configuration."); + + //const string message2 = "If you have some field mappings defined for validation, do not forget also to configure" + // + $" proper field mapping in {nameof(FieldMappingTool)} so data will preserved during migration."; + //Log.LogInformation(message2); + //const string message3 = "If you have different allowed values in some field, either update target field to match" + // + $" allowed values from source, or configure {nameof(FieldValueMap)} in {nameof(FieldMappingTool)}."; + //Log.LogInformation(message3); + + //Log.LogInformation($" You can configure value mapping using '{nameof(FieldValueMap)}' in '{nameof(FieldMappingTool)}'," + // + " or change the process of target system to contain all missing allowed values."); } private static bool TryFindSimilarWorkItemType( From 1ca7b77d89031bca38c43b0f70bf430399b2c835 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Wed, 1 Oct 2025 16:48:23 +0200 Subject: [PATCH 07/14] Fix property name casing --- .../Tools/TfsWorkItemTypeValidatorTool.cs | 12 ++++++------ .../Tools/TfsWorkItemTypeValidatorToolOptions.cs | 12 +++++++----- .../TfsWorkItemTypeValidatorToolOptionsValidator.cs | 8 ++++---- 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs index b4aa5dfb0..953a260b8 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs @@ -395,16 +395,16 @@ private bool ValidateAllowedValues( private bool ShouldValidateWorkItemType(string workItemTypeName) { - if ((Options.IncludeWorkItemtypes.Count > 0) - && !Options.IncludeWorkItemtypes.Contains(workItemTypeName, StringComparer.OrdinalIgnoreCase)) + if ((Options.IncludeWorkItemTypes.Count > 0) + && !Options.IncludeWorkItemTypes.Contains(workItemTypeName, StringComparer.OrdinalIgnoreCase)) { Log.LogInformation( "Skipping validation of work item type '{sourceWit}' because it is not included in validation list.", workItemTypeName); return false; } - else if ((Options.ExcludeWorkItemtypes.Count > 0) - && Options.ExcludeWorkItemtypes.Contains(workItemTypeName, StringComparer.OrdinalIgnoreCase)) + else if ((Options.ExcludeWorkItemTypes.Count > 0) + && Options.ExcludeWorkItemTypes.Contains(workItemTypeName, StringComparer.OrdinalIgnoreCase)) { Log.LogInformation( "Skipping validation of work item type '{sourceWit}' because it is excluded from validation list.", @@ -457,8 +457,8 @@ private void LogValidationResult(bool isValid) Log.LogInformation(" - Configure mapping to another work item type which exists in target using" + $" '{nameof(WorkItemTypeMappingTool)}' configuration."); Log.LogInformation(" - Exclude it from validation. To configure which work item types are validated, use either" - + $" '{nameof(TfsWorkItemTypeValidatorToolOptions.IncludeWorkItemtypes)}' or" - + $" '{nameof(TfsWorkItemTypeValidatorToolOptions.ExcludeWorkItemtypes)}'" + + $" '{nameof(TfsWorkItemTypeValidatorToolOptions.IncludeWorkItemTypes)}' or" + + $" '{nameof(TfsWorkItemTypeValidatorToolOptions.ExcludeWorkItemTypes)}'" + $" of '{nameof(TfsWorkItemTypeValidatorTool)}' configuration (but not both at the same time)."); Log.LogInformation("If field is missing in target, you can:"); diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs index d322270a5..e35407dee 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs @@ -27,15 +27,15 @@ public class TfsWorkItemTypeValidatorToolOptions : ToolOptions /// List of work item types which will be validated. If this list is empty, all work item types will be validated. /// /// null - public List IncludeWorkItemtypes { get; set; } = []; + public List IncludeWorkItemTypes { get; set; } = []; /// /// List of work item types which will be excluded from validation. /// - public List ExcludeWorkItemtypes { get; set; } = []; + public List ExcludeWorkItemTypes { get; set; } = []; /// - /// If , some work item types will be automatically added to list. + /// If , some work item types will be automatically added to list. /// Work item types excluded by default are: Code Review Request, Code Review Response, Feedback Request, /// Feedback Response, Shared Parameter, Shared Steps. /// @@ -105,13 +105,15 @@ public void Normalize() IncludeWorkItemtypes ??= []; ExcludeWorkItemtypes ??= []; + IncludeWorkItemTypes ??= []; + ExcludeWorkItemTypes ??= []; if (ExcludeDefaultWorkItemTypes) { foreach (string defaultExcludedWit in _defaultExcludedWorkItemTypes) { - if (!ExcludeWorkItemtypes.Contains(defaultExcludedWit, _normalizedComparer)) + if (!ExcludeWorkItemTypes.Contains(defaultExcludedWit, _normalizedComparer)) { - ExcludeWorkItemtypes.Add(defaultExcludedWit); + ExcludeWorkItemTypes.Add(defaultExcludedWit); } } } diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptionsValidator.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptionsValidator.cs index 425b3832e..d772e0358 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptionsValidator.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptionsValidator.cs @@ -6,14 +6,14 @@ public class TfsWorkItemTypeValidatorToolOptionsValidator : IValidateOptions 0) && (excludedCount > 0)) { - const string msg = $"'{nameof(options.IncludeWorkItemtypes)}' and '{nameof(options.ExcludeWorkItemtypes)}'" + const string msg = $"'{nameof(options.IncludeWorkItemTypes)}' and '{nameof(options.ExcludeWorkItemTypes)}'" + $" cannot be set both at the same time." - + $" If '{nameof(options.IncludeWorkItemtypes)}' list is not empty," + + $" If '{nameof(options.IncludeWorkItemTypes)}' list is not empty," + $" '{nameof(options.ExcludeDefaultWorkItemTypes)}' must be set to 'false'."; return ValidateOptionsResult.Fail(msg); } From 415cd53659ca4843b76b3da9546857addff7e907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Thu, 2 Oct 2025 08:30:28 +0200 Subject: [PATCH 08/14] Implement exclude list for field validation --- .../Tools/TfsWorkItemTypeValidatorTool.cs | 80 +++++++------ .../TfsWorkItemTypeValidatorToolOptions.cs | 113 +++--------------- 2 files changed, 61 insertions(+), 132 deletions(-) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs index 953a260b8..435513711 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.TeamFoundation.WorkItemTracking.Client; @@ -187,6 +188,9 @@ private bool ValidateWorkItemTypeFields(WorkItemType sourceWit, WorkItemType tar { Log.LogInformation(" Validating source field '{sourceFieldReferenceName}' ({sourceFieldName}).", sourceField.ReferenceName, sourceField.Name); + bool fieldResult = true; + bool fieldIsExcluded = Options.IsSourceFieldExcluded(sourceWit.Name, sourceField.ReferenceName); + LogLevel logLevel = fieldIsExcluded ? LogLevel.Information : LogLevel.Warning; if (fieldPair.IsMapped) { Log.LogInformation(" Source field '{sourceFieldName}' is mapped to '{targetFieldName}'.", @@ -202,13 +206,26 @@ private bool ValidateWorkItemTypeFields(WorkItemType sourceWit, WorkItemType tar } else { - result &= ValidateField(sourceField, fieldPair.TargetField, targetWit.Name); + fieldResult = ValidateField(sourceField, fieldPair.TargetField, targetWit.Name, logLevel); } } else { - LogMissingField(targetWit, sourceField, fieldPair.ExpectedTargetFieldName); - result = false; + LogMissingField(targetWit, sourceField, fieldPair.ExpectedTargetFieldName, logLevel); + fieldResult = false; + } + + if (!fieldResult) + { + if (fieldIsExcluded) + { + Log.LogInformation(" Field '{sourceFieldName}' is excluded from validation, so it is considered valid.", + fieldPair.SourceField.ReferenceName); + } + else + { + result = false; + } } } } @@ -251,9 +268,13 @@ private List GetTargetFieldsToValidate( return result; } - private void LogMissingField(WorkItemType targetWit, FieldDefinition sourceField, string targetFieldName) + private void LogMissingField( + WorkItemType targetWit, + FieldDefinition sourceField, + string targetFieldName, + LogLevel logLevel) { - Log.LogWarning(" Missing field '{targetFieldName}' in '{targetWit}'.", targetFieldName, targetWit.Name); + Log.Log(logLevel, " Missing field '{targetFieldName}' in '{targetWit}'.", targetFieldName, targetWit.Name); Log.LogInformation(" Source field reference name: {sourceFieldReferenceName}", sourceField.ReferenceName); Log.LogInformation(" Source field name: {sourceFieldName}", sourceField.Name); Log.LogInformation(" Field type: {fieldType}", sourceField.FieldType); @@ -262,30 +283,20 @@ private void LogMissingField(WorkItemType targetWit, FieldDefinition sourceField Log.LogInformation(" Allowed values type: {allowedValuesType}", valueType); } - private bool ValidateField(FieldDefinition sourceField, FieldDefinition? targetField, string targetWitName) + private bool ValidateField( + FieldDefinition sourceField, + FieldDefinition targetField, + string targetWitName, + LogLevel validationLogLevel) { - //// If target field is in 'FixedTargetFields' list, it means, that user resolved this filed somehow. - //// For example by value mapping. So any discrepancies found will be logged just as information. - //// Otherwise, discrepancies are logged as warning. - //LogLevel logLevel = Options.IsFieldFixed(targetWitName, targetField.ReferenceName) - // ? LogLevel.Information - // : LogLevel.Warning; - LogLevel logLevel = LogLevel.Warning; - - bool isValid = ValidateFieldType(sourceField, targetField, logLevel); - isValid &= ValidateFieldAllowedValues(targetWitName, sourceField, targetField, logLevel); + bool isValid = ValidateFieldType(sourceField, targetField, validationLogLevel); + isValid &= ValidateFieldAllowedValues(targetWitName, sourceField, targetField, validationLogLevel); if (isValid) { Log.LogDebug(" Target field '{targetFieldReferenceName}' ({targetFieldName}) exists in '{targetWit}' and is valid.", targetField.ReferenceName, targetField.Name, targetWitName); } - else if (logLevel == LogLevel.Information) - { - //Log.LogInformation(" Target field '{targetFieldName}' in '{targetWit}' is considered valid," - // + $" because it is listed in '{nameof(Options.FixedTargetFields)}'.", - // targetField.ReferenceName, targetWitName, sourceField.ReferenceName); - } - return (logLevel == LogLevel.Information) || isValid; + return isValid; } private bool ValidateFieldType(FieldDefinition sourceField, FieldDefinition targetField, LogLevel logLevel) @@ -319,7 +330,7 @@ private bool ValidateFieldAllowedValues( sourceField.ReferenceName, targetField.ReferenceName, sourceValueType, targetValueType); } if (!ValidateAllowedValues(targetWitName, sourceField.ReferenceName, sourceAllowedValues, - targetField.ReferenceName, targetAllowedValues, out List missingValues)) + targetField.ReferenceName, targetAllowedValues, out List missingValues, logLevel)) { isValid = false; Log.Log(logLevel, @@ -343,7 +354,8 @@ private bool ValidateAllowedValues( List sourceAllowedValues, string targetFieldReferenceName, List targetAllowedValues, - out List missingValues) + out List missingValues, + LogLevel logLevel) { missingValues = sourceAllowedValues .Except(targetAllowedValues, StringComparer.OrdinalIgnoreCase) @@ -367,7 +379,7 @@ private bool ValidateAllowedValues( } else { - Log.LogWarning(" Value '{missingValue}' is mapped to '{mappedValue}', which does not exists in target." + Log.Log(logLevel, " Value '{missingValue}' is mapped to '{mappedValue}', which does not exists in target." + $" This is probably invalid '{nameof(FieldValueMap)}' configuration.", missingValue, mappedValue); } @@ -447,9 +459,6 @@ private void LogValidationResult(bool isValid) return; } - //const string msg1 = "Some work item types or their fields are not valid in the target system (see previous logs)." - // + " Either add these fields into target work items, or map source fields to other target fields" - // + $" in options ({nameof(TfsWorkItemTypeValidatorToolOptions.SourceFieldMappings)})."; Log.LogError("Some work item types or their fields are not valid in the target system (see previous logs)."); Log.LogInformation("If the work item type does not exist in target system, you can:"); @@ -475,15 +484,10 @@ private void LogValidationResult(bool isValid) Log.LogInformation($" - Configure value mapping. Add field maps of type '{nameof(FieldValueMap)}'" + $" to '{nameof(FieldMappingTool)}' configuration."); - //const string message2 = "If you have some field mappings defined for validation, do not forget also to configure" - // + $" proper field mapping in {nameof(FieldMappingTool)} so data will preserved during migration."; - //Log.LogInformation(message2); - //const string message3 = "If you have different allowed values in some field, either update target field to match" - // + $" allowed values from source, or configure {nameof(FieldValueMap)} in {nameof(FieldMappingTool)}."; - //Log.LogInformation(message3); - - //Log.LogInformation($" You can configure value mapping using '{nameof(FieldValueMap)}' in '{nameof(FieldMappingTool)}'," - // + " or change the process of target system to contain all missing allowed values."); + Log.LogInformation("To exclude field from validation, just configure it in" + + $" '{nameof(TfsWorkItemTypeValidatorToolOptions.ExcludeSourceFields)}'" + + $" of '{nameof(TfsWorkItemTypeValidatorTool)}' configuration." + + " If field is excluded from validation, all the issues are still logged, just the result of validation is 'valid'."); } private static bool TryFindSimilarWorkItemType( diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs index e35407dee..c167267ef 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorToolOptions.cs @@ -41,31 +41,19 @@ public class TfsWorkItemTypeValidatorToolOptions : ToolOptions /// public bool ExcludeDefaultWorkItemTypes { get; set; } = true; - /// - /// Field reference name mappings. Key is work item type name, value is dictionary of mapping source filed name to - /// target field name. Target field name can be empty string to indicate that this field will not be validated in target. - /// As work item type name, you can use * to define mappings which will be applied to all work item types. - /// - /// null - public Dictionary> SourceFieldMappings { get; set; } = []; - /// /// - /// List of target fields that are considered as fixed. - /// A field marked as fixed will not stop the migration if differences are found. - /// Instead of a warning, only an informational message will be logged. + /// List of fields in source work itemt types, that are excluded from validation. + /// Fields excluded from validation are still validated and all found issues are logged. + /// But the result of the validation is 'valid' and the issues are logged as information instead of warning. /// /// - /// Use this list when you already know about the differences and have resolved them, - /// for example by using . - /// - /// - /// The key is the target work item type name. - /// You can also use * to define fixed fields that apply to all work item types. + /// The key is the source work item type name. + /// You can also use * to exclude fields from all source work item types. /// /// /// null - public Dictionary> FixedTargetFields { get; set; } = []; + public Dictionary> ExcludeSourceFields { get; set; } = []; /// /// Normalizes properties, that all of them are set (not ) and all dictionaries uses @@ -78,33 +66,16 @@ public void Normalize() return; } - Dictionary> oldMappings = SourceFieldMappings; - Dictionary> newMappings = new(_normalizedComparer); - if (oldMappings is not null) - { - foreach (KeyValuePair> mapping in oldMappings) - { - Dictionary normalizedValues = new(_normalizedComparer); - foreach (KeyValuePair fieldMapping in mapping.Value) - { - normalizedValues[fieldMapping.Key.Trim()] = fieldMapping.Value.Trim(); - } - newMappings[mapping.Key.Trim()] = normalizedValues; - } - } - - Dictionary> oldFixedFields = FixedTargetFields; - Dictionary> newFixedFields = new(_normalizedComparer); - if (oldFixedFields is not null) + Dictionary> oldExcludedFields = ExcludeSourceFields; + Dictionary> newExcludedFields = new(_normalizedComparer); + if (oldExcludedFields is not null) { - foreach (KeyValuePair> mapping in oldFixedFields) + foreach (KeyValuePair> mapping in oldExcludedFields) { - newFixedFields[mapping.Key.Trim()] = mapping.Value; + newExcludedFields[mapping.Key.Trim()] = mapping.Value; } } - IncludeWorkItemtypes ??= []; - ExcludeWorkItemtypes ??= []; IncludeWorkItemTypes ??= []; ExcludeWorkItemTypes ??= []; if (ExcludeDefaultWorkItemTypes) @@ -118,72 +89,26 @@ public void Normalize() } } - FixedTargetFields = newFixedFields; - SourceFieldMappings = newMappings; + ExcludeSourceFields = newExcludedFields; _isNormalized = true; } /// - /// Returns true, if field from work item type + /// Returns true, if field from work item type /// is in list of fixed target fields. Handles also fields defined for all work item types (*). /// /// Work item type name. - /// Target field reference name. - public bool IsFieldFixed(string workItemType, string targetFieldName) + /// Target field reference name. + public bool IsSourceFieldExcluded(string workItemType, string fieldReferenceName) { - if (FixedTargetFields.TryGetValue(workItemType, out List fixedFields)) + if (ExcludeSourceFields.TryGetValue(workItemType, out List excludedFields)) { - return fixedFields.Contains(targetFieldName, _normalizedComparer); + return excludedFields.Contains(fieldReferenceName, _normalizedComparer); } - if (FixedTargetFields.TryGetValue(AllWorkItemTypes, out fixedFields)) + if (ExcludeSourceFields.TryGetValue(AllWorkItemTypes, out excludedFields)) { - return fixedFields.Contains(targetFieldName, _normalizedComparer); - } - return false; - } - - /// - /// Search for mapped target field name for given . If there is no mapping for source - /// field name, its value is returned as target field name. - /// Handles also mappings defined for all work item types (*). - /// - /// Work item type name. - /// Source field reference name. - /// Flag if returned value was mapped, or just returned the original. - /// - /// Returns: - /// - /// Target field name if it is foung in mappings. This can be empty string, which means that the source - /// field is not mapped. - /// Source filed name if there is no mapping defined for this field. - /// - /// - public string GetTargetFieldName(string workItemType, string sourceFieldName, out bool isMapped) - { - if (TryGetTargetFieldName(workItemType, sourceFieldName, out string targetFieldName)) - { - isMapped = true; - return targetFieldName; - } - if (TryGetTargetFieldName(AllWorkItemTypes, sourceFieldName, out targetFieldName)) - { - isMapped = true; - return targetFieldName; - } - isMapped = false; - return sourceFieldName; - } - - private bool TryGetTargetFieldName(string workItemType, string sourceFieldName, out string targetFieldName) - { - if (SourceFieldMappings.TryGetValue(workItemType, out Dictionary mappings)) - { - if (mappings.TryGetValue(sourceFieldName, out targetFieldName)) - { - return true; - } + return excludedFields.Contains(fieldReferenceName, _normalizedComparer); } - targetFieldName = string.Empty; return false; } } From 71bb6964a109dad8ae88895c1d130648c56ca9e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Thu, 2 Oct 2025 08:31:13 +0200 Subject: [PATCH 09/14] Remove FieldReferenceNameMappingToolOptionsTests --- ...eldReferenceNameMappingToolOptionsTests.cs | 112 ------------------ 1 file changed, 112 deletions(-) delete mode 100644 src/MigrationTools.Clients.TfsObjectModel.Tests/Tools/FieldReferenceNameMappingToolOptionsTests.cs diff --git a/src/MigrationTools.Clients.TfsObjectModel.Tests/Tools/FieldReferenceNameMappingToolOptionsTests.cs b/src/MigrationTools.Clients.TfsObjectModel.Tests/Tools/FieldReferenceNameMappingToolOptionsTests.cs deleted file mode 100644 index 9eff910c3..000000000 --- a/src/MigrationTools.Clients.TfsObjectModel.Tests/Tools/FieldReferenceNameMappingToolOptionsTests.cs +++ /dev/null @@ -1,112 +0,0 @@ -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MigrationTools.Tools; - -namespace MigrationTools.Tests.Tools -{ - [TestClass] - public class FieldReferenceNameMappingToolOptionsTests - { - [TestMethod] - public void FieldMappingsMustNotBeNullWhenNormalized() - { - TfsWorkItemTypeValidatorToolOptions options = new() { Enabled = true }; - options.SourceFieldMappings = null; - options.Normalize(); - - Assert.IsNotNull(options.SourceFieldMappings); - } - - [TestMethod] - public void ShouldLookupValueCaseInsensitively() - { - TfsWorkItemTypeValidatorToolOptions options = new() - { - Enabled = true, - SourceFieldMappings = new() - { - ["wit"] = new() - { - ["source"] = "target" - } - } - }; - options.Normalize(); - - Assert.AreEqual("target", options.GetTargetFieldName("wit", "source", out bool _)); - Assert.AreEqual("target", options.GetTargetFieldName("WIT", "source", out bool _)); - Assert.AreEqual("target", options.GetTargetFieldName("wit", "SOURCE", out bool _)); - Assert.AreEqual("target", options.GetTargetFieldName("WIT", "SOURCE", out bool _)); - } - - [TestMethod] - public void ShouldReturnSourceValueIfNotMapped() - { - TfsWorkItemTypeValidatorToolOptions options = new() - { - Enabled = true, - SourceFieldMappings = new() - { - ["wit"] = new() - { - ["source"] = "target" - } - } - }; - options.Normalize(); - - Assert.AreEqual("not-mapped-source", options.GetTargetFieldName("wit", "not-mapped-source", out bool _)); - Assert.AreEqual("source", options.GetTargetFieldName("not-mapped-wit", "source", out bool _)); - } - - [TestMethod] - public void ShouldReturnEmptyStringIfMappedToEmptyString() - { - TfsWorkItemTypeValidatorToolOptions options = new() - { - Enabled = true, - SourceFieldMappings = new() - { - ["wit"] = new() - { - ["source"] = "target", - ["source-null"] = "" - } - } - }; - - options.Normalize(); - - Assert.AreEqual("target", options.GetTargetFieldName("wit", "source", out bool _)); - Assert.IsEmpty(options.GetTargetFieldName("wit", "source-null", out bool _)); - } - - [TestMethod] - public void ShouldHandleAllWorkItemTypes() - { - TfsWorkItemTypeValidatorToolOptions options = new() - { - Enabled = true, - SourceFieldMappings = new() - { - [TfsWorkItemTypeValidatorToolOptions.AllWorkItemTypes] = new() - { - ["source-all"] = "target-all" - }, - ["wit"] = new() - { - ["source"] = "target" - }, - ["wit2"] = new() - { - ["source-all"] = "target-wit2" - } - } - }; - options.Normalize(); - - Assert.AreEqual("target-wit2", options.GetTargetFieldName("wit2", "source-all", out bool _)); - Assert.AreEqual("target-all", options.GetTargetFieldName("wit", "source-all", out bool _)); - Assert.AreEqual("target-all", options.GetTargetFieldName("not-mapped-wit", "source-all", out bool _)); - } - } -} From da66076ca641e7962351e37480fbdcffca967799 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Thu, 2 Oct 2025 08:38:33 +0200 Subject: [PATCH 10/14] Remove obsolete class --- .../FieldMappingTool/FieldMappingInfo.cs | 49 ------------------- 1 file changed, 49 deletions(-) delete mode 100644 src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingInfo.cs diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingInfo.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingInfo.cs deleted file mode 100644 index d0e2802ea..000000000 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingInfo.cs +++ /dev/null @@ -1,49 +0,0 @@ -using System; - -namespace MigrationTools.Tools -{ - internal enum FieldMappingType - { - None, - Skip, - SourceToTarget, - TargetToTarget - } - - internal class FieldMappingInfo - { - public static readonly FieldMappingInfo None = new(FieldMappingType.None, string.Empty, string.Empty); - public static readonly FieldMappingInfo Skip = new(FieldMappingType.Skip, string.Empty, string.Empty); - - public FieldMappingInfo CreateSourceToTarget(string sourceFieldName, string targetFieldName) - { - if (string.IsNullOrWhiteSpace(sourceFieldName)) - { - const string msg = $"Source field name cannot be empty for '{nameof(FieldMappingType.SourceToTarget)}' mappping type."; - throw new ArgumentException(nameof(sourceFieldName), msg); - } - return new(FieldMappingType.SourceToTarget, sourceFieldName, targetFieldName); - } - - public FieldMappingInfo CreateTargetToTarget(string sourceFieldName, string targetFieldName) - { - if (string.IsNullOrWhiteSpace(sourceFieldName)) - { - const string msg = $"Source field name cannot be empty for '{nameof(FieldMappingType.TargetToTarget)}' mappping type."; - throw new ArgumentException(nameof(sourceFieldName), msg); - } - return new(FieldMappingType.TargetToTarget, sourceFieldName, targetFieldName); - } - - private FieldMappingInfo(FieldMappingType type, string sourceFieldName, string targetFieldName) - { - MappingType = type; - SourceFieldName = sourceFieldName; - TargetFieldName = targetFieldName; - } - - public FieldMappingType MappingType { get; } - public string SourceFieldName { get; } - public string TargetFieldName { get; } - } -} From 11a4fa37ea7062a1cdc6bedde0ab22adc932ce21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Thu, 2 Oct 2025 08:46:25 +0200 Subject: [PATCH 11/14] Add some comments --- .../FieldMappingToolExtentions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs index a4ae60e1b..53d1df7e7 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/FieldMappingTool/FieldMappingToolExtentions.cs @@ -27,6 +27,15 @@ public static IEnumerable GetFieldMaps( .Cast(); } + /// + /// Return defined field maps of type for work item type , + /// which are defined for source field . If + /// is set, only field maps with given mode are returned. + /// + /// Field mapping tool. + /// Work item type name. + /// Source field reference name. + /// Field map mode. public static IEnumerable GetFieldToFieldMaps( this IFieldMappingTool fieldMappingTool, string witName, @@ -41,6 +50,12 @@ public static IEnumerable GetFieldToFieldMaps( : allMaps; } + /// + /// Returns all all defined field maps in for source field . + /// + /// Field mapping tool. + /// Work item type name. + /// Source field reference name. public static IEnumerable<(string sourceFieldName, string targetFieldName)> GetFieldToFieldMultiMaps( this IFieldMappingTool fieldMappingTool, string witName, From 4a1f5a2b2ea5f6c67f4dcfb20df30cc0f6d4166d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Thu, 2 Oct 2025 08:55:22 +0200 Subject: [PATCH 12/14] typo --- .../Tools/TfsWorkItemTypeValidatorTool.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs index 435513711..566239347 100644 --- a/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs +++ b/src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.TeamFoundation.WorkItemTracking.Client; @@ -287,10 +286,10 @@ private bool ValidateField( FieldDefinition sourceField, FieldDefinition targetField, string targetWitName, - LogLevel validationLogLevel) + LogLevel logLevel) { - bool isValid = ValidateFieldType(sourceField, targetField, validationLogLevel); - isValid &= ValidateFieldAllowedValues(targetWitName, sourceField, targetField, validationLogLevel); + bool isValid = ValidateFieldType(sourceField, targetField, logLevel); + isValid &= ValidateFieldAllowedValues(targetWitName, sourceField, targetField, logLevel); if (isValid) { Log.LogDebug(" Target field '{targetFieldReferenceName}' ({targetFieldName}) exists in '{targetWit}' and is valid.", From 57316425cd7839a2385c99483152c5ff179fef93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Thu, 2 Oct 2025 09:59:03 +0200 Subject: [PATCH 13/14] Regenerate docs --- ...ce.tools.tfsworkitemtypevalidatortool.yaml | 55 +++++----------- docs/static/schema/configuration.schema.json | 65 ++++++++----------- ...ma.tools.tfsworkitemtypevalidatortool.json | 21 +++--- 3 files changed, 52 insertions(+), 89 deletions(-) diff --git a/docs/data/classes/reference.tools.tfsworkitemtypevalidatortool.yaml b/docs/data/classes/reference.tools.tfsworkitemtypevalidatortool.yaml index 46cd1ed83..cdc8d1072 100644 --- a/docs/data/classes/reference.tools.tfsworkitemtypevalidatortool.yaml +++ b/docs/data/classes/reference.tools.tfsworkitemtypevalidatortool.yaml @@ -53,19 +53,10 @@ configurationSamples: { "$type": "TfsWorkItemTypeValidatorToolOptions", "Enabled": true, - "IncludeWorkItemtypes": [], - "ExcludeWorkItemtypes": [], + "IncludeWorkItemTypes": [], + "ExcludeWorkItemTypes": [], "ExcludeDefaultWorkItemTypes": true, - "SourceFieldMappings": { - "User Story": { - "Microsoft.VSTS.Common.Prirucka": "Custom.Prirucka" - } - }, - "FixedTargetFields": { - "User Story": [ - "Custom.Prirucka" - ] - } + "ExcludeSourceFields": {} } sampleFor: MigrationTools.Tools.TfsWorkItemTypeValidatorToolOptions description: >- @@ -83,48 +74,36 @@ options: - parameterName: ExcludeDefaultWorkItemTypes type: Boolean description: >- - If `true`, some work item types will be automatically added to `ExcludeWorkItemtypes` list. + If `true`, some work item types will be automatically added to `ExcludeWorkItemTypes` list. Work item types excluded by default are: Code Review Request, Code Review Response, Feedback Request, Feedback Response, Shared Parameter, Shared Steps. defaultValue: missing XML code comments isRequired: false dotNetType: System.Boolean, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e -- parameterName: ExcludeWorkItemtypes - type: List - description: List of work item types which will be excluded from validation. - defaultValue: missing XML code comments - isRequired: false - dotNetType: System.Collections.Generic.List`1[[System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e -- parameterName: FixedTargetFields +- parameterName: ExcludeSourceFields type: Dictionary description: >- - List of target fields that are considered as `fixed`. - A field marked as fixed will not stop the migration if differences are found. - Instead of a warning, only an informational message will be logged. + List of fields in source work itemt types, that are excluded from validation. + Fields excluded from validation are still validated and all found issues are logged. + But the result of the validation is 'valid' and the issues are logged as information instead of warning. - Use this list when you already know about the differences and have resolved them, - for example by using `FieldMappingTool`. - - The key is the target work item type name. - You can also use `*` to define fixed fields that apply to all work item types. + The key is the source work item type name. + You can also use `*` to exclude fields from all source work item types. defaultValue: null isRequired: false dotNetType: System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Collections.Generic.List`1[[System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e -- parameterName: IncludeWorkItemtypes +- parameterName: ExcludeWorkItemTypes type: List - description: List of work item types which will be validated. If this list is empty, all work item types will be validated. - defaultValue: null + description: List of work item types which will be excluded from validation. + defaultValue: missing XML code comments isRequired: false dotNetType: System.Collections.Generic.List`1[[System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e -- parameterName: SourceFieldMappings - type: Dictionary - description: >- - Field reference name mappings. Key is work item type name, value is dictionary of mapping source filed name to - target field name. Target field name can be empty string to indicate that this field will not be validated in target. - As work item type name, you can use `*` to define mappings which will be applied to all work item types. +- parameterName: IncludeWorkItemTypes + type: List + description: List of work item types which will be validated. If this list is empty, all work item types will be validated. defaultValue: null isRequired: false - dotNetType: System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.Collections.Generic.Dictionary`2[[System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e],[System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e + dotNetType: System.Collections.Generic.List`1[[System.String, System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]], System.Private.CoreLib, Version=8.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e status: missing XML code comments processingTarget: missing XML code comments classFile: src/MigrationTools.Clients.TfsObjectModel/Tools/TfsWorkItemTypeValidatorTool.cs diff --git a/docs/static/schema/configuration.schema.json b/docs/static/schema/configuration.schema.json index e10717524..3d4d08795 100644 --- a/docs/static/schema/configuration.schema.json +++ b/docs/static/schema/configuration.schema.json @@ -2182,22 +2182,6 @@ } } }, - "TfsChangeSetMappingTool": { - "title": "TfsChangeSetMappingTool", - "description": "missing XML code comments", - "type": "object", - "properties": { - "ChangeSetMappingFile": { - "description": "missing XML code comments", - "type": "string" - }, - "Enabled": { - "description": "If set to `true` then the tool will run. Set to `false` and the processor will not run.", - "type": "boolean", - "default": "true" - } - } - }, "TfsEmbededImagesTool": { "title": "TfsEmbededImagesTool", "description": "missing XML code comments", @@ -2234,6 +2218,22 @@ } } }, + "TfsChangeSetMappingTool": { + "title": "TfsChangeSetMappingTool", + "description": "missing XML code comments", + "type": "object", + "properties": { + "Enabled": { + "description": "If set to `true` then the tool will run. Set to `false` and the processor will not run.", + "type": "boolean", + "default": "true" + }, + "ChangeSetMappingFile": { + "description": "missing XML code comments", + "type": "string" + } + } + }, "TfsNodeStructureTool": { "title": "TfsNodeStructureTool", "description": "Tool for creating missing area and iteration path nodes in the target project during migration. Configurable through TfsNodeStructureToolOptions to specify which node types to create.", @@ -2469,20 +2469,11 @@ "default": "true" }, "ExcludeDefaultWorkItemTypes": { - "description": "If `true`, some work item types will be automatically added to `ExcludeWorkItemtypes` list.\r\n Work item types excluded by default are: Code Review Request, Code Review Response, Feedback Request,\r\n Feedback Response, Shared Parameter, Shared Steps.", + "description": "If `true`, some work item types will be automatically added to `ExcludeWorkItemTypes` list.\r\n Work item types excluded by default are: Code Review Request, Code Review Response, Feedback Request,\r\n Feedback Response, Shared Parameter, Shared Steps.", "type": "boolean" }, - "ExcludeWorkItemtypes": { - "description": "List of work item types which will be excluded from validation.", - "type": "array", - "prefixItems": [ - { - "type": "string" - } - ] - }, - "FixedTargetFields": { - "description": "List of target fields that are considered as `fixed`.\r\n A field marked as fixed will not stop the migration if differences are found.\r\n Instead of a warning, only an informational message will be logged.\r\n\n Use this list when you already know about the differences and have resolved them,\r\n for example by using `FieldMappingTool`.\r\n\n The key is the target work item type name.\r\n You can also use `*` to define fixed fields that apply to all work item types.", + "ExcludeSourceFields": { + "description": "List of fields in source work itemt types, that are excluded from validation.\r\n Fields excluded from validation are still validated and all found issues are logged.\r\n But the result of the validation is 'valid' and the issues are logged as information instead of warning.\r\n\n The key is the source work item type name.\r\n You can also use `*` to exclude fields from all source work item types.", "type": "object", "default": "null", "additionalProperties": { @@ -2494,26 +2485,24 @@ ] } }, - "IncludeWorkItemtypes": { - "description": "List of work item types which will be validated. If this list is empty, all work item types will be validated.", + "ExcludeWorkItemTypes": { + "description": "List of work item types which will be excluded from validation.", "type": "array", - "default": "null", "prefixItems": [ { "type": "string" } ] }, - "SourceFieldMappings": { - "description": "Field reference name mappings. Key is work item type name, value is dictionary of mapping source filed name to\r\n target field name. Target field name can be empty string to indicate that this field will not be validated in target.\r\n As work item type name, you can use `*` to define mappings which will be applied to all work item types.", - "type": "object", + "IncludeWorkItemTypes": { + "description": "List of work item types which will be validated. If this list is empty, all work item types will be validated.", + "type": "array", "default": "null", - "additionalProperties": { - "type": "object", - "additionalProperties": { + "prefixItems": [ + { "type": "string" } - } + ] } } }, diff --git a/docs/static/schema/schema.tools.tfsworkitemtypevalidatortool.json b/docs/static/schema/schema.tools.tfsworkitemtypevalidatortool.json index 20265a697..a54f9059a 100644 --- a/docs/static/schema/schema.tools.tfsworkitemtypevalidatortool.json +++ b/docs/static/schema/schema.tools.tfsworkitemtypevalidatortool.json @@ -11,27 +11,22 @@ "default": "true" }, "ExcludeDefaultWorkItemTypes": { - "description": "If `true`, some work item types will be automatically added to `ExcludeWorkItemtypes` list.\r\n Work item types excluded by default are: Code Review Request, Code Review Response, Feedback Request,\r\n Feedback Response, Shared Parameter, Shared Steps.", + "description": "If `true`, some work item types will be automatically added to `ExcludeWorkItemTypes` list.\r\n Work item types excluded by default are: Code Review Request, Code Review Response, Feedback Request,\r\n Feedback Response, Shared Parameter, Shared Steps.", "type": "boolean" }, - "ExcludeWorkItemtypes": { - "description": "List of work item types which will be excluded from validation.", - "type": "array" - }, - "FixedTargetFields": { - "description": "List of target fields that are considered as `fixed`.\r\n A field marked as fixed will not stop the migration if differences are found.\r\n Instead of a warning, only an informational message will be logged.\r\n\n Use this list when you already know about the differences and have resolved them,\r\n for example by using `FieldMappingTool`.\r\n\n The key is the target work item type name.\r\n You can also use `*` to define fixed fields that apply to all work item types.", + "ExcludeSourceFields": { + "description": "List of fields in source work itemt types, that are excluded from validation.\r\n Fields excluded from validation are still validated and all found issues are logged.\r\n But the result of the validation is 'valid' and the issues are logged as information instead of warning.\r\n\n The key is the source work item type name.\r\n You can also use `*` to exclude fields from all source work item types.", "type": "object", "default": "null" }, - "IncludeWorkItemtypes": { + "ExcludeWorkItemTypes": { + "description": "List of work item types which will be excluded from validation.", + "type": "array" + }, + "IncludeWorkItemTypes": { "description": "List of work item types which will be validated. If this list is empty, all work item types will be validated.", "type": "array", "default": "null" - }, - "SourceFieldMappings": { - "description": "Field reference name mappings. Key is work item type name, value is dictionary of mapping source filed name to\r\n target field name. Target field name can be empty string to indicate that this field will not be validated in target.\r\n As work item type name, you can use `*` to define mappings which will be applied to all work item types.", - "type": "object", - "default": "null" } } } \ No newline at end of file From 62ae9921390efc5f2cf72577024eebcdcdd9d919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stano=20Gabo=20Pe=C5=A5ko?= Date: Thu, 2 Oct 2025 10:41:31 +0200 Subject: [PATCH 14/14] Fix failing tests --- .../Services/TelemetryLoggerFake.cs | 8 +------- src/MigrationTools.Shadows/Tools/MockFieldMappingTool.cs | 5 +---- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/MigrationTools.Shadows/Services/TelemetryLoggerFake.cs b/src/MigrationTools.Shadows/Services/TelemetryLoggerFake.cs index 925444917..71dc0ccb0 100644 --- a/src/MigrationTools.Shadows/Services/TelemetryLoggerFake.cs +++ b/src/MigrationTools.Shadows/Services/TelemetryLoggerFake.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace MigrationTools.Services.Shadows +namespace MigrationTools.Services.Shadows { public class TelemetryLoggerFake : ITelemetryLogger { @@ -10,12 +6,10 @@ public class TelemetryLoggerFake : ITelemetryLogger public void TrackException(Exception ex, IDictionary properties = null) { - throw new NotImplementedException(); } public void TrackException(Exception ex, IEnumerable> properties = null) { - } } } diff --git a/src/MigrationTools.Shadows/Tools/MockFieldMappingTool.cs b/src/MigrationTools.Shadows/Tools/MockFieldMappingTool.cs index 965fcdd67..1268e62fc 100644 --- a/src/MigrationTools.Shadows/Tools/MockFieldMappingTool.cs +++ b/src/MigrationTools.Shadows/Tools/MockFieldMappingTool.cs @@ -13,10 +13,7 @@ public void AddFieldMap(string workItemTypeName, IFieldMap fieldToTagFieldMap) throw new NotImplementedException(); } - public List GetFieldMappings(string witName) - { - throw new NotImplementedException(); - } + public List GetFieldMappings(string witName) => []; public void ApplyFieldMappings(WorkItemData source, WorkItemData target) {