Protect Yourself
I’ve been looking at F# to assess how well it plays with the .NET ecosystem and I’ve found a number of blemishes. Most recently, I found that F# has no equivalent to the C# protected access on members/classes. Why should we care about protected?
One of my favorite patterns as an author of an API is template pattern. In this pattern, a base class, typically abstract, defines the way that a process should happen but doesn’t define certain steps. These steps are left as an exercise to concrete implementations. The steps, like the steps in sausage making, are meant to stay out of the public eye.
At one point, I refactored the dotImage class ImageCommand to use that pattern. There are six steps in a typical image command, some optional/default, some not. Rather than make every command implement every single step on its own, I made a template that handles all this for you in the Apply() method (side note, Apply() is virtual, so if you don’t like it you can replace it). Most of the templated methods are protected since they need to be visible to subclasses, but shouldn’t be public. Many, but not all, have reasonable default implementations that handle 80% of all cases.
The problem is that if I implement an image command in F# all this protected methods magically become public. This is contrary to the language reference, which says:
The access specifier protected is not used in F#, although it is acceptable if you are using types authored in languages that do support protected access. Therefore, if you override a protected method, your method remains accessible only within the class and its descendents.
I have verified that this is false – override a protected member and it magically becomes public.
There is hope in the future – the language reserves the keyword ‘protected’ so I think someone has the intent of putting it in. Meantime, I’m left holding the bag. What to do?
One solution is to label things that need to be protected, disassemble the output, run a regular expression to search/replace the accessibility and reassemble. This is straight forward, but fragile. I’d use this only for a last-ditch solution.
I’d prefer to use an assembly editor of some kind. I initially looked at CCI and wrote a simple module visitor to do what I wanted, but my first test broke CCI – it’s just not ready yet. Instead, I tried out Cecil, part of the Mono project and found it worked fine. Here is my solution.
First, define a custom attribute for your code base that can be used to label protected members (at present, I have it configured to only be used on methods and constructors – I don’t have much used for protected fields, but I could see protected properties):
open System
[<AttributeUsage(AttributeTargets.Method ||| AttributeTargets.Constructor, AllowMultiple=false, Inherited=true)>]
type AtalaProtectedAttribute() =
inherit System.Attribute()
Next, create a small C# console application that uses Cecil to apply the change. Most of the app is error checking, so I’ll just paste in the meat (note: in the CLI, protected is called “family”):
ModuleDefinition module = ModuleDefinition.ReadModule(sourceFile);
foreach (TypeDefinition type in module.Types)
{
foreach (MethodDefinition method in type.Methods)
{
int attrIndex = attributeIndex(method.CustomAttributes);
if (attrIndex < 0)
continue;
method.CustomAttributes.RemoveAt(attrIndex);
if (method.IsPublic)
method.IsPublic = false;
if (method.IsPrivate)
method.IsPrivate = false;
method.IsFamily = true;
}
}
// and later
static int attributeIndex(Collection<CustomAttribute> coll)
{
if (coll == null)
return -1;
for (int i = 0; i < coll.Count; i++)
{
CustomAttribute attr = coll[i];
if (attr.AttributeType.Name == "AtalaProtectedAttribute")
return i;
}
return -1;
}
Finally, a call to module.Write() produces an assembly with protected access.
All that remains is the human error of neglecting to put in the [<AtalaProtected>] attribute. This is easily remedied with a unit test that reflects on all assemblies, finds classes that inherit and override protected members and have mismatched protection.