{"record":{"id":"cbbfcc2ed1263e56","repo":"dotnet/orleans","slug":"journal-metadata-property-propertyname-cannot-cbbfcc","errorCode":null,"errorMessage":"Journal metadata property '{propertyName}' cannot be both set and removed.","messagePattern":"Journal metadata property '(.+?)' cannot be both set and removed\\.","errorType":"validation","errorClass":"ArgumentException","httpStatus":null,"severity":"error","filePath":"src/Azure/Orleans.Journaling.AzureStorage/AzureTableJournalStorage.cs","lineNumber":1128,"sourceCode":"        }\n\n        return result;\n    }\n\n    private static IReadOnlySet<string> CopyRemove(IEnumerable<string>? remove, IReadOnlyDictionary<string, string> set)\n    {\n        if (remove is null)\n        {\n            return new HashSet<string>(StringComparer.Ordinal);\n        }\n\n        var result = new HashSet<string>(StringComparer.Ordinal);\n        foreach (var propertyName in remove)\n        {\n            ValidateCallerMetadataPropertyName(propertyName);\n            if (set.ContainsKey(propertyName))\n            {\n                throw new ArgumentException($\"Journal metadata property '{propertyName}' cannot be both set and removed.\", nameof(remove));\n            }\n\n            result.Add(propertyName);\n        }\n\n        return result;\n    }\n\n    private static bool ApplyCallerMetadataUpdate(\n        Dictionary<string, string> metadata,\n        IReadOnlyDictionary<string, string> set,\n        IReadOnlySet<string> remove)\n    {\n        var changed = false;\n        foreach (var propertyName in remove)\n        {\n            changed |= metadata.Remove(propertyName);\n        }","sourceCodeStart":1110,"sourceCodeEnd":1146,"githubUrl":"https://github.com/dotnet/orleans/blob/fca799fa70ecb6ad975224271703ca43221f58de/src/Azure/Orleans.Journaling.AzureStorage/AzureTableJournalStorage.cs#L1110-L1146","documentation":"A metadata update passed the same property name in both the set dictionary and the remove set. CopyRemove (AzureTableJournalStorage.cs:1115) validates each removed key against the set dictionary and rejects the contradiction because the net effect would be ambiguous. The argument blamed is `remove`.","triggerScenarios":"Calling a journal metadata-update API (e.g., the method that calls CopyRemove) with overlapping keys in its set and remove parameters; thrown at AzureTableJournalStorage.cs:1128.","commonSituations":"Caller builds set and remove from the same source list; a UI applies a diff that marks a key as both changed and deleted; copying a key collection into both arguments by mistake.","solutions":["Disjoint the keys: remove a name from set before passing it in remove, or vice versa.","Decide per-key intent (set wins vs remove wins) and filter the other collection before the call.","Add a precondition check in calling code: `set.Keys.Intersect(remove).Any()` should be false."],"exampleFix":"// before\nstorage.UpdateMetadata(set: new() { [\"k\"] = \"v\" }, remove: new[] { \"k\" });\n\n// after\nvar set = new Dictionary<string, string> { [\"k\"] = \"v\" };\nvar remove = new[] { \"other\" };\nstorage.UpdateMetadata(set: set, remove: remove);","handlingStrategy":"validation","validationCode":"var overlap = set.Keys.Intersect(remove, StringComparer.Ordinal).ToList();\nif (overlap.Count > 0) throw new ArgumentException($\"Keys both set and removed: {string.Join(\", \", overlap)}\");","typeGuard":"static bool IsMetadataUpdateDisjoint(\n    IReadOnlyDictionary<string,string> set, IEnumerable<string> remove)\n    => !remove.Any(r => set.ContainsKey(r));","tryCatchPattern":null,"preventionTips":["Build set and remove from a single source of truth so a key has one intent.","Assert disjointness in a precondition check.","Unit-test metadata-diff logic for overlap."],"tags":["metadata","validation","api-usage"],"backgroundTag":null,"analyzedSha":"fca799fa70ecb6ad975224271703ca43221f58de","analyzedAt":"2026-08-13T19:55:57.938Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}