{"record":{"id":"b1f75f68b591a64b","repo":"HangfireIO/Hangfire","slug":"only-sqlservertimeoutjob-type-supports-transacti","errorCode":null,"errorMessage":"Only 'SqlServerTimeoutJob' type supports transactional acknowledge, '{fetchedJob.GetType().Name}' given.","messagePattern":"Only 'SqlServerTimeoutJob' type supports transactional acknowledge, '(.+?)' given\\.","errorType":"exception","errorClass":"NotSupportedException","httpStatus":null,"severity":"error","filePath":"src/Hangfire.SqlServer/SqlServerWriteOnlyTransaction.cs","lineNumber":556,"sourceCode":"        public override void RemoveFromQueue(IFetchedJob fetchedJob)\n        {\n            if (fetchedJob == null) throw new ArgumentNullException(nameof(fetchedJob));\n\n            if (fetchedJob is SqlServerTimeoutJob timeoutJob)\n            {\n                var query = _storage.GetQueryFromTemplate(static schemaName =>\n$@\"delete JQ from [{schemaName}].JobQueue JQ with (forceseek, rowlock) where Queue = @queue and Id = @id and FetchedAt = @fetchedAt\");\n\n                AddCommand(_queueCommands, timeoutJob.Queue, batch => batch.Create(query)\n                    .AddParameter(\"@queue\", timeoutJob.Queue, DbType.String)\n                    .AddParameter(\"@id\", timeoutJob.Id, DbType.Int64)\n                    .AddParameter(\"@fetchedAt\", timeoutJob.FetchedAt, DbType.DateTime));\n\n                timeoutJob.SetTransaction(this);\n            }\n            else\n            {\n                throw new NotSupportedException(\n                    \"Only '\" + nameof(SqlServerTimeoutJob) + \"' type supports transactional acknowledge, '\" + fetchedJob.GetType().Name + \"' given.\");\n            }\n        }\n\n        private static void AppendBatch<TKey>(\n            SortedDictionary<TKey, List<Func<DbConnection, DbCommand>>> collection,\n            SqlCommandBatch batch)\n        {\n            foreach (var pair in collection)\n            {\n                foreach (var command in pair.Value)\n                {\n                    var dbCommand = command(batch.Connection);\n                    batch.Append(dbCommand);\n                }\n            }\n        }\n","sourceCodeStart":538,"sourceCodeEnd":574,"githubUrl":"https://github.com/HangfireIO/Hangfire/blob/c236dd0f930f831ec151e436e138ddc429a02a72/src/Hangfire.SqlServer/SqlServerWriteOnlyTransaction.cs#L538-L574","documentation":"NotSupportedException thrown in SqlServerWriteOnlyTransaction.RemoveFromQueue (SqlServerWriteOnlyTransaction.cs:556) when the IFetchedJob passed in is not the internal SqlServerTimeoutJob type. This RemoveFromQueue overload is the transactional-acknowledge deletion path: SqlServerStorage registers the RemoveFromQueue(typeof(SqlServerTimeoutJob)) feature gated by UseTransactionalAcknowledge (SqlServerStorage.cs:517-519). Only SqlServerTimeoutJob carries the Queue/Id/FetchedAt fields needed for the parameterized DELETE and the SetTransaction wiring, so any other IFetchedJob implementation is rejected.","triggerScenarios":"Enable options.UseTransactionalAcknowledge = true and have a worker fetch a job from a non-default queue provider (MSMQ or a custom IPersistentJobQueueProvider) so the fetched job is not a SqlServerTimeoutJob; the transactional-acknowledge machinery then calls RemoveFromQueue with that foreign IFetchedJob and throws. Also reproducible by passing a custom IFetchedJob instance into RemoveFromQueue directly.","commonSituations":"Mixing the default SQL Server queue with MSMQ or custom queue providers while transactional acknowledge is on; a custom queue provider whose IFetchedJob implementation is fed into the SQL transaction's RemoveFromQueue; enabling UseTransactionalAcknowledge globally without checking that all queues are SQL-backed.","solutions":["Only enable UseTransactionalAcknowledge when all queues are served by the default SqlServerJobQueueProvider (so fetched jobs are SqlServerTimeoutJob).","Disable UseTransactionalAcknowledge for any queues backed by external/custom providers.","For a custom provider that must support transactional acknowledge, implement the removal within that provider rather than relying on SqlServerWriteOnlyTransaction.RemoveFromQueue.","Register the RemoveFromQueue feature in your provider only for the IFetchedJob type it actually produces."],"exampleFix":"// before\nvar options = new SqlServerStorageOptions { UseTransactionalAcknowledge = true };\n// but an external/custom queue provider is registered for some queue\n// after — disable transactional acknowledge when external providers are in use\nvar options = new SqlServerStorageOptions { UseTransactionalAcknowledge = false };","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"// Guard before calling transactional RemoveFromQueue:\nif (fetchedJob is Hangfire.SqlServer.SqlServerTimeoutJob sqlJob)\n{\n    transaction.RemoveFromQueue(fetchedJob);\n}\nelse\n{\n    // Use the provider-appropriate removal path; do not pass foreign IFetchedJob instances.\n    fetchedJob.RemoveFromQueue();\n}","tryCatchPattern":null,"preventionTips":["Enable UseTransactionalAcknowledge only when every queue is served by the default SQL Server provider.","Before mixing in an external/custom queue provider, disable transactional acknowledge for those queues.","Custom providers should implement their own transactional removal instead of relying on SqlServerWriteOnlyTransaction.RemoveFromQueue.","Register the RemoveFromQueue feature in a provider only for the concrete IFetchedJob type it produces."],"tags":["transactional-acknowledge","queue-provider","type-mismatch","notsupportedexception"],"backgroundTag":null,"analyzedSha":"c236dd0f930f831ec151e436e138ddc429a02a72","analyzedAt":"2026-08-13T20:27:11.027Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}