Internal And Protected

By agsmith

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.

6 Responses to “Internal And Protected”

  1. Karl Shifflett Says:

    Andrew,

    Deep, brother Deep!

    Your blog will quickly get a huge following.

    One problem. Mole forget to read your work and by-passes the scope attributes to display “hidden data”. :-)

    Best to you,

    Karl

  2. Internal And Protected Virtual « Andrew Smith Says:

    [...] Andrew Smith Random Thoughts about C#, VS, WinForms, WPF and .NET in General « Internal And Protected [...]

  3. Josh Smith Says:

    Holy smokes!! This is really amazing stuff. I agree with Karl, your blog is going to develop a cult-like following if you keep this up! :)

    Josh

  4. Alex Sarafian Says:

    Mr Smith recently I had the same problem. What I hadn’t realized was that intentionally protected internal scope allows class that inherit my class to use the protected internal member.

    I thought it was a bug. Now I realize that it is a not so good implementation in Object oriented approach. As i understand it a logic & applies to scopes.
    protected allows children class to access protected members of the father
    internal allows only class from the specific assebly to access specified members.

    So if you apply the & bitwise logic to protectd & internal, the natural conclution is what we both expected. That’s why i first thought of it as a bug.

    Personaly I believe that it is a huge problem, and being forced to used a solution like yours is not very appealing thought usefull if it comes to that.

    I should not be forced to use reflection nor by any means allow the consumer of my control understand that there are members that i have made tricks to protect from accessing.

    Thank you.

  5. agsmith Says:

    Hi Alex. Actually what you are describing – internal protected – is what the clr refers to as AssemblyOrFamily (or you might think of it as InternalOrProtected). This means that the member may be accessed by someone internally in the assembly OR by any derived class even one that is in another assembly. I described this in the blurb on scopes. The scope that is not supported currently is InternalAndProtected meaning that only members that are within that assembly AND derived from the class may access the member. I agree that we should not need to use reflection to do what the compiler should be handling for us. I merely offered the reflection approach as a way to try and enforce what is not currently supported.

  6. Alex Sarafian Says:

    I fully understood what you explained. I just thought from my expierence that two keywords interact with the AND logic, but from what you explained I was mistaken.

    Anyway thanks again for your answer.
    Nice solution though.

Leave a Reply