343 lines
8.8 KiB
C#
343 lines
8.8 KiB
C#
|
|
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<OrcidClient> _logger;
|
||
|
|
|
||
|
|
public OrcidClient(
|
||
|
|
HttpClient httpClient,
|
||
|
|
ILogger<OrcidClient> 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<OrcidSearchResponse?> 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<OrcidSearchResponse>(JsonOptions);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogError(ex, "Error searching ORCID: {Query}", query);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<OrcidRecord?> 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<OrcidRecord>(JsonOptions);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogError(ex, "Error getting ORCID record: {OrcidId}", orcidId);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<OrcidWorks?> 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<OrcidWorks>(JsonOptions);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogError(ex, "Error getting ORCID works: {OrcidId}", orcidId);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<OrcidEmployments?> 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<OrcidEmployments>(JsonOptions);
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
_logger.LogError(ex, "Error getting ORCID employments: {OrcidId}", orcidId);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
public async Task<OrcidEducations?> 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<OrcidEducations>(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<OrcidSearchResult>? 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<OrcidAffiliationGroup>? AffiliationGroup { get; set; }
|
||
|
|
}
|
||
|
|
|
||
|
|
public class OrcidEducations
|
||
|
|
{
|
||
|
|
[JsonPropertyName("affiliation-group")]
|
||
|
|
public List<OrcidAffiliationGroup>? AffiliationGroup { get; set; }
|
||
|
|
}
|
||
|
|
|
||
|
|
public class OrcidAffiliationGroup
|
||
|
|
{
|
||
|
|
public List<OrcidAffiliationSummaryWrapper>? 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<OrcidWorkGroup>? Group { get; set; }
|
||
|
|
}
|
||
|
|
|
||
|
|
public class OrcidWorkGroup
|
||
|
|
{
|
||
|
|
[JsonPropertyName("work-summary")]
|
||
|
|
public List<OrcidWorkSummary>? 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<OrcidExternalId>? ExternalId { get; set; }
|
||
|
|
}
|
||
|
|
|
||
|
|
public class OrcidExternalId
|
||
|
|
{
|
||
|
|
[JsonPropertyName("external-id-type")]
|
||
|
|
public string? ExternalIdType { get; set; }
|
||
|
|
|
||
|
|
[JsonPropertyName("external-id-value")]
|
||
|
|
public string? ExternalIdValue { get; set; }
|
||
|
|
}
|