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
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="PowerGEM.PGJobService.Api.Client" Version="0.9.0.30" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="PowerGEM.PGJobService.Api.Client" Version="0.9.0.30" />
                    
Directory.Packages.props
<PackageReference Include="PowerGEM.PGJobService.Api.Client" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add PowerGEM.PGJobService.Api.Client --version 0.9.0.30
                    
#r "nuget: PowerGEM.PGJobService.Api.Client, 0.9.0.30"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package PowerGEM.PGJobService.Api.Client@0.9.0.30
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=PowerGEM.PGJobService.Api.Client&version=0.9.0.30
                    
Install as a Cake Addin
#tool nuget:?package=PowerGEM.PGJobService.Api.Client&version=0.9.0.30
                    
Install as a Cake Tool

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

  1. Using Visual Studio, open the PGJobServiceApiClient.sln file
  2. Modify the values in Program.cs as appropriate
  3. Set PowerGEM.PGJobService.Api.Client.Demo as the startup project
  4. 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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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