From 0a12d048e7797f64b8ead1fe49b4e9928dbaceef Mon Sep 17 00:00:00 2001 From: "RB Johnson (He/Him)" Date: Thu, 12 Feb 2026 14:32:49 -0800 Subject: [PATCH 1/3] Job status endpoint --- .../GetJobStatus/GetAllJobStatusHandler.cs | 39 +++++ .../GetJobStatus/GetAllJobStatusRequest.cs | 16 ++ .../GetJobStatus/GetAllJobStatusResponse.cs | 29 ++++ .../GetJobStatus/IJobStatusService.cs | 24 +++ .../Operations/GetJobStatus/JobStatusInfo.cs | 61 ++++++++ .../Operations/OperationsConstants.cs | 2 + .../Features/Routing/KnownRoutes.cs | 2 + .../Features/Routing/RouteNames.cs | 2 + .../Resources.Designer.cs | 11 +- src/Microsoft.Health.Fhir.Core/Resources.resx | 6 +- .../Operations/CosmosJobStatusService.cs | 26 ++++ .../Controllers/JobStatusController.cs | 53 +++++++ .../Resources/JobStatusResponseExtensions.cs | 41 ++++++ ...Microsoft.Health.Fhir.Shared.Api.projitems | 2 + .../Operations/SqlJobStatusService.cs | 138 ++++++++++++++++++ ...rBuilderSqlServerRegistrationExtensions.cs | 5 + 16 files changed, 455 insertions(+), 2 deletions(-) create mode 100644 src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusHandler.cs create mode 100644 src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusRequest.cs create mode 100644 src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusResponse.cs create mode 100644 src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/IJobStatusService.cs create mode 100644 src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/JobStatusInfo.cs create mode 100644 src/Microsoft.Health.Fhir.CosmosDb/Features/Operations/CosmosJobStatusService.cs create mode 100644 src/Microsoft.Health.Fhir.Shared.Api/Controllers/JobStatusController.cs create mode 100644 src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs create mode 100644 src/Microsoft.Health.Fhir.SqlServer/Features/Operations/SqlJobStatusService.cs diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusHandler.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusHandler.cs new file mode 100644 index 0000000000..1b7e9f71a9 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusHandler.cs @@ -0,0 +1,39 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Threading; +using System.Threading.Tasks; +using EnsureThat; +using MediatR; + +namespace Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus +{ + /// + /// Handler for getting all async job statuses. + /// + public class GetAllJobStatusHandler : IRequestHandler + { + private readonly IJobStatusService _jobStatusService; + + /// + /// Initializes a new instance of the class. + /// + /// The job status service. + public GetAllJobStatusHandler(IJobStatusService jobStatusService) + { + _jobStatusService = EnsureArg.IsNotNull(jobStatusService, nameof(jobStatusService)); + } + + /// + public async Task Handle(GetAllJobStatusRequest request, CancellationToken cancellationToken) + { + EnsureArg.IsNotNull(request, nameof(request)); + + var jobs = await _jobStatusService.GetAllJobStatusAsync(cancellationToken); + + return new GetAllJobStatusResponse(jobs); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusRequest.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusRequest.cs new file mode 100644 index 0000000000..bd30888590 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusRequest.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using MediatR; + +namespace Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus +{ + /// + /// Request to get all async job statuses. + /// + public class GetAllJobStatusRequest : IRequest + { + } +} diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusResponse.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusResponse.cs new file mode 100644 index 0000000000..f23e0dfa2f --- /dev/null +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/GetAllJobStatusResponse.cs @@ -0,0 +1,29 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; + +namespace Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus +{ + /// + /// Represents the response containing a list of job status information. + /// + public class GetAllJobStatusResponse + { + /// + /// Initializes a new instance of the class. + /// + /// The list of job status information. + public GetAllJobStatusResponse(IReadOnlyList jobs) + { + Jobs = jobs; + } + + /// + /// Gets the list of job status information. + /// + public IReadOnlyList Jobs { get; } + } +} diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/IJobStatusService.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/IJobStatusService.cs new file mode 100644 index 0000000000..772e6d8f85 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/IJobStatusService.cs @@ -0,0 +1,24 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus +{ + /// + /// Service interface for retrieving job status information. + /// + public interface IJobStatusService + { + /// + /// Gets the status information for all async jobs (Export, Import, Reindex, BulkDelete, BulkUpdate). + /// + /// The cancellation token. + /// A list of job status information. + Task> GetAllJobStatusAsync(CancellationToken cancellationToken); + } +} diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/JobStatusInfo.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/JobStatusInfo.cs new file mode 100644 index 0000000000..753f1d6a25 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/JobStatusInfo.cs @@ -0,0 +1,61 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using Microsoft.Health.JobManagement; + +namespace Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus +{ + /// + /// Represents the status information for an async job. + /// + public class JobStatusInfo + { + /// + /// Gets or sets the job identifier. + /// + public long JobId { get; set; } + + /// + /// Gets or sets the group identifier for the job. + /// + public long GroupId { get; set; } + + /// + /// Gets or sets the type of the job (e.g., Export, Import, Reindex). + /// + public string JobType { get; set; } + + /// + /// Gets or sets the queue type. + /// + public QueueType QueueType { get; set; } + + /// + /// Gets or sets the status of the job. + /// + public Microsoft.Health.JobManagement.JobStatus Status { get; set; } + + /// + /// Gets or sets the content location URL for the job. + /// + public Uri ContentLocation { get; set; } + + /// + /// Gets or sets the date and time when the job was created. + /// + public DateTime CreateDate { get; set; } + + /// + /// Gets or sets the date and time when the job started. + /// + public DateTime? StartDate { get; set; } + + /// + /// Gets or sets the date and time when the job ended. + /// + public DateTime? EndDate { get; set; } + } +} diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs index c6cf1db775..ed654f6982 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/OperationsConstants.cs @@ -59,6 +59,8 @@ public static class OperationsConstants public const string ValueSetExpand = $"valueset-expand"; + public const string Jobs = "jobs"; + public static readonly ReadOnlyCollection ExcludedResourceTypesForBulkUpdate = new ReadOnlyCollection(new[] { "SearchParameter", "StructureDefinition" }); } } diff --git a/src/Microsoft.Health.Fhir.Core/Features/Routing/KnownRoutes.cs b/src/Microsoft.Health.Fhir.Core/Features/Routing/KnownRoutes.cs index fc51a2e60c..dd3ccf3c7e 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Routing/KnownRoutes.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Routing/KnownRoutes.cs @@ -114,5 +114,7 @@ internal class KnownRoutes public const string ExpandResourceType = KnownResourceTypes.ValueSet + "/" + Expand; public const string ExpandResourceId = KnownResourceTypes.ValueSet + "/" + IdRouteSegment + "/" + Expand; public const string ExpandOperationDefinition = OperationDefinition + "/" + OperationsConstants.ValueSetExpand; + + public const string JobStatus = OperationsConstants.Operations + "/" + OperationsConstants.Jobs; } } diff --git a/src/Microsoft.Health.Fhir.Core/Features/Routing/RouteNames.cs b/src/Microsoft.Health.Fhir.Core/Features/Routing/RouteNames.cs index ea81baedd6..55c656b64c 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Routing/RouteNames.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Routing/RouteNames.cs @@ -100,5 +100,7 @@ internal static class RouteNames internal const string ExpandById = nameof(ExpandById); internal const string ExpandDefinition = nameof(ExpandDefinition); + + internal const string GetAllJobStatus = nameof(GetAllJobStatus); } } diff --git a/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs b/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs index 533c6f8b8b..a00f024fb5 100644 --- a/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs +++ b/src/Microsoft.Health.Fhir.Core/Resources.Designer.cs @@ -953,6 +953,15 @@ internal static string JobNotReindexOrchestratorJob { } } + /// + /// Looks up a localized string similar to The job status endpoint is not supported for Cosmos DB deployments.. + /// + internal static string JobStatusNotSupportedForCosmosDb { + get { + return ResourceManager.GetString("JobStatusNotSupportedForCosmosDb", resourceCulture); + } + } + /// /// Looks up a localized string similar to Bundle.link values omitted because they exceeded the maximum Uri length.. /// @@ -1710,7 +1719,7 @@ internal static string SearchParameterDefinitionNullorEmptyCodeValue { } /// - /// Looks up a localized string similar to The search parameter with Uri '{0}' is defined by the FHIR specification and cannot be updated or deleted. Custom search parameters must use a different URL.. + /// Looks up a localized string similar to The search parameter with Uri '{0}' is defined by the FHIR specification and cannot be updated or deleted. Custom search parameters must use a different URL.. /// internal static string SearchParameterDefinitionSystemDefined { get { diff --git a/src/Microsoft.Health.Fhir.Core/Resources.resx b/src/Microsoft.Health.Fhir.Core/Resources.resx index 64bac96ba3..df303a3f2b 100644 --- a/src/Microsoft.Health.Fhir.Core/Resources.resx +++ b/src/Microsoft.Health.Fhir.Core/Resources.resx @@ -870,4 +870,8 @@ There are no search parameters to reindex for job Id: {0}. - \ No newline at end of file + + The job status endpoint is not supported for Cosmos DB deployments. + Error message when job status endpoint is called on a Cosmos DB deployment + + diff --git a/src/Microsoft.Health.Fhir.CosmosDb/Features/Operations/CosmosJobStatusService.cs b/src/Microsoft.Health.Fhir.CosmosDb/Features/Operations/CosmosJobStatusService.cs new file mode 100644 index 0000000000..57339ef045 --- /dev/null +++ b/src/Microsoft.Health.Fhir.CosmosDb/Features/Operations/CosmosJobStatusService.cs @@ -0,0 +1,26 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus; + +namespace Microsoft.Health.Fhir.CosmosDb.Features.Operations +{ + /// + /// Cosmos DB implementation of the job status service. + /// This feature is not supported on Cosmos DB. + /// + public class CosmosJobStatusService : IJobStatusService + { + /// + public Task> GetAllJobStatusAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(Fhir.Core.Resources.JobStatusNotSupportedForCosmosDb); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/JobStatusController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/JobStatusController.cs new file mode 100644 index 0000000000..3d955bc963 --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/JobStatusController.cs @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Threading.Tasks; +using EnsureThat; +using MediatR; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Health.Api.Features.Audit; +using Microsoft.Health.Fhir.Api.Features.Filters; +using Microsoft.Health.Fhir.Api.Features.Resources; +using Microsoft.Health.Fhir.Api.Features.Routing; +using Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus; +using Microsoft.Health.Fhir.Core.Features.Routing; +using Microsoft.Health.Fhir.ValueSets; + +namespace Microsoft.Health.Fhir.Api.Controllers +{ + /// + /// Controller for retrieving job status information. + /// + [ServiceFilter(typeof(AuditLoggingFilterAttribute))] + [ServiceFilter(typeof(OperationOutcomeExceptionFilterAttribute))] + [ValidateModelState] + public class JobStatusController : Controller + { + private readonly IMediator _mediator; + + /// + /// Initializes a new instance of the class. + /// + /// The mediator. + public JobStatusController(IMediator mediator) + { + _mediator = EnsureArg.IsNotNull(mediator, nameof(mediator)); + } + + /// + /// Gets the status of all async jobs (Export, Import, Reindex, BulkDelete, BulkUpdate). + /// + /// A list of job status information. + [HttpGet] + [Route(KnownRoutes.JobStatus, Name = RouteNames.GetAllJobStatus)] + [AuditEventType(AuditEventSubType.Read)] + public async Task GetAllJobStatus() + { + var response = await _mediator.Send(new GetAllJobStatusRequest(), HttpContext.RequestAborted); + + return new JsonResult(response.Jobs.ToJobStatusResult()); + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs new file mode 100644 index 0000000000..21bebc44dd --- /dev/null +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System.Collections.Generic; +using System.Linq; +using Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus; + +namespace Microsoft.Health.Fhir.Api.Features.Resources +{ + /// + /// Extension methods for job status responses. + /// + public static class JobStatusResponseExtensions + { + /// + /// Converts a list of JobStatusInfo to a result object for JSON serialization. + /// + /// The list of job status information. + /// An object suitable for JSON serialization. + public static object ToJobStatusResult(this IReadOnlyList jobs) + { + return new + { + jobs = jobs.Select(j => new + { + jobId = j.JobId, + groupId = j.GroupId, + jobType = j.JobType, + queueType = j.QueueType.ToString(), + status = j.Status.ToString(), + contentLocation = j.ContentLocation?.ToString(), + createDate = j.CreateDate, + startDate = j.StartDate, + endDate = j.EndDate, + }).ToList(), + }; + } + } +} diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems b/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems index f6bf0cd8ea..11f02c5e0e 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems +++ b/src/Microsoft.Health.Fhir.Shared.Api/Microsoft.Health.Fhir.Shared.Api.projitems @@ -16,6 +16,7 @@ + @@ -49,6 +50,7 @@ + diff --git a/src/Microsoft.Health.Fhir.SqlServer/Features/Operations/SqlJobStatusService.cs b/src/Microsoft.Health.Fhir.SqlServer/Features/Operations/SqlJobStatusService.cs new file mode 100644 index 0000000000..8ccc94019f --- /dev/null +++ b/src/Microsoft.Health.Fhir.SqlServer/Features/Operations/SqlJobStatusService.cs @@ -0,0 +1,138 @@ +// ------------------------------------------------------------------------------------------------- +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information. +// ------------------------------------------------------------------------------------------------- + +using System; +using System.Collections.Generic; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using EnsureThat; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Logging; +using Microsoft.Health.Fhir.Core.Features.Operations; +using Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus; +using Microsoft.Health.Fhir.Core.Features.Routing; +using Microsoft.Health.JobManagement; +using Microsoft.Health.SqlServer.Features.Client; + +namespace Microsoft.Health.Fhir.SqlServer.Features.Operations +{ + /// + /// SQL Server implementation of the job status service. + /// + public class SqlJobStatusService : IJobStatusService + { + private readonly SqlConnectionWrapperFactory _sqlConnectionWrapperFactory; + private readonly IUrlResolver _urlResolver; + private readonly ILogger _logger; + + /// + /// Initializes a new instance of the class. + /// + /// The SQL connection wrapper factory. + /// The URL resolver. + /// The logger. + public SqlJobStatusService( + SqlConnectionWrapperFactory sqlConnectionWrapperFactory, + IUrlResolver urlResolver, + ILogger logger) + { + _sqlConnectionWrapperFactory = EnsureArg.IsNotNull(sqlConnectionWrapperFactory, nameof(sqlConnectionWrapperFactory)); + _urlResolver = EnsureArg.IsNotNull(urlResolver, nameof(urlResolver)); + _logger = EnsureArg.IsNotNull(logger, nameof(logger)); + } + + /// + public async Task> GetAllJobStatusAsync(CancellationToken cancellationToken) + { + var jobs = new List(); + + var queueTypes = new[] + { + QueueType.Export, + QueueType.Import, + QueueType.Reindex, + QueueType.BulkDelete, + QueueType.BulkUpdate, + }; + + using SqlConnectionWrapper sqlConnectionWrapper = await _sqlConnectionWrapperFactory.ObtainSqlConnectionWrapperAsync(cancellationToken, true); + using SqlCommandWrapper sqlCommandWrapper = sqlConnectionWrapper.CreateRetrySqlCommand(); + + foreach (var queueType in queueTypes) + { + var queueJobs = await GetJobsByQueueTypeAsync(sqlCommandWrapper, queueType, cancellationToken); + jobs.AddRange(queueJobs); + } + + return jobs; + } + + private async Task> GetJobsByQueueTypeAsync(SqlCommandWrapper sqlCommandWrapper, QueueType queueType, CancellationToken cancellationToken) + { + var jobs = new List(); + var operationName = GetOperationNameForQueueType(queueType); + + sqlCommandWrapper.CommandText = @" + SELECT JobId, GroupId, Status, CreateDate, StartDate, EndDate + FROM dbo.JobQueue + WHERE QueueType = @QueueType + AND JobId = GroupId + AND Status <> 5 + ORDER BY CreateDate DESC"; + + sqlCommandWrapper.Parameters.Clear(); + sqlCommandWrapper.Parameters.AddWithValue("@QueueType", (byte)queueType); + + try + { + using SqlDataReader reader = await sqlCommandWrapper.ExecuteReaderAsync(CommandBehavior.Default, cancellationToken); + while (await reader.ReadAsync(cancellationToken)) + { + var jobId = reader.GetInt64(0); + var groupId = reader.GetInt64(1); + var status = (JobStatus)reader.GetByte(2); + var createDate = reader.GetDateTime(3); + DateTime? startDate = await reader.IsDBNullAsync(4, cancellationToken) ? null : reader.GetDateTime(4); + DateTime? endDate = await reader.IsDBNullAsync(5, cancellationToken) ? null : reader.GetDateTime(5); + + var jobStatusInfo = new JobStatusInfo + { + JobId = jobId, + GroupId = groupId, + QueueType = queueType, + JobType = operationName, + Status = status, + ContentLocation = _urlResolver.ResolveOperationResultUrl(operationName, groupId.ToString()), + CreateDate = createDate, + StartDate = startDate, + EndDate = endDate, + }; + + jobs.Add(jobStatusInfo); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Error retrieving jobs for queue type {QueueType}", queueType); + } + + return jobs; + } + + private static string GetOperationNameForQueueType(QueueType queueType) + { + return queueType switch + { + QueueType.Export => OperationsConstants.Export, + QueueType.Import => OperationsConstants.Import, + QueueType.Reindex => OperationsConstants.Reindex, + QueueType.BulkDelete => OperationsConstants.BulkDelete, + QueueType.BulkUpdate => OperationsConstants.BulkUpdate, + _ => queueType.ToString(), + }; + } + } +} diff --git a/src/Microsoft.Health.Fhir.SqlServer/Registration/FhirServerBuilderSqlServerRegistrationExtensions.cs b/src/Microsoft.Health.Fhir.SqlServer/Registration/FhirServerBuilderSqlServerRegistrationExtensions.cs index 66d43333bb..2d08a5ba47 100644 --- a/src/Microsoft.Health.Fhir.SqlServer/Registration/FhirServerBuilderSqlServerRegistrationExtensions.cs +++ b/src/Microsoft.Health.Fhir.SqlServer/Registration/FhirServerBuilderSqlServerRegistrationExtensions.cs @@ -86,6 +86,11 @@ public static IFhirServerBuilder AddSqlServer(this IFhirServerBuilder fhirServer .AsSelf() .AsImplementedInterfaces(); + services.Add() + .Scoped() + .AsSelf() + .AsImplementedInterfaces(); + services.Add() .Singleton() .AsImplementedInterfaces(); From 57f6b2aa49c527043bb0bbe1cfe9bbb9134adac1 Mon Sep 17 00:00:00 2001 From: "RB Johnson (He/Him)" Date: Fri, 20 Feb 2026 10:59:54 -0800 Subject: [PATCH 2/3] Change output to parameters --- .../Controllers/JobStatusController.cs | 3 +- .../Resources/JobStatusResponseExtensions.cs | 83 +++++++++++++++---- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/JobStatusController.cs b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/JobStatusController.cs index 3d955bc963..738f15c874 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Controllers/JobStatusController.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Controllers/JobStatusController.cs @@ -8,6 +8,7 @@ using MediatR; using Microsoft.AspNetCore.Mvc; using Microsoft.Health.Api.Features.Audit; +using Microsoft.Health.Fhir.Api.Features.ActionResults; using Microsoft.Health.Fhir.Api.Features.Filters; using Microsoft.Health.Fhir.Api.Features.Resources; using Microsoft.Health.Fhir.Api.Features.Routing; @@ -47,7 +48,7 @@ public async Task GetAllJobStatus() { var response = await _mediator.Send(new GetAllJobStatusRequest(), HttpContext.RequestAborted); - return new JsonResult(response.Jobs.ToJobStatusResult()); + return FhirResult.Create(response.Jobs.ToJobStatusResult()); } } } diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs index 21bebc44dd..ae896fbaf6 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs @@ -5,7 +5,10 @@ using System.Collections.Generic; using System.Linq; +using Hl7.Fhir.Model; +using Microsoft.Health.Fhir.Core.Extensions; using Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus; +using Microsoft.Health.Fhir.Core.Models; namespace Microsoft.Health.Fhir.Api.Features.Resources { @@ -15,27 +18,73 @@ namespace Microsoft.Health.Fhir.Api.Features.Resources public static class JobStatusResponseExtensions { /// - /// Converts a list of JobStatusInfo to a result object for JSON serialization. + /// Converts a list of JobStatusInfo to a FHIR Parameters resource. /// /// The list of job status information. - /// An object suitable for JSON serialization. - public static object ToJobStatusResult(this IReadOnlyList jobs) + /// A FHIR Parameters resource as a ResourceElement + public static ResourceElement ToJobStatusResult(this IReadOnlyList jobs) { - return new + var parameters = new Parameters(); + + foreach (var job in jobs) { - jobs = jobs.Select(j => new - { - jobId = j.JobId, - groupId = j.GroupId, - jobType = j.JobType, - queueType = j.QueueType.ToString(), - status = j.Status.ToString(), - contentLocation = j.ContentLocation?.ToString(), - createDate = j.CreateDate, - startDate = j.StartDate, - endDate = j.EndDate, - }).ToList(), - }; + var part = new Parameters.ParameterComponent + { + Name = job.JobType + " " + job.JobId, + }; + + part.Part.Add(new Parameters.ParameterComponent + { + Name = "id", + Value = new Integer64(job.GroupId), + }); + + part.Part.Add(new Parameters.ParameterComponent + { + Name = "type", + Value = new FhirString(job.JobType), + }); + + part.Part.Add(new Parameters.ParameterComponent + { + Name = "uri", + Value = new FhirUri(job.ContentLocation), + }); + + part.Part.Add(new Parameters.ParameterComponent + { + Name = "status", + Value = new FhirString(job.Status.ToString()), + }); + + part.Part.Add(new Parameters.ParameterComponent + { + Name = "createTime", + Value = new FhirDateTime(job.CreateDate), + }); + + if (job.StartDate != null) + { + part.Part.Add(new Parameters.ParameterComponent + { + Name = "startTime", + Value = new FhirDateTime((System.DateTimeOffset)job.StartDate), + }); + } + + if (job.EndDate != null) + { + part.Part.Add(new Parameters.ParameterComponent + { + Name = "endTime", + Value = new FhirDateTime((System.DateTimeOffset)job.EndDate), + }); + } + + parameters.Parameter.Add(part); + } + + return parameters.ToResourceElement(); } } } From 36550a3d93c1874cd7732d50dda868b6ec5c7aaa Mon Sep 17 00:00:00 2001 From: "RB Johnson (He/Him)" Date: Mon, 23 Feb 2026 14:20:29 -0800 Subject: [PATCH 3/3] Fix job status values --- .../Operations/GetJobStatus/JobStatusInfo.cs | 10 ---------- .../Resources/JobStatusResponseExtensions.cs | 11 +---------- .../Features/Operations/SqlJobStatusService.cs | 16 ++++++---------- 3 files changed, 7 insertions(+), 30 deletions(-) diff --git a/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/JobStatusInfo.cs b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/JobStatusInfo.cs index 753f1d6a25..891f0384ae 100644 --- a/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/JobStatusInfo.cs +++ b/src/Microsoft.Health.Fhir.Core/Features/Operations/GetJobStatus/JobStatusInfo.cs @@ -13,11 +13,6 @@ namespace Microsoft.Health.Fhir.Core.Features.Operations.GetJobStatus /// public class JobStatusInfo { - /// - /// Gets or sets the job identifier. - /// - public long JobId { get; set; } - /// /// Gets or sets the group identifier for the job. /// @@ -48,11 +43,6 @@ public class JobStatusInfo /// public DateTime CreateDate { get; set; } - /// - /// Gets or sets the date and time when the job started. - /// - public DateTime? StartDate { get; set; } - /// /// Gets or sets the date and time when the job ended. /// diff --git a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs index ae896fbaf6..7f09ca08dc 100644 --- a/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs +++ b/src/Microsoft.Health.Fhir.Shared.Api/Features/Resources/JobStatusResponseExtensions.cs @@ -30,7 +30,7 @@ public static ResourceElement ToJobStatusResult(this IReadOnlyList> GetJobsByQueueTypeAsync(SqlCommandWrappe var operationName = GetOperationNameForQueueType(queueType); sqlCommandWrapper.CommandText = @" - SELECT JobId, GroupId, Status, CreateDate, StartDate, EndDate + SELECT GroupId, min(Status) AS Status, min(CreateDate) AS CreateDate, max(EndDate) AS EndDate FROM dbo.JobQueue WHERE QueueType = @QueueType - AND JobId = GroupId AND Status <> 5 + GROUP BY GroupId ORDER BY CreateDate DESC"; sqlCommandWrapper.Parameters.Clear(); @@ -91,23 +91,19 @@ AND Status <> 5 using SqlDataReader reader = await sqlCommandWrapper.ExecuteReaderAsync(CommandBehavior.Default, cancellationToken); while (await reader.ReadAsync(cancellationToken)) { - var jobId = reader.GetInt64(0); - var groupId = reader.GetInt64(1); - var status = (JobStatus)reader.GetByte(2); - var createDate = reader.GetDateTime(3); - DateTime? startDate = await reader.IsDBNullAsync(4, cancellationToken) ? null : reader.GetDateTime(4); - DateTime? endDate = await reader.IsDBNullAsync(5, cancellationToken) ? null : reader.GetDateTime(5); + var groupId = reader.GetInt64(0); + var status = (JobStatus)reader.GetByte(1); + var createDate = reader.GetDateTime(2); + DateTime? endDate = await reader.IsDBNullAsync(5, cancellationToken) ? null : reader.GetDateTime(3); var jobStatusInfo = new JobStatusInfo { - JobId = jobId, GroupId = groupId, QueueType = queueType, JobType = operationName, Status = status, ContentLocation = _urlResolver.ResolveOperationResultUrl(operationName, groupId.ToString()), CreateDate = createDate, - StartDate = startDate, EndDate = endDate, };