Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Switch GitHubGraphQLClient to use GraphQL.Client
Co-authored-by: Malcolmnixon <1863707+Malcolmnixon@users.noreply.github.com>
  • Loading branch information
Copilot and Malcolmnixon committed Feb 16, 2026
commit 332fe85a1cd3c9085bb7b60f3f5aeac746ee6b33
2 changes: 2 additions & 0 deletions src/DemaConsulting.BuildMark/DemaConsulting.BuildMark.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@

<ItemGroup>
<PackageReference Include="DemaConsulting.TestResults" Version="1.4.0" />
<PackageReference Include="GraphQL.Client" Version="6.1.0" />
<PackageReference Include="GraphQL.Client.Serializer.SystemTextJson" Version="6.1.0" />
<PackageReference Include="Microsoft.Sbom.Targets" Version="4.1.5" PrivateAssets="All" />
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.103" PrivateAssets="All" />
<PackageReference Include="Microsoft.CodeAnalysis.NetAnalyzers" Version="10.0.103">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,11 @@
// SOFTWARE.

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using GraphQL;
using GraphQL.Client.Http;
using GraphQL.Client.Serializer.SystemTextJson;

namespace DemaConsulting.BuildMark.RepoConnectors;
namespace DemaConsulting.BuildMark.RepoConnectors.GitHub;

/// <summary>
/// Helper class for executing GitHub GraphQL queries.
Expand All @@ -35,19 +36,14 @@ internal sealed class GitHubGraphQLClient : IDisposable
private const string DefaultGitHubGraphQLEndpoint = "https://api.github.com/graphql";

/// <summary>
/// HTTP client for making GraphQL requests.
/// GraphQL HTTP client for making GraphQL requests.
/// </summary>
private readonly HttpClient _httpClient;
private readonly GraphQLHttpClient _graphqlClient;

/// <summary>
/// Indicates whether this instance owns the HTTP client and should dispose it.
/// Indicates whether this instance owns the GraphQL client and should dispose it.
/// </summary>
private readonly bool _ownsHttpClient;

/// <summary>
/// GraphQL endpoint URL.
/// </summary>
private readonly string _graphqlEndpoint;
private readonly bool _ownsGraphQLClient;

/// <summary>
/// Initializes a new instance of the <see cref="GitHubGraphQLClient"/> class.
Expand All @@ -57,13 +53,19 @@ internal sealed class GitHubGraphQLClient : IDisposable
public GitHubGraphQLClient(string token, string? graphqlEndpoint = null)
{
// Initialize HTTP client with authentication and user agent headers
_httpClient = new HttpClient();
_httpClient.DefaultRequestHeaders.Authorization =
var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("Bearer", token);
_httpClient.DefaultRequestHeaders.UserAgent.Add(
httpClient.DefaultRequestHeaders.UserAgent.Add(
new ProductInfoHeaderValue("BuildMark", "1.0"));
_graphqlEndpoint = graphqlEndpoint ?? DefaultGitHubGraphQLEndpoint;
_ownsHttpClient = true;

// Create GraphQL HTTP client with the configured HTTP client
var options = new GraphQLHttpClientOptions
{
EndPoint = new Uri(graphqlEndpoint ?? DefaultGitHubGraphQLEndpoint)
};
_graphqlClient = new GraphQLHttpClient(options, new SystemTextJsonSerializer(), httpClient);
_ownsGraphQLClient = true;
}

/// <summary>
Expand All @@ -78,9 +80,12 @@ public GitHubGraphQLClient(string token, string? graphqlEndpoint = null)
internal GitHubGraphQLClient(HttpClient httpClient, string? graphqlEndpoint = null)
{
// Use provided HTTP client (typically a mocked one for testing)
_httpClient = httpClient;
_graphqlEndpoint = graphqlEndpoint ?? DefaultGitHubGraphQLEndpoint;
_ownsHttpClient = false;
var options = new GraphQLHttpClientOptions
{
EndPoint = new Uri(graphqlEndpoint ?? DefaultGitHubGraphQLEndpoint)
};
_graphqlClient = new GraphQLHttpClient(options, new SystemTextJsonSerializer(), httpClient);
_ownsGraphQLClient = false;
}

/// <summary>
Expand All @@ -97,10 +102,10 @@ public async Task<List<int>> FindIssueIdsLinkedToPullRequestAsync(
{
try
{
// GraphQL query to get closing issues for a pull request
var graphqlQuery = new
// Create GraphQL request
var request = new GraphQLRequest
{
query = @"
Query = @"
query($owner: String!, $repo: String!, $prNumber: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $prNumber) {
Expand All @@ -112,41 +117,22 @@ public async Task<List<int>> FindIssueIdsLinkedToPullRequestAsync(
}
}
}",
variables = new
Variables = new
{
owner,
repo,
prNumber
}
};

// Serialize query and send POST request to GraphQL endpoint
var jsonContent = JsonSerializer.Serialize(graphqlQuery);
using var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
// Execute GraphQL query
var response = await _graphqlClient.SendQueryAsync<GitHubGraphQLTypes.FindIssueIdsResponse>(request);

// Execute GraphQL query and ensure success
var response = await _httpClient.PostAsync(_graphqlEndpoint, content);
response.EnsureSuccessStatusCode();

// Parse response JSON
var responseBody = await response.Content.ReadAsStringAsync();
var jsonDoc = JsonDocument.Parse(responseBody);

// Extract issue numbers from the GraphQL response
var issueNumbers = new List<int>();
if (jsonDoc.RootElement.TryGetProperty("data", out var data) &&
data.TryGetProperty("repository", out var repository) &&
repository.TryGetProperty("pullRequest", out var pullRequest) &&
pullRequest.TryGetProperty("closingIssuesReferences", out var closingIssues) &&
closingIssues.TryGetProperty("nodes", out var nodes))
{
// Enumerate all issue nodes and extract their numbers
foreach (var node in nodes.EnumerateArray().Where(n => n.TryGetProperty("number", out _)))
{
node.TryGetProperty("number", out var number);
issueNumbers.Add(number.GetInt32());
}
}
// Extract issue numbers from the GraphQL response, filtering out null or invalid values
var issueNumbers = response.Data?.Repository?.PullRequest?.ClosingIssuesReferences?.Nodes?
.Where(n => n.Number.HasValue)
.Select(n => n.Number!.Value)
.ToList() ?? [];

// Return list of linked issue numbers
return issueNumbers;
Expand All @@ -159,14 +145,14 @@ public async Task<List<int>> FindIssueIdsLinkedToPullRequestAsync(
}

/// <summary>
/// Disposes the HTTP client if owned by this instance.
/// Disposes the GraphQL client if owned by this instance.
/// </summary>
public void Dispose()
{
// Clean up HTTP client resources only if we own it
if (_ownsHttpClient)
// Clean up GraphQL client resources only if we own it
if (_ownsGraphQLClient)
{
_httpClient.Dispose();
_graphqlClient.Dispose();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2024-2025 Dema Consulting
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

namespace DemaConsulting.BuildMark.RepoConnectors.GitHub;

/// <summary>
/// GitHub GraphQL response types.
/// </summary>
internal static class GitHubGraphQLTypes
Comment thread
Malcolmnixon marked this conversation as resolved.
Outdated
{
/// <summary>
/// Response for finding issues linked to a pull request.
/// </summary>
/// <param name="Repository">Repository data.</param>
internal record FindIssueIdsResponse(
RepositoryData? Repository);

/// <summary>
/// Repository data containing pull request information.
/// </summary>
/// <param name="PullRequest">Pull request data.</param>
internal record RepositoryData(
PullRequestData? PullRequest);

/// <summary>
/// Pull request data containing closing issues.
/// </summary>
/// <param name="ClosingIssuesReferences">Closing issues references.</param>
internal record PullRequestData(
ClosingIssuesReferencesData? ClosingIssuesReferences);

/// <summary>
/// Closing issues references data containing nodes.
/// </summary>
/// <param name="Nodes">Issue nodes.</param>
internal record ClosingIssuesReferencesData(
List<IssueNode>? Nodes);

/// <summary>
/// Issue node containing issue number.
/// </summary>
/// <param name="Number">Issue number.</param>
internal record IssueNode(
int? Number);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

using DemaConsulting.BuildMark.RepoConnectors.GitHub;
using Octokit;

namespace DemaConsulting.BuildMark.RepoConnectors;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

using System.Net;
using System.Text;
using DemaConsulting.BuildMark.RepoConnectors;
using DemaConsulting.BuildMark.RepoConnectors.GitHub;

namespace DemaConsulting.BuildMark.Tests;

Expand Down