using System.Net.Http.Headers; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.Extensions.Logging; namespace RealCV.Infrastructure.Clients; public sealed class OrcidClient { private readonly HttpClient _httpClient; private readonly ILogger _logger; public OrcidClient( HttpClient httpClient, ILogger logger) { _httpClient = httpClient; _logger = logger; _httpClient.BaseAddress = new Uri("https://pub.orcid.org/v3.0/"); _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } public async Task SearchResearchersAsync(string query, int start = 0, int rows = 20) { try { var encodedQuery = Uri.EscapeDataString(query); var url = $"search?q={encodedQuery}&start={start}&rows={rows}"; _logger.LogDebug("Searching ORCID: {Query}", query); var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { _logger.LogWarning("ORCID API returned {StatusCode} for search: {Query}", response.StatusCode, query); return null; } return await response.Content.ReadFromJsonAsync(JsonOptions); } catch (Exception ex) { _logger.LogError(ex, "Error searching ORCID: {Query}", query); return null; } } public async Task GetRecordAsync(string orcidId) { try { // Normalize ORCID ID format (remove URL prefix if present) orcidId = NormalizeOrcidId(orcidId); var url = $"{orcidId}/record"; _logger.LogDebug("Getting ORCID record: {OrcidId}", orcidId); var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { _logger.LogWarning("ORCID API returned {StatusCode} for ID: {OrcidId}", response.StatusCode, orcidId); return null; } return await response.Content.ReadFromJsonAsync(JsonOptions); } catch (Exception ex) { _logger.LogError(ex, "Error getting ORCID record: {OrcidId}", orcidId); return null; } } public async Task GetWorksAsync(string orcidId) { try { orcidId = NormalizeOrcidId(orcidId); var url = $"{orcidId}/works"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { return null; } return await response.Content.ReadFromJsonAsync(JsonOptions); } catch (Exception ex) { _logger.LogError(ex, "Error getting ORCID works: {OrcidId}", orcidId); return null; } } public async Task GetEmploymentsAsync(string orcidId) { try { orcidId = NormalizeOrcidId(orcidId); var url = $"{orcidId}/employments"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { return null; } return await response.Content.ReadFromJsonAsync(JsonOptions); } catch (Exception ex) { _logger.LogError(ex, "Error getting ORCID employments: {OrcidId}", orcidId); return null; } } public async Task GetEducationsAsync(string orcidId) { try { orcidId = NormalizeOrcidId(orcidId); var url = $"{orcidId}/educations"; var response = await _httpClient.GetAsync(url); if (!response.IsSuccessStatusCode) { return null; } return await response.Content.ReadFromJsonAsync(JsonOptions); } catch (Exception ex) { _logger.LogError(ex, "Error getting ORCID educations: {OrcidId}", orcidId); return null; } } private static string NormalizeOrcidId(string orcidId) { // Remove URL prefixes orcidId = orcidId.Replace("https://orcid.org/", "") .Replace("http://orcid.org/", "") .Trim(); return orcidId; } private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; } // Response models public class OrcidSearchResponse { [JsonPropertyName("num-found")] public int NumFound { get; set; } public List? Result { get; set; } } public class OrcidSearchResult { [JsonPropertyName("orcid-identifier")] public OrcidIdentifier? OrcidIdentifier { get; set; } } public class OrcidIdentifier { public string? Uri { get; set; } public string? Path { get; set; } public string? Host { get; set; } } public class OrcidRecord { [JsonPropertyName("orcid-identifier")] public OrcidIdentifier? OrcidIdentifier { get; set; } public OrcidPerson? Person { get; set; } [JsonPropertyName("activities-summary")] public OrcidActivitiesSummary? ActivitiesSummary { get; set; } } public class OrcidPerson { public OrcidName? Name { get; set; } public OrcidBiography? Biography { get; set; } } public class OrcidName { [JsonPropertyName("given-names")] public OrcidValue? GivenNames { get; set; } [JsonPropertyName("family-name")] public OrcidValue? FamilyName { get; set; } [JsonPropertyName("credit-name")] public OrcidValue? CreditName { get; set; } } public class OrcidValue { public string? Value { get; set; } } public class OrcidBiography { public string? Content { get; set; } } public class OrcidActivitiesSummary { public OrcidEmployments? Employments { get; set; } public OrcidEducations? Educations { get; set; } public OrcidWorks? Works { get; set; } } public class OrcidEmployments { [JsonPropertyName("affiliation-group")] public List? AffiliationGroup { get; set; } } public class OrcidEducations { [JsonPropertyName("affiliation-group")] public List? AffiliationGroup { get; set; } } public class OrcidAffiliationGroup { public List? Summaries { get; set; } } public class OrcidAffiliationSummaryWrapper { [JsonPropertyName("employment-summary")] public OrcidAffiliationSummary? EmploymentSummary { get; set; } [JsonPropertyName("education-summary")] public OrcidAffiliationSummary? EducationSummary { get; set; } } public class OrcidAffiliationSummary { [JsonPropertyName("department-name")] public string? DepartmentName { get; set; } [JsonPropertyName("role-title")] public string? RoleTitle { get; set; } [JsonPropertyName("start-date")] public OrcidDate? StartDate { get; set; } [JsonPropertyName("end-date")] public OrcidDate? EndDate { get; set; } public OrcidOrganization? Organization { get; set; } } public class OrcidDate { public OrcidValue? Year { get; set; } public OrcidValue? Month { get; set; } public OrcidValue? Day { get; set; } } public class OrcidOrganization { public string? Name { get; set; } public OrcidAddress? Address { get; set; } } public class OrcidAddress { public string? City { get; set; } public string? Region { get; set; } public string? Country { get; set; } } public class OrcidWorks { public List? Group { get; set; } } public class OrcidWorkGroup { [JsonPropertyName("work-summary")] public List? WorkSummary { get; set; } } public class OrcidWorkSummary { public string? Title { get; set; } public OrcidTitle? TitleObj { get; set; } public string? Type { get; set; } [JsonPropertyName("publication-date")] public OrcidDate? PublicationDate { get; set; } [JsonPropertyName("journal-title")] public OrcidValue? JournalTitle { get; set; } [JsonPropertyName("external-ids")] public OrcidExternalIds? ExternalIds { get; set; } } public class OrcidTitle { public OrcidValue? Title { get; set; } } public class OrcidExternalIds { [JsonPropertyName("external-id")] public List? ExternalId { get; set; } } public class OrcidExternalId { [JsonPropertyName("external-id-type")] public string? ExternalIdType { get; set; } [JsonPropertyName("external-id-value")] public string? ExternalIdValue { get; set; } }