/// <summary>
        /// Runs the docker image build command to build this image
        /// </summary>
        /// <inheritdoc />
        public override async Task <string> Resolve(CancellationToken ct = default)
        {
            if (ct.IsCancellationRequested)
            {
                return(null);
            }

            if (DeleteOnExit)
            {
                ResourceReaper.RegisterImageForCleanup(ImageName, DockerClient);
            }

            _logger.LogDebug("Begin building image: {}", ImageName);

            var tempTarPath = Path.Combine(Path.GetTempPath(), ImageName.Replace('/', '_') + ".tar");

            try
            {
                using (var tempFile = new FileStream(tempTarPath, FileMode.Create))
                    using (var tarArchive = TarArchive.CreateOutputTarArchive(tempFile))
                    {
                        if (!string.IsNullOrWhiteSpace(BasePath))
                        {
                            // the algorithm here is carefully crafted to minimise the use of
                            // Path.GetFullPath. Path.GetFullPath is used very sparingly and
                            // completely avoided in loops. The reason is because Path.GetFullPath
                            // is a very expensive call and can reduce CPU time by at least 1 order
                            // of magnitude if avoided
                            var fullBasePath = Path.GetFullPath(OS.NormalizePath(BasePath));

                            var ignoreFullPaths = GetIgnores(fullBasePath);

                            // sending a full path will result in entries with full path
                            var allFullPaths = GetAllFilesInDirectory(fullBasePath);

                            // a thread pool that is starved can decrease the performance of
                            // this method dramatically. Using `AsParallel()` will circumvent such issues.
                            // as a result, methods and classes used by this needs to be thread safe.
                            var validFullPaths = allFullPaths
                                                 .AsParallel()
                                                 .Where(f => !IsFileIgnored(ignoreFullPaths, f));

                            foreach (var fullPath in validFullPaths)
                            {
                                // we can safely perform a substring without expanding the paths
                                // using Path.GetFullPath because we know fullBasePath has already been
                                // expanded and the paths in validFullPaths are derived from fullBasePath
                                var relativePath = fullPath.Substring(fullBasePath.Length);

                                // if fullBasePath does not end with directory separator,
                                // relativePath will start with directory separator and that should not be the case
                                if (relativePath.StartsWith(Path.DirectorySeparatorChar.ToString()))
                                {
                                    relativePath = relativePath.Substring(1);
                                }

                                await new MountableFile(fullPath)
                                .TransferTo(tarArchive, relativePath, ct)
                                .ConfigureAwait(false);
                            }

                            _logger.LogDebug("Transferred base path [{}] into tar archive", BasePath);
                        }

                        foreach (var entry in Transferables)
                        {
                            var destinationPath = entry.Key;
                            var transferable    = entry.Value;
                            await transferable
                            .TransferTo(tarArchive, destinationPath, ct)
                            .ConfigureAwait(false);

                            _logger.LogDebug("Transferred [{}] into tar archive", destinationPath);
                        }

                        tarArchive.Close();
                    }

                if (ct.IsCancellationRequested)
                {
                    return(null);
                }

                var buildImageParameters = new ImageBuildParameters
                {
                    Dockerfile = DockerfilePath,
                    Labels     = DeleteOnExit ? ResourceReaper.Labels : null,
                    Tags       = new List <string> {
                        ImageName
                    }
                };

                using (var tempFile = new FileStream(tempTarPath, FileMode.Open))
                {
                    var output =
                        await DockerClient.Images.BuildImageFromDockerfileAsync(tempFile, buildImageParameters, ct);

                    using (var reader = new StreamReader(output))
                    {
                        while (!reader.EndOfStream)
                        {
                            _logger.LogTrace(reader.ReadLine());
                        }
                    }
                }
            }
            finally
            {
                File.Delete(tempTarPath);
            }

            _logger.LogInformation("Dockerfile image built: {}", ImageName);

            // we should not catch exceptions thrown by inspect because the image is
            // expected to be available since we've just built it
            var image = await DockerClient.Images.InspectImageAsync(ImageName, ct);

            ImageId = image.ID;

            return(ImageId);
        }