{"record":{"id":"e399ef749346e527","repo":"louthy/language-ext","slug":"hashable-attribute-should-have-a-struct-type-that-derives","errorCode":null,"errorMessage":"Hashable attribute should have a struct type that derives from LanguageExt.Traits.Hashable<> passed as its argument","messagePattern":"Hashable attribute should have a struct type that derives from LanguageExt\\.Traits\\.Hashable<> passed as its argument","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"LanguageExt.Core/DataTypes/Record/Attributes.cs","lineNumber":55,"sourceCode":"                 .ImplementedInterfaces\n                 .AsIterable()\n                 .Exists(i => i.ToString().StartsWith(\"LanguageExt.Traits.Ord`1\")))\n        {\n            throw new Exception(\"Ord attribute should have a struct type that derives from LanguageExt.Traits.Ord<> passed as its argument\");\n        }\n    }\n}\n\npublic class HashableAttribute : Attribute\n{\n    public HashableAttribute(Type type)\n    {\n        if (!type.GetTypeInfo()\n                 .ImplementedInterfaces\n                 .AsIterable()\n                 .Exists(i => i.ToString().StartsWith(\"LanguageExt.Traits.Hashable`1\")))\n        {\n            throw new Exception(\"Hashable attribute should have a struct type that derives from LanguageExt.Traits.Hashable<> passed as its argument\");\n        }\n    }\n}\n\n/// <summary>\n/// Stops the base type fields being used for any Record operations\n/// </summary>\n/// <remarks>This is *not* used for the [Record] code-gen</remarks>\npublic class IgnoreBaseAttribute : Attribute;\n","sourceCodeStart":37,"sourceCodeEnd":65,"githubUrl":"https://github.com/louthy/language-ext/blob/2f0e3628242889774d4141960a35671a0280051f/LanguageExt.Core/DataTypes/Record/Attributes.cs#L37-L65","documentation":"HashableAttribute.Validate checks that the type supplied to the [Hashable] attribute implements LanguageExt.Traits.Hashable<T>; if not it throws. Records use this type to compute hash codes, so an invalid argument breaks hashing of the record.","triggerScenarios":"Decorating a record with [Hashable(typeof(X))] where X's implemented interfaces contain nothing starting with \"LanguageExt.Traits.Hashable`1\" — e.g. X implements the legacy LanguageExt.Hashable<T> or no hashable interface at all.","commonSituations":"Upgrading from earlier LanguageExt versions with legacy Hashable<T>; passing a class instead of a struct; passing the record type itself rather than a dedicated hasher.","solutions":["Implement LanguageExt.Traits.Hashable<T> on the struct passed to [Hashable] and return its GetHashCode.","Migrate legacy hashers to the Traits namespace interface when upgrading LanguageExt.","Verify the argument is a struct comparer, not the record type or an unrelated type."],"exampleFix":"// before\n[Hashable(typeof(MyHash))]  // implements legacy Hashable<Rec>\npublic partial record Rec(int X);\n\n// after\npublic readonly struct MyHash : LanguageExt.Traits.Hashable<Rec>\n{\n    public int GetHashCode(Rec x) => x.X.GetHashCode();\n}\n[Hashable(typeof(MyHash))]\npublic partial record Rec(int X);","handlingStrategy":"validation","validationCode":"// before applying the attribute\ntypeof(MyHash).GetInterfaces().Any(i =>\n    i.IsGenericType && i.GetGenericTypeDefinition() == typeof(LanguageExt.Traits.Hashable<>));","typeGuard":"static bool IsValidHasher<T>(T t) => t is LanguageExt.Traits.Hashable<RecordType>;","tryCatchPattern":null,"preventionTips":["Implement LanguageExt.Traits.Hashable<T> explicitly on a dedicated struct.","Do not reuse legacy Hashable implementations across major upgrades without migration.","Smoke-test record hashing (e.g. add records to a HashSet) in CI."],"tags":["csharp","languageext","records","hashing","attributes"],"backgroundTag":"invalid-argument-value","analyzedSha":"2f0e3628242889774d4141960a35671a0280051f","analyzedAt":"2026-09-15T03:31:55.716Z","contentChangedAt":"2026-09-15T03:31:55.716Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}