Skip to content

Implement Hangfire storage provider for Oracle Database#1

Merged
wouternijenhuis merged 16 commits intomainfrom
copilot/add-hangfire-oracle-storage
Jan 22, 2026
Merged

Implement Hangfire storage provider for Oracle Database#1
wouternijenhuis merged 16 commits intomainfrom
copilot/add-hangfire-oracle-storage

Conversation

Copy link
Contributor

Copilot AI commented Jan 22, 2026

Implementation Complete: Hangfire.Oracle.Core Storage Provider

✅ All Requirements Met

  • Project Setup

    • Multi-targeting: .NET 8.0 and .NET 10.0
    • Updated to latest NuGet packages versions
  • Core Implementation

    • OracleStorage : JobStorage - Main storage class with connection management
    • OracleStorageConnection : IStorageConnection - Complete read operations implementation
    • OracleWriteOnlyTransaction : IWriteOnlyTransaction - Complete write operations implementation
    • OracleMonitoringApi : IMonitoringApi - Full dashboard and monitoring support
  • Advanced Features

    • OracleDistributedLock - Distributed locking using Oracle row locks
    • OracleJobQueue - Queue polling with FOR UPDATE SKIP LOCKED
    • OracleStorageExtensions - Easy configuration via extension methods
  • Database Schema

    • Install.sql with 11 tables, sequences, and indexes
    • Uninstall.sql for cleanup
    • Automatic schema creation support
  • Configuration

    • OracleStorageOptions with 11 configurable options
    • Schema name/prefix customization
    • Timeout and interval configuration
    • Fetch size optimization
  • CI/CD & Release Automation

    • GitHub Actions workflow for automated releases
    • Automated NuGet package creation
    • Symbol packages (.snupkg) for debugging
    • Auto-generated release notes
    • Optional NuGet.org publishing
    • Multi-framework support in release builds
  • Quality Assurance

    • 19 unit tests (all passing on both .NET 8.0 and .NET 10.0)
    • No security vulnerabilities
    • Code review issues addressed
    • Oracle-specific SQL syntax fixed
    • Serialization/deserialization consistency fixes
    • Transaction management improvements
    • Code quality enhancements
  • Documentation

    • Comprehensive README with usage examples
    • ASP.NET Core integration guide
    • Configuration reference
    • Implementation summary
    • Release process documentation

🐛 Recent Bug Fixes & Improvements

  • Transaction management for SetRangeInHash: Added proper transaction handling to ensure atomicity when setting multiple hash values. If any operation fails, all changes are rolled back to prevent inconsistent state.

  • Documented polling loop behavior: Added clear comments explaining the intentional infinite loop in FetchNextJob, which is part of Hangfire's design for continuous job polling with cancellation token support.

  • Fixed dictionary access pattern: Changed direct dictionary indexer access to TryGetValue pattern in ScheduledJobs method to prevent potential KeyNotFoundException and improve code safety.

  • Fixed server data deserialization: The Servers() method now properly deserializes server data to match AnnounceServer() serialization format.

Original prompt

Build a .NET class library that implements a Hangfire storage provider for Oracle: project Hangfire.Oracle.Core with OracleStorage : JobStorage plus required interfaces (IStorageConnection, IWriteOnlyTransaction, IMonitoringApi). Use Dapper + Dapper.Oracle + Oracle.ManagedDataAccess.Core. Provide Oracle schema/DDL scripts, options (schema/prefix, invisibility timeout, fetch size), queue polling, distributed locks, transactions, counters/sets/hashes, and unit tests.


💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.

Copilot AI and others added 4 commits January 22, 2026 18:05
Co-authored-by: wouternijenhuis <21281799+wouternijenhuis@users.noreply.github.com>
Co-authored-by: wouternijenhuis <21281799+wouternijenhuis@users.noreply.github.com>
…SKIP LOCKED)

Co-authored-by: wouternijenhuis <21281799+wouternijenhuis@users.noreply.github.com>
Co-authored-by: wouternijenhuis <21281799+wouternijenhuis@users.noreply.github.com>
@wouternijenhuis wouternijenhuis marked this pull request as ready for review January 22, 2026 18:13
Copilot AI review requested due to automatic review settings January 22, 2026 18:13
@wouternijenhuis
Copy link
Owner

@copilot add support for net10 and update to latest nuget package versions

Copilot AI changed the title [WIP] Build .NET class library for Hangfire Oracle storage provider Implement Hangfire storage provider for Oracle Database Jan 22, 2026
Copilot AI requested a review from wouternijenhuis January 22, 2026 18:17
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request implements a comprehensive Hangfire storage provider for Oracle Database. The implementation provides a complete .NET 8.0 class library that enables Hangfire to use Oracle as its backing store for background job processing.

Changes:

  • Implements core Hangfire storage interfaces (OracleStorage, IStorageConnection, IWriteOnlyTransaction, IMonitoringApi)
  • Adds Oracle-specific features including distributed locking, queue polling, and automatic schema management
  • Includes 19 unit tests for validation of constructors, configuration, and initialization

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 30 comments.

Show a summary per file
File Description
src/Hangfire.Oracle.Core/Hangfire.Oracle.Core.csproj Project file with dependencies (Hangfire.Core, Dapper, Oracle.ManagedDataAccess.Core)
src/Hangfire.Oracle.Core/OracleStorage.cs Main storage class implementing JobStorage with connection and schema management
src/Hangfire.Oracle.Core/OracleStorageOptions.cs Configuration options for timeouts, intervals, and schema settings
src/Hangfire.Oracle.Core/OracleStorageExtensions.cs Extension methods for easy Hangfire configuration
src/Hangfire.Oracle.Core/OracleStorageConnection.cs Read operations implementing IStorageConnection
src/Hangfire.Oracle.Core/OracleWriteOnlyTransaction.cs Write operations implementing IWriteOnlyTransaction with batched commands
src/Hangfire.Oracle.Core/OracleMonitoringApi.cs Dashboard and monitoring support implementing IMonitoringApi
src/Hangfire.Oracle.Core/OracleJobQueue.cs Queue polling and job fetching with FOR UPDATE SKIP LOCKED
src/Hangfire.Oracle.Core/OracleDistributedLock.cs Distributed locking implementation using Oracle row locks
src/Hangfire.Oracle.Core/Scripts/Install.sql Database schema creation script with 11 tables and sequences
src/Hangfire.Oracle.Core/Scripts/Uninstall.sql Schema cleanup script for removing all Hangfire objects
tests/Hangfire.Oracle.Core.Tests/Hangfire.Oracle.Core.Tests.csproj Test project configuration with xUnit dependencies
tests/Hangfire.Oracle.Core.Tests/OracleStorageTests.cs Unit tests for OracleStorage constructor validation and initialization
tests/Hangfire.Oracle.Core.Tests/OracleStorageOptionsTests.cs Unit tests for configuration options validation
tests/Hangfire.Oracle.Core.Tests/OracleStorageConnectionTests.cs Unit tests for storage connection creation
tests/Hangfire.Oracle.Core.Tests/OracleWriteOnlyTransactionTests.cs Unit tests for transaction constructor validation
tests/Hangfire.Oracle.Core.Tests/OracleMonitoringApiTests.cs Unit tests for monitoring API constructor validation
Hangfire.Oracle.sln Solution file organizing source and test projects
README.md Comprehensive documentation with usage examples and configuration reference
IMPLEMENTATION.md Detailed implementation summary with architecture and design decisions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 219 to 226
return servers.Select(x => new ServerDto
{
Name = x.ID,
Heartbeat = x.LAST_HEARTBEAT,
Queues = JobHelper.FromJson<string[]>(x.DATA) ?? Array.Empty<string>(),
StartedAt = DateTime.MinValue,
WorkersCount = 0
}).ToList();
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Servers method attempts to deserialize the DATA column as a string array, but the AnnounceServer method in OracleStorageConnection.cs serializes an anonymous object with WorkerCount, Queues, and StartedAt properties. This mismatch will cause deserialization to fail. The deserialization should match the serialization format.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the deserialization mismatch. The Servers() method now properly deserializes the server data using a ServerData DTO class that matches the serialization format in AnnounceServer(). This extracts WorkerCount, Queues, and StartedAt correctly instead of trying to deserialize as a string array. (commit: 1dacf7e)

Comment on lines 417 to 429
using var connection = _storage.CreateAndOpenConnection();

foreach (var pair in keyValuePairs)
{
connection.Execute(
$@"MERGE INTO {_storage.GetTableName("HASH")} h
USING (SELECT :key AS KEY_NAME, :field AS FIELD FROM DUAL) src
ON (h.KEY_NAME = src.KEY_NAME AND h.FIELD = src.FIELD)
WHEN MATCHED THEN UPDATE SET h.VALUE = :value
WHEN NOT MATCHED THEN INSERT (ID, KEY_NAME, FIELD, VALUE, EXPIRE_AT)
VALUES ({_storage.GetTableName("HASH_SEQ")}.NEXTVAL, :key, :field, :value, NULL)",
new { key, field = pair.Key, value = pair.Value });
}
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The SetRangeInHash method in OracleStorageConnection executes a MERGE statement for each key-value pair in a loop without any transaction management. If the operation fails partway through, the hash could be left in an inconsistent state. Consider wrapping these operations in a transaction or documenting that partial updates are acceptable.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added transaction management to SetRangeInHash. All MERGE operations now execute within a transaction with proper commit/rollback handling to ensure atomicity and prevent inconsistent state if any operation fails. (commit: 6f981de)

Comment on lines 43 to 60
while (fetchedJob == null)
{
cancellationToken.ThrowIfCancellationRequested();

foreach (var provider in providers)
{
fetchedJob = provider.Dequeue(cancellationToken);
if (fetchedJob != null)
break;
}

if (fetchedJob == null)
{
cancellationToken.WaitHandle.WaitOne(_storage.Options.QueuePollInterval);
}
}

return fetchedJob;
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The FetchNextJob method has an infinite loop that only exits when a job is fetched or the cancellation token is triggered. If all queues are consistently empty and no cancellation occurs, this will loop indefinitely. While the QueuePollInterval wait provides some throttling, consider adding additional safeguards such as a maximum retry count or logging to help diagnose issues in production.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added documentation explaining the intentional infinite loop design. The loop is part of Hangfire's architecture for continuous job polling and exits when a job is fetched or the cancellation token is triggered, with QueuePollInterval providing throttling. (commit: 6f981de)

(job, state) => new SucceededJobDto
{
Job = job,
Result = state.Data.ContainsKey("Result") ? state.Data["Result"] : null,
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inefficient use of 'ContainsKey' and indexer.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed inefficient dictionary access in ScheduledJobs. Changed from direct indexer access state.Data["EnqueueAt"] to TryGetValue pattern to avoid potential KeyNotFoundException and improve code safety. (commit: 6f981de)

{
Job = job,
Result = state.Data.ContainsKey("Result") ? state.Data["Result"] : null,
TotalDuration = state.Data.ContainsKey("PerformanceDuration") && state.Data.ContainsKey("Latency")
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inefficient use of 'ContainsKey' and indexer.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

{
private readonly OracleStorage _storage;
private readonly string _resource;
private readonly OracleConnection _connection;
Copy link

Copilot AI Jan 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable is manually disposed in a finally block - consider a C# using statement as a preferable resource management technique.

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback

Co-authored-by: wouternijenhuis <21281799+wouternijenhuis@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 22, 2026

@copilot add support for net10 and update to latest nuget package versions

Added .NET 10.0 support (multi-targeting with .NET 8.0) and updated all test packages to their latest versions. The core packages (Hangfire.Core, Dapper, Oracle.ManagedDataAccess.Core, Dapper.Oracle) were already at their latest stable versions. All 19 tests pass on both frameworks. (commit: 1136448)

@wouternijenhuis
Copy link
Owner

@copilot create a GitHub action workflow to create a release

Co-authored-by: wouternijenhuis <21281799+wouternijenhuis@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 22, 2026

@copilot create a GitHub action workflow to create a release

Created a complete GitHub Actions release workflow that:

✅ Triggers on version tags (e.g., v1.0.0)
✅ Builds for .NET 8.0 and .NET 10.0
✅ Runs all tests
✅ Creates NuGet packages with symbols
✅ Generates release notes from commits
✅ Creates GitHub Releases automatically
✅ Publishes to NuGet.org (optional, requires NUGET_API_KEY secret)

Also added:

  • Package metadata to the .csproj file
  • RELEASE.md with complete documentation

To create a release, simply tag and push:

git tag v1.0.0
git push origin v1.0.0

(commit: 1f912fe)

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • s1.symcb.com
    • Triggering command: /usr/bin/dotnet dotnet restore (dns block)
  • s2.symcb.com
    • Triggering command: /usr/bin/dotnet dotnet restore (dns block)
  • sv.symcb.com
    • Triggering command: /usr/bin/dotnet dotnet restore (dns block)
  • sv.symcd.com
    • Triggering command: /usr/bin/dotnet dotnet restore (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
wouternijenhuis and others added 2 commits January 22, 2026 20:02
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ion format

Co-authored-by: wouternijenhuis <21281799+wouternijenhuis@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 22, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • s1.symcb.com
    • Triggering command: /usr/bin/dotnet dotnet build --no-incremental (dns block)
  • s2.symcb.com
    • Triggering command: /usr/bin/dotnet dotnet build --no-incremental (dns block)
  • sv.symcb.com
    • Triggering command: /usr/bin/dotnet dotnet build --no-incremental (dns block)
  • sv.symcd.com
    • Triggering command: /usr/bin/dotnet dotnet build --no-incremental (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…lling loop, fix dictionary access

Co-authored-by: wouternijenhuis <21281799+wouternijenhuis@users.noreply.github.com>
Copy link
Contributor Author

Copilot AI commented Jan 22, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • s1.symcb.com
    • Triggering command: /usr/bin/dotnet dotnet build --no-incremental (dns block)
  • s2.symcb.com
    • Triggering command: /usr/bin/dotnet dotnet build --no-incremental (dns block)
  • sv.symcb.com
    • Triggering command: /usr/bin/dotnet dotnet build --no-incremental (dns block)
  • sv.symcd.com
    • Triggering command: /usr/bin/dotnet dotnet build --no-incremental (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Wouter Nijenhuis added 3 commits January 22, 2026 20:38
…ring capabilities

- Added models for StoredServer, StoredSet, and StoredState to represent database entities.
- Developed OracleStorage class for managing job storage and connections.
- Introduced OracleStorageOptions for configurable storage settings.
- Created job queue implementation with OracleQueue and OracleJobQueueProvider.
- Implemented queue monitoring with OracleQueueMonitor and IQueueMonitor interface.
- Added schema management with OracleSchemaManager for table creation and versioning.
- Enhanced job fetching and processing with FetchedJobContext and IJobQueue interfaces.
- Established QueueStatistics for monitoring job counts in queues.
- Updated test project dependencies and configurations.
- Improved OracleWriteOnlyTransaction to support asynchronous commits and better error handling.
- Added logging for transaction commits and errors.
- Optimized job queue fetching with support for Oracle 19c's SKIP LOCKED feature.
- Updated SQL installation script for better performance and compatibility with Oracle versions.
- Refactored tests to remove unnecessary dependencies and improve clarity.
…entation and SQL script to avoid reserved word conflict
@wouternijenhuis wouternijenhuis merged commit 946f5f7 into main Jan 22, 2026
@wouternijenhuis wouternijenhuis deleted the copilot/add-hangfire-oracle-storage branch January 22, 2026 20:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants