{"record":{"id":"4f997cb23368e31e","repo":"elsa-workflows/elsa-core","slug":"this-external-identity-provisioner-does-not-support-atomic","errorCode":null,"errorMessage":"This external identity provisioner does not support atomic link replacement.","messagePattern":"This external identity provisioner does not support atomic link replacement\\.","errorType":"exception","errorClass":"NotSupportedException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.ExternalAuthentication/Contracts/ExternalAuthenticationContracts.cs","lineNumber":172,"sourceCode":"\npublic interface IExternalIdentityProvisioner\n{\n    /// <summary>\n    /// Finds the link for a normalized external identity without exposing its persisted subject representation.\n    /// </summary>\n    ValueTask<ExternalIdentityLink?> FindLinkAsync(string tenantId, string connectionKey, ExternalIdentity identity, CancellationToken cancellationToken = default);\n\n    /// <summary>\n    /// Creates the requested link and, when requested, its credential-less user; compensates a losing writer; or returns the winner of a concurrent operation.\n    /// </summary>\n    ValueTask<ProvisioningResult> CreateLinkOrGetExistingAsync(ProvisioningRequest request, CancellationToken cancellationToken = default);\n\n    /// <summary>\n    /// Atomically removes the tenant-scoped link identified by <see cref=\"ExternalIdentityLinkReplaceRequest.LinkId\"/>\n    /// and creates its replacement, or returns the conflicting link without changing the original.\n    /// </summary>\n    ValueTask<ExternalIdentityLinkReplaceResult> ReplaceAsync(ExternalIdentityLinkReplaceRequest request, CancellationToken cancellationToken = default) =>\n        throw new NotSupportedException(\"This external identity provisioner does not support atomic link replacement.\");\n}\n\n/// <summary>\n/// Provides tenant-bounded administration of existing external identity links.\n/// Creation remains on <see cref=\"IExternalIdentityProvisioner\"/> so administrator prelinks and JIT provisioning use the same atomic tuple operation.\n/// </summary>\npublic interface IExternalIdentityLinkManagementStore\n{\n    ValueTask<Page<ExternalIdentityLink>> FindAsync(ExternalIdentityLinkFilter filter, CancellationToken cancellationToken = default);\n    ValueTask<bool> DeleteAsync(string tenantId, string linkId, CancellationToken cancellationToken = default);\n}\n\npublic interface IPermissionGrantSource\n{\n    string Type { get; }\n    PermissionGrantSourceDescriptor Describe();\n    ValueTask<PermissionGrantResult> GetGrantsAsync(PermissionGrantContext context, CancellationToken cancellationToken = default);\n}","sourceCodeStart":154,"sourceCodeEnd":190,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.ExternalAuthentication/Contracts/ExternalAuthenticationContracts.cs#L154-L190","documentation":"The default implementation of IExternalIdentityProvisionerAdministrator.ReplaceAsync is a NotSupportedException: atomic link replacement (remove one tenant-scoped link and create its replacement in one operation, or report the conflict unchanged) is an optional capability. Provisioners that cannot perform the swap atomically throw this to signal the caller must use the non-atomic remove+create path or a different provisioner.","triggerScenarios":"Calling ReplaceAsync on an IExternalIdentityProvisioner (cast to IExternalIdentityProvisionerAdministrator or injected as such) whose concrete implementation does not override the default interface method — i.e. any provisioner that did not opt into atomic link replacement.","commonSituations":"Admin tooling that assumes all provisioners support relinking a user to a different external identity; migrating identity providers where the old provisioner class predates the ReplaceAsync capability; generic administration UIs invoking ReplaceAsync uniformly across provisioners.","solutions":["Check capability before calling (e.g. whether the provisioner type overrides ReplaceAsync) and fall back to RemoveAsync + link creation in a compensating flow","Implement ReplaceAsync in your custom provisioner if atomic swap semantics are required","Update the admin flow to surface 'replacement not supported' for provisioners lacking the capability instead of letting it throw","Use a provisioner implementation that supports atomic replacement for the tenant in question"],"exampleFix":"// before\nvar result = await administrator.ReplaceAsync(request, ct); // throws NotSupportedException\n// after\nExternalIdentityLinkReplaceResult result;\ntry { result = await administrator.ReplaceAsync(request, ct); }\ncatch (NotSupportedException)\n{\n    await administrator.RemoveAsync(new(request.TenantId, request.LinkId), ct);\n    result = await provisioner.LinkAsync(request.CreateRequest, ct);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try { return await administrator.ReplaceAsync(request, ct); }\ncatch (NotSupportedException)\n{\n    // fall back: remove + create in a compensating sequence\n    await administrator.RemoveAsync(new(request.TenantId, request.LinkId), ct);\n    return await provisioner.LinkAsync(request.ToCreateRequest(), ct);\n}","preventionTips":["Feature-detect ReplaceAsync support per provisioner before exposing relink UI","Document which provisioners implement atomic replacement","Prefer provisioner implementations that override ReplaceAsync for admin workflows","Test link-replacement flows against every registered provisioner"],"tags":["identity","unsupported-operation"],"backgroundTag":"unsupported-operation","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}