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);
}
}