In my Azure DevOps Project, I have a Git repository that I would like to copy to another Azure DevOps Project.
In other words, I should be able to copy the original repo into other Azure DevOps projects as needed.
To import work items into Azure DevOps, I have written the following code.
Would you be able to review and make suggestions? Especially, I want to optimize the way HttpClient is being passed to the core service layer from the controller..
Part of this code is already reviewed as you see in this post - Export and import work items from Azure DevOps.
Note:
- Since the destination and/or source of the HttpClient changes every time, I would get the details from the payload.
- At times, the Post method has to return "id" as int/string/nothing.
public class ImportController : Controller { private readonly ILogger<ImportController> _logger; private readonly IImportFactory _importFactory; public ImportController(ILogger<ImportController> logger, IImportFactory importFactory) { _logger = logger; _importFactory = importFactory; } [HttpPost] public async Task<IActionResult> ImportData([FromForm]ImportData importData) { _importFactory.Initialize(importData.devOpsProjectSettings); await _importFactory.Import(importData.file); return Ok(); } } public interface IImportService<T> where T : class { Task<T> Post(string uri, HttpContent content); void SetHttpClient(HttpClient httpClient); } public class ImportService<T> : BaseService<T>, IImportService<T> where T : class { private readonly ILogger<ImportService<T>> _logger; public ImportService(ILogger<ImportService<T>> logger) : base() { _logger = logger; } public async Task<T> Post(string uri, HttpContent content) { var result = await SendRequest(uri, content); return result; } public void SetHttpClient(HttpClient httpClient) { this._httpClient = httpClient; } } public class SprintCore { [Newtonsoft.Json.JsonIgnore] public string id { get; set; } } public class WorkItemCore { public int id { get; set; } public string identifier { get; set; } } public class ServiceEndpointCore { public string id { get; set; } } public 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 IImportService<ServiceEndpointCore> _importRepositoryService; private const string WorkItemPathPrefix = "/fields/"; private readonly string _versionQueryString; private DevOpsProjectSettings _devOpsProjectSettings { get; set; } private HttpClient _httpClient; private string _sprintCreationURL; private string _sprintPublishURL; private string _projectId; private string _repositoryCreationURL; public ImportFactory(ILogger<ImportFactory> logger, IConfiguration configuration, IImportService<SprintCore> importSprintService, IImportService<WorkItemCore> importWorkItemService, IImportService<ServiceEndpointCore> importRepositoryService) { _logger = logger; _devopsConfiguration = configuration.GetSection(nameof(DevOps)).Get<DevOps>(); _importSprintService = importSprintService; _importWorkItemService = importWorkItemService; _importRepositoryService = importRepositoryService; _versionQueryString = $"?api-version={_devopsConfiguration.APIVersion}"; } public void Initialize(DevOpsProjectSettings devOpsProjectSettings) { _devOpsProjectSettings = devOpsProjectSettings; _projectId = devOpsProjectSettings.ProjectId; _sprintCreationURL = $"{_projectId}/_apis/wit/classificationNodes/Iterations{_versionQueryString}"; _sprintPublishURL = $"{_projectId}/{devOpsProjectSettings.TeamId}/_apis/work/teamsettings/iterations{_versionQueryString}"; _repositoryCreationURL = $"_apis/git/repositories{_versionQueryString}"; _httpClient = new HttpClient(); _httpClient.BaseAddress = new Uri(devOpsProjectSettings.DevOpsOrgURL); _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.ASCII.GetBytes(string.Format("{0}:{1}", "", devOpsProjectSettings.PersonalAccessToken)))); _importSprintService.SetHttpClient(_httpClient); _importWorkItemService.SetHttpClient(_httpClient); _importRepositoryService.SetHttpClient(_httpClient); } 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); await CreateRepositories(board.repositories); await ImportRepository(board.repositories, _devOpsProjectSettings); return board; } private async Task ImportRepository(Repositories repositories, DevOpsProjectSettings devOpsProjectSettings) { var _serviceEndpointImportURL = string.Empty; var _serviceEndpointCreationURL = $"_apis/serviceendpoint/endpoints{_versionQueryString}"; foreach (Repository repository in repositories.value) { _serviceEndpointImportURL = $"{_projectId}/_apis/git/repositories/{repository.name}/importRequests{_versionQueryString}"; devOpsProjectSettings.serviceEndpoint.name = $"Import_External_Repo_{repository.name}"; devOpsProjectSettings.serviceEndpoint.url = $"{devOpsProjectSettings.DevOpsSourceURL}{Uri.EscapeDataString(repository.name)}"; devOpsProjectSettings.serviceEndpoint.serviceEndpointProjectReferences[0].name= $"Import_External_Repo_{repository.name}"; var serviceEndpointId = await _importRepositoryService.Post(_serviceEndpointCreationURL, GetJsonContent(devOpsProjectSettings.serviceEndpoint)); var importRepo = new ImportRepo(); importRepo.parameters.serviceEndpointId = serviceEndpointId.id; importRepo.parameters.gitSource.url = devOpsProjectSettings.serviceEndpoint.url; await _importRepositoryService.Post(_serviceEndpointImportURL, GetJsonContent(importRepo)); } } private async Task CreateRepositories(Repositories repositories) { foreach (Repository repository in repositories.value) { repository.project.id = _projectId; await _importSprintService.Post(_repositoryCreationURL, GetJsonContent(repository)); } } 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(_devOpsProjectSettings.SourceProjectName, _devOpsProjectSettings.TargetProjectName) } }; var parentId = FindParentId(workItem.details); if (parentId != 0) { operations.Add(new WorkItemOperation() { path = "/relations/-", value = new Relationship() { url = $"{_devOpsProjectSettings.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); } }