44using Microsoft . Extensions . Logging ;
55using Microsoft . Extensions . Options ;
66using Microsoft . TeamFoundation . WorkItemTracking . Client ;
7+ using MigrationTools . FieldMaps . AzureDevops . ObjectModel ;
78using MigrationTools . Tools . Infrastructure ;
89using MigrationTools . Tools . Interfaces ;
910
@@ -103,16 +104,26 @@ private bool ValidateWorkItemTypeFields(WorkItemType sourceWit, WorkItemType tar
103104
104105 if ( targetFields . ContainsKey ( targetFieldName ) )
105106 {
106- Log . LogDebug ( " Source field '{sourceFieldName}' exists in '{targetWit}'." , sourceFieldName , targetWit . Name ) ;
107+ if ( sourceField . IsIdentity )
108+ {
109+ const string message = " Source field '{sourceFieldName}' is identity field."
110+ + " Validation is not performed on identity fields, because they usually differ in allowed values." ;
111+ Log . LogDebug ( message , sourceFieldName ) ;
112+ }
113+ else
114+ {
115+ ValidateField ( sourceField , targetFields [ targetFieldName ] , targetWit . Name ) ;
116+ }
107117 }
108118 else
109119 {
110120 Log . LogWarning ( " Missing field '{targetFieldName}' in '{targetWit}'." , targetFieldName , targetWit . Name ) ;
111121 Log . LogInformation ( " Source field reference name: {sourceFieldReferenceName}" , sourceFieldName ) ;
112122 Log . LogInformation ( " Source field name: {sourceFieldName}" , sourceField . Name ) ;
113123 Log . LogInformation ( " Field type: {fieldType}" , sourceField . FieldType ) ;
114- IEnumerable < string > allowedValues = sourceField . AllowedValues . OfType < string > ( ) . Select ( val => $ "'{ val } '") ;
115- Log . LogInformation ( " Allowed values: {allowedValues}" , string . Join ( ", " , allowedValues ) ) ;
124+ ( string valueType , List < string > allowedValues ) = GetAllowedValues ( sourceField ) ;
125+ Log . LogInformation ( " Allowed values: {allowedValues}" , string . Join ( ", " , allowedValues . Select ( v => $ "'{ v } '") ) ) ;
126+ Log . LogInformation ( " Allowed values type: {allowedValuesType}" , valueType ) ;
116127 result = false ;
117128 }
118129 }
@@ -124,6 +135,97 @@ private bool ValidateWorkItemTypeFields(WorkItemType sourceWit, WorkItemType tar
124135 return result ;
125136 }
126137
138+ private void ValidateField ( FieldDefinition sourceField , FieldDefinition targetField , string targetWitName )
139+ {
140+ // If target field is in 'FixedTargetFields' list, it means, that user resolved this filed somehow.
141+ // For example by value mapping. So any discrepancies found will be logged just as information.
142+ // Otherwise, discrepancies are logged as warning.
143+ LogLevel logLevel = Options . IsFieldFixed ( targetWitName , targetField . ReferenceName )
144+ ? LogLevel . Information
145+ : LogLevel . Warning ;
146+ bool isValid = ValidateFieldType ( sourceField , targetField , logLevel ) ;
147+ isValid &= ValidateFieldAllowedValues ( sourceField , targetField , logLevel ) ;
148+ if ( isValid )
149+ {
150+ Log . LogDebug ( " Target field '{targetFieldName}' exists in '{targetWit}' and is valid." ,
151+ targetField . ReferenceName , targetWitName ) ;
152+ }
153+ else if ( logLevel == LogLevel . Information )
154+ {
155+ Log . LogInformation ( " Target field '{targetFieldName}' in '{targetWit}' is considered valid,"
156+ + $ " because it is listed in '{ nameof ( Options . FixedTargetFields ) } '.",
157+ targetField . ReferenceName , targetWitName , sourceField . ReferenceName ) ;
158+ }
159+ }
160+
161+ private bool ValidateFieldType ( FieldDefinition sourceField , FieldDefinition targetField , LogLevel logLevel )
162+ {
163+ if ( sourceField . FieldType != targetField . FieldType )
164+ {
165+ Log . Log ( logLevel ,
166+ " Source field '{sourceField}' and target field '{targetField}' have different types:"
167+ + " source = '{sourceFieldType}', target = '{targetFieldType}'." ,
168+ sourceField . ReferenceName , targetField . ReferenceName , sourceField . FieldType , targetField . FieldType ) ;
169+ return false ;
170+ }
171+ return true ;
172+ }
173+
174+ private bool ValidateFieldAllowedValues ( FieldDefinition sourceField , FieldDefinition targetField , LogLevel logLevel )
175+ {
176+ bool isValid = true ;
177+ ( string sourceValueType , List < string > sourceAllowedValues ) = GetAllowedValues ( sourceField ) ;
178+ ( string targetValueType , List < string > targetAllowedValues ) = GetAllowedValues ( targetField ) ;
179+ if ( ! sourceValueType . Equals ( targetValueType , StringComparison . OrdinalIgnoreCase ) )
180+ {
181+ isValid = false ;
182+ Log . Log ( logLevel ,
183+ " Source field '{sourceField}' and target field '{targetField}' have different allowed values types:"
184+ + " source = '{sourceFieldAllowedValueType}', target = '{targetFieldAllowedValueType}'." ,
185+ sourceField . ReferenceName , targetField . ReferenceName , sourceValueType , targetValueType ) ;
186+ }
187+ if ( ! AllowedValuesAreSame ( sourceAllowedValues , targetAllowedValues ) )
188+ {
189+ isValid = false ;
190+ Log . Log ( logLevel ,
191+ " Source field '{sourceField}' and target field '{targetField}' have different allowed values." ,
192+ sourceField . ReferenceName , targetField . ReferenceName ) ;
193+ Log . LogInformation ( " Source allowed values: {sourceAllowedValues}" ,
194+ string . Join ( ", " , sourceAllowedValues . Select ( val => $ "'{ val } '") ) ) ;
195+ Log . LogInformation ( " Target allowed values: {targetAllowedValues}" ,
196+ string . Join ( ", " , targetAllowedValues . Select ( val => $ "'{ val } '") ) ) ;
197+ }
198+
199+ return isValid ;
200+ }
201+
202+ private bool AllowedValuesAreSame ( List < string > sourceAllowedValues , List < string > targetAllowedValues )
203+ {
204+ if ( sourceAllowedValues . Count != targetAllowedValues . Count )
205+ {
206+ return false ;
207+ }
208+ foreach ( string sourceValue in sourceAllowedValues )
209+ {
210+ if ( ! targetAllowedValues . Contains ( sourceValue , StringComparer . OrdinalIgnoreCase ) )
211+ {
212+ return false ;
213+ }
214+ }
215+ return true ;
216+ }
217+
218+ private ( string valueType , List < string > allowedValues ) GetAllowedValues ( FieldDefinition field )
219+ {
220+ string valueType = field . SystemType . Name ;
221+ List < string > allowedValues = [ ] ;
222+ for ( int i = 0 ; i < field . AllowedValues . Count ; i ++ )
223+ {
224+ allowedValues . Add ( field . AllowedValues [ i ] ) ;
225+ }
226+ return ( valueType , allowedValues ) ;
227+ }
228+
127229 private bool ShouldValidateWorkItemType ( string workItemTypeName )
128230 {
129231 if ( ( Options . IncludeWorkItemtypes . Count > 0 )
@@ -181,11 +283,14 @@ private void LogValidationResult(bool isValid)
181283
182284 const string message1 = "Some work item types or their fields are not present in the target system (see previous logs)."
183285 + " Either add these fields into target work items, or map source fields to other target fields"
184- + $ " in options ({ nameof ( TfsWorkItemTypeValidatorToolOptions . FieldMappings ) } ).";
286+ + $ " in options ({ nameof ( TfsWorkItemTypeValidatorToolOptions . SourceFieldMappings ) } ).";
185287 Log . LogError ( message1 ) ;
186288 const string message2 = "If you have some field mappings defined for validation, do not forget also to configure"
187289 + $ " proper field mapping in { nameof ( FieldMappingTool ) } so data will preserved during migration.";
188290 Log . LogInformation ( message2 ) ;
291+ const string message3 = "If you have different allowed values in some field, either update target field to match"
292+ + $ " allowed values from source, or configure { nameof ( FieldValueMap ) } in { nameof ( FieldMappingTool ) } .";
293+ Log . LogInformation ( message3 ) ;
189294 }
190295
191296 private static bool TryFindSimilarWorkItemType (
0 commit comments