PowerGEM.PGJobService.Api.Client
0.9.0.30
dotnet add package PowerGEM.PGJobService.Api.Client --version 0.9.0.30
NuGet\Install-Package PowerGEM.PGJobService.Api.Client -Version 0.9.0.30
<PackageReference Include="PowerGEM.PGJobService.Api.Client" Version="0.9.0.30" />
<PackageVersion Include="PowerGEM.PGJobService.Api.Client" Version="0.9.0.30" />
<PackageReference Include="PowerGEM.PGJobService.Api.Client" />
paket add PowerGEM.PGJobService.Api.Client --version 0.9.0.30
#r "nuget: PowerGEM.PGJobService.Api.Client, 0.9.0.30"
#:package PowerGEM.PGJobService.Api.Client@0.9.0.30
#addin nuget:?package=PowerGEM.PGJobService.Api.Client&version=0.9.0.30
#tool nuget:?package=PowerGEM.PGJobService.Api.Client&version=0.9.0.30
PowerGEMCloudClient
This repository contains a .NET client library for interacting with the PowerGEM PGJobService API, along with a demo application that shows how to use the client in practice.
Projects
PowerGEM.PGJobService.Api.Client
A reusable .NET Standard library that wraps the PGJobService HTTP API with strongly-typed models and async methods. It is designed for easy integration into .NET applications that need to submit and monitor jobs via PGJobService.
The client is published as a NuGet package here:
PowerGEM.PGJobService.Api.Client on NuGet.org
PowerGEM.PGJobService.Api.Client.Demo
A simple demo application that illustrates how to use the PowerGEM.PGJobService.Api.Client library to interact with a running PGJobService instance. It includes examples for:
- Submitting jobs
- Polling job status
- Handling job results and errors
Getting Started
Install via NuGet
dotnet add package PowerGEM.PGJobService.Api.Client
Example Usage
using Duende.IdentityModel.OidcClient;
using Microsoft.Identity.Client;
using System.Text.Json;
namespace PowerGEM.PGJobService.Api.Client.Demo
{
internal class Program
{
private static readonly JsonSerializerOptions Pretty = new(JsonSerializerOptions.Web)
{
WriteIndented = true
};
private static string JsonSerialize(object? value)
=> JsonSerializer.Serialize(value, Pretty);
static async Task Main(string[] args)
{
try
{
// ------------------------------------------------------------------------------------------
// DEMO OVERVIEW
//
// This demo shows how to create and use the JobServiceClient with different authentication modes,
// depending on how your PGJobService API is configured.
//
// AVAILABLE CLIENT CONFIGURATIONS
//
// • CreateEntraJobServiceClient
// Use when the API is secured with Microsoft Entra ID (Azure AD).
//
// • CreateOidcJobServiceClient
// Use when the API is secured with a generic OpenID Connect (OIDC) provider.
//
// • CreateNoAuthJobServiceClient
// Use when the API does not require authentication (e.g. local development or testing).
//
// ------------------------------------------------------------------------------------------
// AUTHENTICATION WORKFLOWS
//
// Both Entra and OIDC clients support multiple authentication workflows via the AuthWorkflow enum:
//
// • Interactive
// Launches a browser for user login. Best suited for desktop or developer scenarios.
//
// • DeviceCode
// Prompts the user to authenticate on a separate device/browser using a code.
// Ideal for headless or non-interactive environments (e.g. services, CLI tools).
//
// Device code flow requires a callback (DeviceCodeCallback) to display instructions
// to the user (console, logs, UI, etc.).
//
// ------------------------------------------------------------------------------------------
// TOKEN CACHING
//
// Both implementations optionally support token caching to avoid repeated login prompts.
// This is controlled via the enableTokenCaching parameter.
//
// Cached data typically includes access tokens, refresh tokens, and expiration metadata.
//
// This demo uses simple file-based caching for illustration purposes only.
// Tokens (including refresh tokens) are stored UNENCRYPTED on disk.
//
// *** IMPORTANT ***
// This approach is NOT recommended for production use.
//
// For production scenarios, use secure storage such as:
//
// • OS-protected storage
// - Windows: DPAPI
// - macOS: Keychain
// - Linux: libsecret / keyrings
//
// • Microsoft.Identity.Client.Extensions.Msal (RECOMMENDED)
// - Provides cross-platform, encrypted token caching backed by OS-protected storage
// - Integrates directly with MSAL (Entra ID)
// - Can also be used as a secure persistence layer for custom OIDC token storage
//
// • Secure external stores
// - Azure Key Vault
// - AWS Secrets Manager
//
// ------------------------------------------------------------------------------------------
// TO RUN THIS DEMO
//
// 1. Choose the appropriate Create*JobServiceClient method
// 2. Provide your API and authentication configuration values
// 3. Uncomment the selected method below
// -------------------------------------------------------------------------------------------------
var jobServiceClient = CreateEntraJobServiceClient(AuthWorkflow.Interactive, enableTokenCaching: false);
//var jobServiceClient = CreateOidcJobServiceClient(AuthWorkflow.Interactive, enableTokenCaching: false);
//var jobServiceClient = CreateNoAuthJobServiceClient();
var jobCreateRequest = new JobCreateRequest
{
CaseLocation = "<case-data-zip-filename>", // Model data zip filename (should already be uploaded to application folder)
ModelLocation = "<model-data-zip-filename>", // Model data zip filename (should already be uploaded to application folder)
ScriptTemplateVariables = new List<StringStringKeyValuePair> // Variables to be searched / replaced in the template
{
new StringStringKeyValuePair
{
Key = "<variable-1-name>", Value = "<value-1-value>"
},
new StringStringKeyValuePair
{
Key = "<variable-2-name>", Value = "<value-2-value>"
}
},
Apps = new List<AppRunRequest> // List of applications to run (should contain at least 1)
{
new AppRunRequest // First application to run
{
App = AppEnum.ProbeLt, // Application to run
CaseLocation = string.Empty, // Currently unused
ModelLocation = string.Empty, // Currently unused
Template = "<template-filename>", // File name of the template to use (should already be uploaded to application folder)
CloseApp = true, // Whether to close the app (probe, tara) after running
ValidateMaster = false, // Whether master partition should finish before running other partitions
SequenceJobFor = SequenceJobForEnum.Manual, // Sequencing method
// Sequence start date
// Required when SequenceJobFor=Year and IgnoreLeapYear=false
// Required when SequenceJobFor=Manual and PartitionJobBy!=Manual or PartitionInterval!=Manual
// Required when SequenceJobFor=Month|Quarter|Week
SequenceStart = null,
// Sequence end date
// Required when SequenceJobFor=Manual and PartitionJobBy!=Manual or PartitionInterval!=Manual
SequenceEnd = null,
IgnoreLeapYear = true, // Whether to ignore leap years
PartitionJobBy = PartitionJobByEnum.Manual, // Partitioning method
PartitionSubsetOffset = null, // Offset for partition subset
PartitionSubsetIntervals = null, // Intervals for partition subset
JobPartitions = 1, // Number of partitions to run in parallel, must be at least 1
PartitionIntervalMethod = PartitionIntervalMethodEnum.Manual, // Partition interval method
// Intervals for each partition
// Required when PartitionJobBy=Manual and PartitionIntervalMethod=Manual (must include at least 1 positive integer)
PartitionIntervals = [1]
},
new AppRunRequest // Second application to run
{
App = AppEnum.Tara, // Application to run
CaseLocation = string.Empty, // Currently unused
ModelLocation = string.Empty, // Currently unused
Template = "<template-filename>", // File name of the template to use (should already be uploaded to application folder)
CloseApp = true, // Whether to close the app (probe, tara) after running
ValidateMaster = false, // Whether master partition should finish before running other partitions
SequenceJobFor = SequenceJobForEnum.Manual, // Sequencing method
// Sequence start date
// Required when SequenceJobFor=Year and IgnoreLeapYear=false
// Required when SequenceJobFor=Manual and PartitionJobBy!=Manual or PartitionInterval!=Manual
// Required when SequenceJobFor=Month|Quarter|Week
SequenceStart = null,
// Sequence end date
// Required when SequenceJobFor=Manual and PartitionJobBy!=Manual or PartitionInterval!=Manual
SequenceEnd = null,
IgnoreLeapYear = true, // Whether to ignore leap years
PartitionJobBy = PartitionJobByEnum.Manual, // Partitioning method
PartitionSubsetOffset = null, // Offset for partition subset
PartitionSubsetIntervals = null, // Intervals for partition subset
JobPartitions = 1, // Number of partitions to run in parallel, must be at least 1
PartitionIntervalMethod = PartitionIntervalMethodEnum.Manual, // Partition interval method
// Intervals for each partition
// Required when PartitionJobBy=Manual and PartitionIntervalMethod=Manual (must include at least 1 positive integer)
PartitionIntervals = [1]
}
}
};
Console.WriteLine($"Creating job: {JsonSerialize(jobCreateRequest)}");
var jobCreateResult = await jobServiceClient.JobCreateAsync(jobCreateRequest);
Console.WriteLine($"Job created with ID: {jobCreateResult.JobId}");
Job job;
do
{
Thread.Sleep(5000);
job = await jobServiceClient.JobAsync(jobCreateResult.JobId);
Console.WriteLine($"Job status: {job.Status.ToString()}");
} while (job.Status != JobStatusEnum.Failed && job.Status != JobStatusEnum.Completed &&
job.Status != JobStatusEnum.Canceled);
Console.WriteLine($"Job {job.Status.ToString().ToLower()}: {JsonSerialize(job)}");
}
catch (ApiException<ValidationProblemDetails> ex)
{
Console.WriteLine($"A validation error occurred. | Errors: '{JsonSerialize(ex.Result.Errors)}'");
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred. | Message: '{ex.Message}'");
}
finally
{
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
private static JobServiceClient CreateEntraJobServiceClient(AuthWorkflow authWorkflow, bool enableTokenCaching)
{
var apiBaseUrl = "<your-api-base-url>";
var clientId = "<your-entra-client-id>";
var tenantId = "<your-entra-tenant-id>";
var scopes = new[] { "<your-entra-api-scope>" };
var redirectUri = "http://localhost"; // Must also be configured as a matching redirect URI in your Entra app registration (e.g. http://localhost)
var app = PublicClientApplicationBuilder
.Create(clientId)
.WithAuthority($"https://login.microsoftonline.com/{tenantId}")
.WithRedirectUri(redirectUri)
.Build();
// Example: Persist tokens to a local file to avoid repeated login prompts.
//
// MSAL exposes token cache serialization hooks (BeforeAccess / AfterAccess)
// that allow you to control how tokens are loaded and saved.
//
// This example uses a simple file-based cache for demonstration purposes.
// Tokens (including refresh tokens) are written to disk in an UNENCRYPTED format.
//
// *** WARNING ***
// Do NOT use this approach in production.
//
// For production scenarios, use:
// • Microsoft.Identity.Client.Extensions.Msal (RECOMMENDED)
// • OS-protected storage (DPAPI, Keychain, etc.)
if (enableTokenCaching)
{
var cacheFilePath = Path.Combine(Environment.CurrentDirectory, "entra_token_cache.bin");
app.UserTokenCache.SetBeforeAccess(args =>
{
if (File.Exists(cacheFilePath))
args.TokenCache.DeserializeMsalV3(File.ReadAllBytes(cacheFilePath));
});
app.UserTokenCache.SetAfterAccess(args =>
{
if (args.HasStateChanged)
{
var bytes = args.TokenCache.SerializeMsalV3();
Directory.CreateDirectory(Path.GetDirectoryName(cacheFilePath)!);
File.WriteAllBytes(cacheFilePath, bytes);
}
});
}
return new JobServiceClient(
apiBaseUrl,
new HttpClient(),
new EntraTokenProviderOptions
{
AuthWorkflow = authWorkflow,
// Displays device code instructions to the user (console output in this demo).
// In real applications, this could write to logs, UI, or other user-facing channels.
DeviceCodeCallback = Console.WriteLine,
App = app,
Scopes = scopes
});
}
private static JobServiceClient CreateOidcJobServiceClient(AuthWorkflow authWorkflow, bool enableTokenCaching)
{
var apiBaseUrl = "<your-api-base-url>";
var authority = "<your-oidc-authority>";
var clientId = "<your-oidc-client-id>";
var scope = "<your-oidc-api-scope>";
var port = 45656; // Replace with your redirect URI port from your OIDC client registration (e.g. http://localhost:45656)
var redirectUri = $"http://localhost:{port}"; // Must also be configured as a matching redirect URI in your OIDC client registration (e.g. http://localhost:45656)
// Example: Persist tokens using a custom IOidcTokenCacheStore implementation.
//
// Unlike MSAL, the OIDC client does not provide built-in token caching,
// so caching is handled via the IOidcTokenCacheStore abstraction.
//
// This example uses a simple file-based implementation for demonstration purposes.
// Tokens (including refresh tokens) are stored UNENCRYPTED on disk.
//
// *** WARNING ***
// Do NOT use this implementation in production.
//
// For production scenarios, implement IOidcTokenCacheStore using secure storage:
//
// • Microsoft.Identity.Client.Extensions.Msal (RECOMMENDED)
// - Provides encrypted, cross-platform storage backed by OS-protected mechanisms
// (Windows DPAPI, macOS Keychain, Linux libsecret)
// - Can also be used independently to securely persist tokens for custom OIDC implementations
//
// • OS-protected storage
// - Windows: DPAPI
// - macOS: Keychain
// - Linux: libsecret / keyrings
//
// • Secure external stores
// - Azure Key Vault
// - AWS Secrets Manager
//
// At minimum, refresh tokens MUST be encrypted at rest in production scenarios,
// and access to the cache must be restricted to the current user or application process.
var oidcTokenCacheStore = enableTokenCaching
? new FileOidcTokenCacheStore(Path.Combine(Environment.CurrentDirectory, "oidc_token_cache.json"))
: null;
return new JobServiceClient(
apiBaseUrl,
new HttpClient(),
new OidcTokenProviderOptions
{
AuthWorkflow = authWorkflow,
// Displays device code instructions to the user (console output in this demo).
// In real applications, this could write to logs, UI, or other user-facing channels.
DeviceCodeCallback = Console.WriteLine,
OidcClientOptions = new OidcClientOptions
{
Authority = authority,
ClientId = clientId,
Scope = scope,
RedirectUri = redirectUri,
Browser = new SystemBrowser(port)
},
TokenCacheStore = oidcTokenCacheStore
});
}
private static JobServiceClient CreateNoAuthJobServiceClient()
{
var apiBaseUrl = "<your-api-base-url>";
var apiKey = "<your-api-key>";
// When the API is configured for use without token authentication, the API key is required instead.
return new JobServiceClient(
apiBaseUrl,
new HttpClient(),
apiKey);
}
}
/// <summary>
/// Simple file-based implementation of IOidcTokenCacheStore used for demo purposes only.
/// Stores tokens as plain JSON on disk (NOT secure).
/// Do NOT use in production.
/// </summary>
public class FileOidcTokenCacheStore : IOidcTokenCacheStore
{
private readonly string _filePath;
public FileOidcTokenCacheStore(string filePath)
{
_filePath = filePath;
}
public async Task<OidcTokenCacheData?> LoadAsync(CancellationToken cancellationToken)
{
if (!File.Exists(_filePath))
return null;
var json = await File.ReadAllTextAsync(_filePath, cancellationToken);
return JsonSerializer.Deserialize<OidcTokenCacheData>(json);
}
public async Task SaveAsync(OidcTokenCacheData token, CancellationToken cancellationToken)
{
var json = JsonSerializer.Serialize(token);
Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
await File.WriteAllTextAsync(_filePath, json, cancellationToken);
}
}
}
Demo
- Using Visual Studio, open the PGJobServiceApiClient.sln file
- Modify the values in Program.cs as appropriate
- Set
PowerGEM.PGJobService.Api.Client.Demoas the startup project - Build and run the application
Contributing
Contributions, issues, and feature requests are welcome! Feel free to open an issue or pull request.
License
This project is licensed under the MIT License.
See the LICENSE file for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net9.0
- Duende.IdentityModel.OidcClient (>= 7.1.0)
- Microsoft.Identity.Client (>= 4.83.3)
- Newtonsoft.Json (>= 13.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.9.0.30 | 90 | 8/31/2026 |
| 0.9.0.29 | 94 | 8/26/2026 |
| 0.9.0.28 | 93 | 8/24/2026 |
| 0.9.0.27 | 99 | 8/19/2026 |
| 0.9.0.26 | 120 | 8/3/2026 |
| 0.9.0.25 | 105 | 7/27/2026 |
| 0.9.0.24 | 114 | 4/17/2026 |
| 0.9.0.23 | 138 | 4/10/2026 |
| 0.9.0.22 | 113 | 4/8/2026 |
| 0.9.0.21 | 112 | 4/7/2026 |
| 0.9.0.20 | 119 | 3/25/2026 |
| 0.9.0.19 | 121 | 1/12/2026 |
| 0.9.0.18 | 214 | 11/4/2025 |
| 0.9.0.14 | 219 | 5/29/2025 |
| 0.9.0.13 | 206 | 5/29/2025 |
| 0.9.0.9 | 283 | 5/14/2025 |
| 0.9.0.7 | 292 | 5/12/2025 |
| 0.9.0.5 | 281 | 5/12/2025 |
| 0.9.0.1 | 120 | 1/12/2026 |