I like to make sure that the members of the types (and the types themselves) that I define are only available to the types that need it. This is done by providing the appropriate scope. C# and VB.Net define 5 possible scopes – public, private, protected, internal (friend in VB) and internal protected (protected friend in VB). I’ll give a brief background on each so if you already know this stuff you can skip ahead:
Public
Public members are visible to all types whether they are defined in the same assembly or within another assembly. So given a class A, anyone that has an instance of A can call its Foo method.
public class A
{
public void Foo() {}
}
Protected
Protected is sometimes referred to as Family in the CLR documentation. Basically protected means that only derived classes can access that member. So given a class A, only classes that derive from A can call its Foo method.
public class A
{
protected void Foo() { }
}
Private
Private members are only available within that class. So given a class A, only class A can call its Foo method.
public class A
{
private void Foo() { }
}
Assembly
Assembly scope is referred to as internal in C# and Friend in VB.Net. Assembly scoped members can be accessed by any type defined within the same assembly. So if you have assembly 1 with a class defined as follows, any other type defined within assembly 1 may access the Foo method but types defined in other assemblies cannot.
public class A
{
internal void Foo() { }
}
Assembly Or Protected
Assembly Or Protected combines the scopes of Assembly and Protected. In C# this would be defined as internal protected and in VB.Net the member would be defined as Protected Friend. Members with this scope can be access by any type within the same assembly or types in other assemblies that derive from this type. So if you have assembly 1 with a class defined as follows, any other type defined within assembly 1 may access the Foo method and also any type that derives from A (including types defined in an assembly other than assembly 1) may access the Foo method.
public class A
{
internal protected void Foo() { }
}
But there’s actually another scope – Assembly And Family. I used the terminology that’s used in the CLR documentation because it’s not available in C# or VB.Net. This scope is supposed to limit member access to only derived types within that assembly. Outside of that assembly or to non-derived types in the same assembly, it is as if that member doesn’t exist.
So if you have class A in assembly 1 that has a method named Foo with this scope, only classes within assembly 1 that derive from class A may access the Foo method. Personally I think this would be a useful scope to support. I even asked Jeffrey Richter at the PDC about the possibility of supporting it and he basically told me it wouldn’t happen and that I was the only person that had asked him that question. I was a little suprised by this. I spend a lot of time in reflector and there seem to be lots of cases where they could have used this type of functionality themselves.
Without this scope, you are forced to use internal instead and try to manage this via guidelines/standards. I can think of a couple of possible approaches that you could use to verify that this is the case – you could write an fxcop/static analysis rule that does the check or you could use reflection. The following method is a simple take on the latter. I’ll list the code first and then explain how it works.
public static class Utilities
{
[MethodImpl(MethodImplOptions.NoInlining)]
[Conditional("DEBUG")]
public static void VerifyCallerIsFamily()
{
// get the method doing the check
StackFrame sfCallee = new StackFrame(1, false);
MethodBase calleeMethod = sfCallee.GetMethod();
StackFrame sfCaller = new StackFrame(2, false);
MethodBase callerMethod = sfCaller.GetMethod();
Debug.Assert(calleeMethod.IsAssembly, “This method is meant to try and implement a scope of ‘Assembly And Family’ so the calling method should be internal.”);
if (false == calleeMethod.DeclaringType.IsAssignableFrom(callerMethod.DeclaringType))
{
const string Format = “The ‘{0}.{1}’ method is being called from ‘{2}.{3}’. It should only be called by derived types.”;
string message = string.Format(Format,
calleeMethod.DeclaringType.Name,
calleeMethod.Name,
callerMethod.DeclaringType.Name,
callerMethod.Name);
throw new InvalidOperationException(message);
}
}
}
The following will be our test scenario classes:
public class Base
{
internal void OnlyCallFromDerivedClasses()
{
Utilities.VerifyCallerIsFamily();
// do something
}
}
public class Derived : Base
{
public void VerifyCanCallMethod()
{
this.OnlyCallFromDerivedClasses();
}
}
public class NotDerived
{
public void VerifyCannotCallMethod()
{
Base b = new Base();
b.OnlyCallFromDerivedClasses();
}
}
We could then test this out:
Derived d = new Derived();
d.VerifyCanCallMethod();
NotDerived not = new NotDerived();
not.VerifyCannotCallMethod();
The call to VerifyCanCallMethod on d will pass because Derived is a derived class and has the rights to make this call. The second call will result in an exception – “The ‘Base.OnlyCallFromDerivedClasses’ method is being called from ‘NotDerived.VerifyCannotCallMethod’. It should only be called by derived types.” – which is the behavior that we want.
Ok so how does this work. The majority of the code relies upon the use of the StackFrame class to obtain the information about the method requesting the verification (the callee) and the method that is calling that method (the caller). We have to pass in 1, since this is implemented as a helper method. Passing in 0 would return the VerifyCallerIsFamily method which we do not want.
Once we have the stackframes, we can get the MethodBase instances. These are reflection objects that provide information about the method being called. We can then use the IsAssignableFrom method to ensure that the caller is either the same type as the callee or a derived class. If it is not we raise an appropriate exception.
You may notice that I decorated the VerifyCallerIsFamily with 2 attributes. The MethodImpl attribute is used to ensure that the jitter will not inline the execution of the method. The method is a little large so its unlikely that it would but its best to make sure. We need to do this because we’re relying on getting the methods using specific indexes in the stack frame. The Conditional attribute is used to indicate that calls to this method should not be included unless the code is being compiled with the DEBUG compilation constant. In other words, we only want to do this check in a debug version. My main reason for doing this is that using a StackFrame has overhead and once we release our assembly, its really not necessary to do this check since the callee method is internal and our testing can be limited to checking calls within the same assembly.
I still hope they implement that scope in a future version of C# and VB.Net but until then this approach may help you to get close.