Skip to main content
added 89 characters in body
Source Link
Peter Csala
  • 10.8k
  • 1
  • 16
  • 36
  • Please note that the int.Parse is error-prone, please prefer int.TryParse instead
class ImportFactory : IImportFactory
{
    private ConcurrentDictionary<int, int> idMapper = new ConcurrentDictionary<int, int>();
    private readonly ILogger<ImportFactory> _logger;
    private readonly Devops _devopsConfiguration;
    private readonly IImportService<WorkItemCore> _importWorkItemService;
    private readonly IImportService<SprintCore> _importSprintService;

    private readonly string _versionQueryString;
    private readonly string _sprintCreationURL;
    private readonly string _sprintPublishURL;
    private readonly string _projectId;
    private const string WorkItemPathPrefix = "/fields/";

    public ImportFactory(ILogger<ImportFactory> logger,
        IConfiguration configuration,
        IImportService<SprintCore> importSprintService,
        IImportService<WorkItemCore> importWorkItemService)
    {
        _logger = logger;
        _devopsConfiguration = configuration.GetSection(nameof(Devops)).Get<Devops>();
        _importSprintService = importSprintService;
        _importWorkItemService = importWorkItemService;

        _versionQueryString = $"?api-version={_devopsConfiguration.APIVersion}";
        _sprintCreationURL_projectId = $"{_devopsConfiguration.Target.ProjectIdProjectId;
        _sprintCreationURL = $"{_projectId}/_apis/wit/classificationNodes/Iterations{_versionQueryString}";
        _sprintPublishURL = $"{_devopsConfiguration.Target.ProjectId_projectId}/{_devopsConfiguration.Target.TargetTeamId}/_apis/work/teamsettings/iterations{_versionQueryString}";
    }

    public async Task<Board> Import(IFormFile file)
    {
        using var reader = new StreamReader(file.OpenReadStream());
        string fileContent = await reader.ReadToEndAsync();

        var board = JsonConvert.DeserializeObject<Board>(fileContent);

        await CreateSprints(board.sprints);
        await CreateWorkItems(board.workItemCollection);

        return board;
    }

    private async Task CreateSprints(Sprints sprints)
    {
        foreach (Sprint sprint in sprints.value)
        {
            var result = await _importWorkItemService.Post(_sprintCreationURL, GetJsonContent(sprint));
            await _importSprintService.Post(_sprintPublishURL, GetJsonContent(new { id = result.identifier }));
        }
    }

    private async Task CreateWorkItems(Dictionary<string, WorkItemQueryResult> workItems)
    {
        foreach (var workItemCategory in workItems.Keys)
        {
            var categoryURL = $"{_devopsConfiguration.Target.ProjectId_projectId}/_apis/wit/workitems/%24{workItemCategory}{_versionQueryString}";

            foreach (var workItem in workItems[workItemCategory].workItems)
            {
                await CreateWorkItem(categoryURL, workItem);
            }
        }
    }

    private async Task CreateWorkItem(string categoryURL, WorkItem workItem)
    {
        var operations = new List<WorkItemOperation>
        {
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.Title",
                value = workItem.details.fields.Title ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.Description",
                value = workItem.details.fields.Description ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}Microsoft.VSTS.Common.AcceptanceCriteria",
                value = workItem.details.fields.AcceptanceCriteria ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.IterationPath",
                value = workItem.details.fields.IterationPath.Replace(_devopsConfiguration.Source.ProjectName, _devopsConfiguration.Target.ProjectName)
            }
        };

        var parentId = FindParentId(workItem.details);
        if (parentId != 0)
        {
            operations.Add(new WorkItemOperation()
            {
                path = "/relations/-",
                value = new Relationship()
                {
                    url = $"{_devopsConfiguration.Target.DevOpsOrgURL}{_devopsConfiguration.Target.ProjectId_projectId}/_apis/wit/workitems/{idMapper[parentId]}",
                    attributes = new RelationshipAttribute()
                }
            });
        }

        var result = await _importWorkItemService.Post(categoryURL, GetJsonContent(operations, "application/json-patch+json"));

        if (!idMapper.ContainsKey(workItem.id))
        {
            idMapper.TryAdd(workItem.id, result.id);
        }
    }

    private int FindParentId(WorkItemDetails details)
    {
        var parentRelation = details.relations?.Where(relation => relation.attributes.name.Equals("Parent")).FirstOrDefault();
        return parentRelation == null ? 0 : int.Parse(parentRelation.url.Split("/")[parentRelation.url.Split("/").Length - 1]);
    }

    private HttpContent GetJsonContent(object data, string mediaType = "application/json")
    {
        var jsonString = JsonConvert.SerializeObject(data);
        return new StringContent(jsonString, Encoding.UTF8, mediaType);
    }
}
class ImportFactory : IImportFactory
{
    private ConcurrentDictionary<int, int> idMapper = new ConcurrentDictionary<int, int>();
    private readonly ILogger<ImportFactory> _logger;
    private readonly Devops _devopsConfiguration;
    private readonly IImportService<WorkItemCore> _importWorkItemService;
    private readonly IImportService<SprintCore> _importSprintService;

    private readonly string _versionQueryString;
    private readonly string _sprintCreationURL;
    private readonly string _sprintPublishURL;
    private const string WorkItemPathPrefix = "/fields/";

    public ImportFactory(ILogger<ImportFactory> logger,
        IConfiguration configuration,
        IImportService<SprintCore> importSprintService,
        IImportService<WorkItemCore> importWorkItemService)
    {
        _logger = logger;
        _devopsConfiguration = configuration.GetSection(nameof(Devops)).Get<Devops>();
        _importSprintService = importSprintService;
        _importWorkItemService = importWorkItemService;

        _versionQueryString = $"?api-version={_devopsConfiguration.APIVersion}";
        _sprintCreationURL = $"{_devopsConfiguration.Target.ProjectId}/_apis/wit/classificationNodes/Iterations{_versionQueryString}";
        _sprintPublishURL = $"{_devopsConfiguration.Target.ProjectId}/{_devopsConfiguration.Target.TargetTeamId}/_apis/work/teamsettings/iterations{_versionQueryString}";
    }

    public async Task<Board> Import(IFormFile file)
    {
        using var reader = new StreamReader(file.OpenReadStream());
        string fileContent = await reader.ReadToEndAsync();

        var board = JsonConvert.DeserializeObject<Board>(fileContent);

        await CreateSprints(board.sprints);
        await CreateWorkItems(board.workItemCollection);

        return board;
    }

    private async Task CreateSprints(Sprints sprints)
    {
        foreach (Sprint sprint in sprints.value)
        {
            var result = await _importWorkItemService.Post(_sprintCreationURL, GetJsonContent(sprint));
            await _importSprintService.Post(_sprintPublishURL, GetJsonContent(new { id = result.identifier }));
        }
    }

    private async Task CreateWorkItems(Dictionary<string, WorkItemQueryResult> workItems)
    {
        foreach (var workItemCategory in workItems.Keys)
        {
            var categoryURL = $"{_devopsConfiguration.Target.ProjectId}/_apis/wit/workitems/%24{workItemCategory}{_versionQueryString}";

            foreach (var workItem in workItems[workItemCategory].workItems)
            {
                await CreateWorkItem(categoryURL, workItem);
            }
        }
    }

    private async Task CreateWorkItem(string categoryURL, WorkItem workItem)
    {
        var operations = new List<WorkItemOperation>
        {
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.Title",
                value = workItem.details.fields.Title ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.Description",
                value = workItem.details.fields.Description ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}Microsoft.VSTS.Common.AcceptanceCriteria",
                value = workItem.details.fields.AcceptanceCriteria ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.IterationPath",
                value = workItem.details.fields.IterationPath.Replace(_devopsConfiguration.Source.ProjectName, _devopsConfiguration.Target.ProjectName)
            }
        };

        var parentId = FindParentId(workItem.details);
        if (parentId != 0)
        {
            operations.Add(new WorkItemOperation()
            {
                path = "/relations/-",
                value = new Relationship()
                {
                    url = $"{_devopsConfiguration.Target.DevOpsOrgURL}{_devopsConfiguration.Target.ProjectId}/_apis/wit/workitems/{idMapper[parentId]}",
                    attributes = new RelationshipAttribute()
                }
            });
        }

        var result = await _importWorkItemService.Post(categoryURL, GetJsonContent(operations, "application/json-patch+json"));

        if (!idMapper.ContainsKey(workItem.id))
        {
            idMapper.TryAdd(workItem.id, result.id);
        }
    }

    private int FindParentId(WorkItemDetails details)
    {
        var parentRelation = details.relations?.Where(relation => relation.attributes.name.Equals("Parent")).FirstOrDefault();
        return parentRelation == null ? 0 : int.Parse(parentRelation.url.Split("/")[parentRelation.url.Split("/").Length - 1]);
    }

    private HttpContent GetJsonContent(object data, string mediaType = "application/json")
    {
        var jsonString = JsonConvert.SerializeObject(data);
        return new StringContent(jsonString, Encoding.UTF8, mediaType);
    }
}
  • Please note that the int.Parse is error-prone, please prefer int.TryParse instead
class ImportFactory : IImportFactory
{
    private ConcurrentDictionary<int, int> idMapper = new ConcurrentDictionary<int, int>();
    private readonly ILogger<ImportFactory> _logger;
    private readonly Devops _devopsConfiguration;
    private readonly IImportService<WorkItemCore> _importWorkItemService;
    private readonly IImportService<SprintCore> _importSprintService;

    private readonly string _versionQueryString;
    private readonly string _sprintCreationURL;
    private readonly string _sprintPublishURL;
    private readonly string _projectId;
    private const string WorkItemPathPrefix = "/fields/";

    public ImportFactory(ILogger<ImportFactory> logger,
        IConfiguration configuration,
        IImportService<SprintCore> importSprintService,
        IImportService<WorkItemCore> importWorkItemService)
    {
        _logger = logger;
        _devopsConfiguration = configuration.GetSection(nameof(Devops)).Get<Devops>();
        _importSprintService = importSprintService;
        _importWorkItemService = importWorkItemService;

        _versionQueryString = $"?api-version={_devopsConfiguration.APIVersion}";
        _projectId = _devopsConfiguration.Target.ProjectId;
        _sprintCreationURL = $"{_projectId}/_apis/wit/classificationNodes/Iterations{_versionQueryString}";
        _sprintPublishURL = $"{_projectId}/{_devopsConfiguration.Target.TargetTeamId}/_apis/work/teamsettings/iterations{_versionQueryString}";
    }

    public async Task<Board> Import(IFormFile file)
    {
        using var reader = new StreamReader(file.OpenReadStream());
        string fileContent = await reader.ReadToEndAsync();

        var board = JsonConvert.DeserializeObject<Board>(fileContent);

        await CreateSprints(board.sprints);
        await CreateWorkItems(board.workItemCollection);

        return board;
    }

    private async Task CreateSprints(Sprints sprints)
    {
        foreach (Sprint sprint in sprints.value)
        {
            var result = await _importWorkItemService.Post(_sprintCreationURL, GetJsonContent(sprint));
            await _importSprintService.Post(_sprintPublishURL, GetJsonContent(new { id = result.identifier }));
        }
    }

    private async Task CreateWorkItems(Dictionary<string, WorkItemQueryResult> workItems)
    {
        foreach (var workItemCategory in workItems.Keys)
        {
            var categoryURL = $"{_projectId}/_apis/wit/workitems/%24{workItemCategory}{_versionQueryString}";

            foreach (var workItem in workItems[workItemCategory].workItems)
            {
                await CreateWorkItem(categoryURL, workItem);
            }
        }
    }

    private async Task CreateWorkItem(string categoryURL, WorkItem workItem)
    {
        var operations = new List<WorkItemOperation>
        {
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.Title",
                value = workItem.details.fields.Title ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.Description",
                value = workItem.details.fields.Description ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}Microsoft.VSTS.Common.AcceptanceCriteria",
                value = workItem.details.fields.AcceptanceCriteria ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.IterationPath",
                value = workItem.details.fields.IterationPath.Replace(_devopsConfiguration.Source.ProjectName, _devopsConfiguration.Target.ProjectName)
            }
        };

        var parentId = FindParentId(workItem.details);
        if (parentId != 0)
        {
            operations.Add(new WorkItemOperation()
            {
                path = "/relations/-",
                value = new Relationship()
                {
                    url = $"{_devopsConfiguration.Target.DevOpsOrgURL}{_projectId}/_apis/wit/workitems/{idMapper[parentId]}",
                    attributes = new RelationshipAttribute()
                }
            });
        }

        var result = await _importWorkItemService.Post(categoryURL, GetJsonContent(operations, "application/json-patch+json"));

        if (!idMapper.ContainsKey(workItem.id))
        {
            idMapper.TryAdd(workItem.id, result.id);
        }
    }

    private int FindParentId(WorkItemDetails details)
    {
        var parentRelation = details.relations?.Where(relation => relation.attributes.name.Equals("Parent")).FirstOrDefault();
        return parentRelation == null ? 0 : int.Parse(parentRelation.url.Split("/")[parentRelation.url.Split("/").Length - 1]);
    }

    private HttpContent GetJsonContent(object data, string mediaType = "application/json")
    {
        var jsonString = JsonConvert.SerializeObject(data);
        return new StringContent(jsonString, Encoding.UTF8, mediaType);
    }
}
Source Link
Peter Csala
  • 10.8k
  • 1
  • 16
  • 36

Url handling

Rather than recreating some URLs over and over again you can define them only once and use them multiple times:

...

private readonly string _versionQueryString;
private readonly string _sprintCreationURL;
private readonly string _sprintPublishURL;
private const string WorkItemPathPrefix = "/fields/";

public ImportFactory(...)
{
    ...

    _versionQueryString = $"?api-version={_devopsConfiguration.APIVersion}";
    _sprintCreationURL = $"{_devopsConfiguration.Target.ProjectId}/_apis/wit/classificationNodes/Iterations{_versionQueryString}";
    _sprintPublishURL = $"{_devopsConfiguration.Target.ProjectId}/{_devopsConfiguration.Target.TargetTeamId}/_apis/work/teamsettings/iterations{_versionQueryString}";
}

public methods

It is a good practice to

  • start with your public methods after the constructors
  • continue with private/protected methods that are used inside the public methods
  • finish with the helper methods that are used inside the non public facing methods
//Ctor
public ImportFactory(...)
//Public
public async Task<Board> Import(IFormFile file)
//Private/Protected
private async Task CreateSprints(Sprints sprints)
private async Task CreateWorkItems(Dictionary<string, WorkItemQueryResult> workItems)
private async Task CreateWorkItem(string categoryURL, WorkItem workItem)
//Helpers
private int FindParentId(WorkItemDetails details)
private HttpContent GetJsonContent(object data, string mimeType = "application/json")

Import method

  • You can use the using declaration since C# 8 to simplify your code
  • You can also use the ReadToEndAsync method of the StreamReader to take advantage of non-blocking I/O
public async Task<Board> Import(IFormFile file)
{
    using var reader = new StreamReader(file.OpenReadStream());
    string fileContent = await reader.ReadToEndAsync();

    var board = JsonConvert.DeserializeObject<Board>(fileContent);

    await CreateSprints(board.sprints);
    await CreateWorkItems(board.workItemCollection);

    return board;
}

CreateSprints method

  • There is no need for the PublishSprints method, since the whole can be simplified to a single line
await _importSprintService.Post(_sprintPublishURL, GetJsonContent(new { id = result.identifier }));
  • Since the Post is an async method you should await it
  • The GetJsonContent helper method looks like this
private HttpContent GetJsonContent(object data, string mediaType = "application/json")
{
    var jsonString = JsonConvert.SerializeObject(data);
    return new StringContent(jsonString, Encoding.UTF8, mediaType);
}
  • After all of these modifications the CreateSprints looks like this
private async Task CreateSprints(Sprints sprints)
{
    foreach (Sprint sprint in sprints.value)
    {
        var result = await _importWorkItemService.Post(_sprintCreationURL, GetJsonContent(sprint));
        await _importSprintService.Post(_sprintPublishURL, GetJsonContent(new { id = result.identifier }));
    }
}
  • I'm not sure whether this _importWorkItemService.Post(_sprintCreationURL, GetJsonContent(sprint) should be called on _importWorkItemService or on _importSprintService
    • Without knowing the actual implementation I can't tell but it is likely a bug in your code

CreateWorkItems method

  • Since CreateWorkItem should be an async method (see next section) that's why this should be async as well
  • I've renamed your workItemCollection to workItems since you had a sprints parameter in CreateSprints
private async Task CreateWorkItems(Dictionary<string, WorkItemQueryResult> workItems)
{
    foreach (var workItemCategory in workItems.Keys)
    {
        var categoryURL = $"{_devopsConfiguration.Target.ProjectId}/_apis/wit/workitems/%24{workItemCategory}{_versionQueryString}";

        foreach (var workItem in workItems[workItemCategory].workItems)
        {
            await CreateWorkItem(categoryURL, workItem);
        }
    }
}

CreateWorkItem method

  • The GetCurrentIdEquivalentId and TryAddIdMapper can be inlined since they are not complex and they are used only inside the CreateWorkItem method once
  • You can use the collection initializer to populate your operations List
var operations = new List<WorkItemOperation>
{
    new WorkItemOperation()
    {
        path = $"{WorkItemPathPrefix}System.Title",
        value = workItem.details.fields.Title ?? ""
    },
    new WorkItemOperation()
    {
        path = $"{WorkItemPathPrefix}System.Description",
        value = workItem.details.fields.Description ?? ""
    },
    new WorkItemOperation()
    {
        path = $"{WorkItemPathPrefix}Microsoft.VSTS.Common.AcceptanceCriteria",
        value = workItem.details.fields.AcceptanceCriteria ?? ""
    },
    new WorkItemOperation()
    {
        path = $"{WorkItemPathPrefix}System.IterationPath",
        value = workItem.details.fields.IterationPath.Replace(_devopsConfiguration.Source.ProjectName, _devopsConfiguration.Target.ProjectName)
    }
};
  • Please prefer await over .Result
await _importWorkItemService.Post(categoryURL, GetJsonContent(operations, "application/json-patch+json"));

FindParentId method

  • This can be simplified by using the ?. and ?: operators
private int FindParentId(WorkItemDetails details)
{
    var parentRelation = details.relations?.Where(relation => relation.attributes.name.Equals("Parent")).FirstOrDefault();
    return parentRelation == null ? 0 : int.Parse(parentRelation.url.Split("/")[parentRelation.url.Split("/").Length - 1]);
}

For the sake of completeness here is the full code after refactoring

class ImportFactory : IImportFactory
{
    private ConcurrentDictionary<int, int> idMapper = new ConcurrentDictionary<int, int>();
    private readonly ILogger<ImportFactory> _logger;
    private readonly Devops _devopsConfiguration;
    private readonly IImportService<WorkItemCore> _importWorkItemService;
    private readonly IImportService<SprintCore> _importSprintService;

    private readonly string _versionQueryString;
    private readonly string _sprintCreationURL;
    private readonly string _sprintPublishURL;
    private const string WorkItemPathPrefix = "/fields/";

    public ImportFactory(ILogger<ImportFactory> logger,
        IConfiguration configuration,
        IImportService<SprintCore> importSprintService,
        IImportService<WorkItemCore> importWorkItemService)
    {
        _logger = logger;
        _devopsConfiguration = configuration.GetSection(nameof(Devops)).Get<Devops>();
        _importSprintService = importSprintService;
        _importWorkItemService = importWorkItemService;

        _versionQueryString = $"?api-version={_devopsConfiguration.APIVersion}";
        _sprintCreationURL = $"{_devopsConfiguration.Target.ProjectId}/_apis/wit/classificationNodes/Iterations{_versionQueryString}";
        _sprintPublishURL = $"{_devopsConfiguration.Target.ProjectId}/{_devopsConfiguration.Target.TargetTeamId}/_apis/work/teamsettings/iterations{_versionQueryString}";
    }

    public async Task<Board> Import(IFormFile file)
    {
        using var reader = new StreamReader(file.OpenReadStream());
        string fileContent = await reader.ReadToEndAsync();

        var board = JsonConvert.DeserializeObject<Board>(fileContent);

        await CreateSprints(board.sprints);
        await CreateWorkItems(board.workItemCollection);

        return board;
    }

    private async Task CreateSprints(Sprints sprints)
    {
        foreach (Sprint sprint in sprints.value)
        {
            var result = await _importWorkItemService.Post(_sprintCreationURL, GetJsonContent(sprint));
            await _importSprintService.Post(_sprintPublishURL, GetJsonContent(new { id = result.identifier }));
        }
    }

    private async Task CreateWorkItems(Dictionary<string, WorkItemQueryResult> workItems)
    {
        foreach (var workItemCategory in workItems.Keys)
        {
            var categoryURL = $"{_devopsConfiguration.Target.ProjectId}/_apis/wit/workitems/%24{workItemCategory}{_versionQueryString}";

            foreach (var workItem in workItems[workItemCategory].workItems)
            {
                await CreateWorkItem(categoryURL, workItem);
            }
        }
    }

    private async Task CreateWorkItem(string categoryURL, WorkItem workItem)
    {
        var operations = new List<WorkItemOperation>
        {
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.Title",
                value = workItem.details.fields.Title ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.Description",
                value = workItem.details.fields.Description ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}Microsoft.VSTS.Common.AcceptanceCriteria",
                value = workItem.details.fields.AcceptanceCriteria ?? ""
            },
            new WorkItemOperation()
            {
                path = $"{WorkItemPathPrefix}System.IterationPath",
                value = workItem.details.fields.IterationPath.Replace(_devopsConfiguration.Source.ProjectName, _devopsConfiguration.Target.ProjectName)
            }
        };

        var parentId = FindParentId(workItem.details);
        if (parentId != 0)
        {
            operations.Add(new WorkItemOperation()
            {
                path = "/relations/-",
                value = new Relationship()
                {
                    url = $"{_devopsConfiguration.Target.DevOpsOrgURL}{_devopsConfiguration.Target.ProjectId}/_apis/wit/workitems/{idMapper[parentId]}",
                    attributes = new RelationshipAttribute()
                }
            });
        }

        var result = await _importWorkItemService.Post(categoryURL, GetJsonContent(operations, "application/json-patch+json"));

        if (!idMapper.ContainsKey(workItem.id))
        {
            idMapper.TryAdd(workItem.id, result.id);
        }
    }

    private int FindParentId(WorkItemDetails details)
    {
        var parentRelation = details.relations?.Where(relation => relation.attributes.name.Equals("Parent")).FirstOrDefault();
        return parentRelation == null ? 0 : int.Parse(parentRelation.url.Split("/")[parentRelation.url.Split("/").Length - 1]);
    }

    private HttpContent GetJsonContent(object data, string mediaType = "application/json")
    {
        var jsonString = JsonConvert.SerializeObject(data);
        return new StringContent(jsonString, Encoding.UTF8, mediaType);
    }
}